diff --git a/www/docs/pages/clients/basics.mdx b/www/docs/pages/clients/basics.mdx new file mode 100644 index 000000000..d8a709996 --- /dev/null +++ b/www/docs/pages/clients/basics.mdx @@ -0,0 +1,773 @@ +# Client Basics + +Learn the fundamentals of creating and managing MCP clients, including lifecycle management, initialization, and error handling. + +## Creating Clients + +MCP-Go provides client constructors for each supported transport. The choice of transport determines how your client communicates with the server. + +### Client Constructor Patterns + +```go +// STDIO client - for command-line tools +client, err := client.NewStdioClient("command", "arg1", "arg2") + +// HTTP client - for web services +client := client.NewHTTPClient("http://localhost:8080/mcp") + +// SSE client - for real-time web applications +client := client.NewSSEClient("http://localhost:8080/mcp/sse") + +// In-process client - for testing and embedded scenarios +client := client.NewInProcessClient(server) +``` + +### STDIO Client Creation + +```go +package main + +import ( + "context" + "errors" + "fmt" + "log" + "math" + "net/http" + "sync" + "time" + + "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/mcp" +) + +func createStdioClient() (client.Client, error) { + // Create client that spawns a subprocess + c, err := client.NewStdioClient( + "go", "run", "/path/to/server/main.go", + ) + if err != nil { + return nil, fmt.Errorf("failed to create STDIO client: %w", err) + } + + return c, nil +} + +// With custom environment variables +func createStdioClientWithEnv() (client.Client, error) { + c, err := client.NewStdioClientWithOptions(client.StdioOptions{ + Command: "go", + Args: []string{"run", "/path/to/server/main.go"}, + Env: []string{ + "LOG_LEVEL=debug", + "DATABASE_URL=sqlite://test.db", + }, + WorkingDir: "/path/to/server", + Timeout: 30 * time.Second, + }) + if err != nil { + return nil, fmt.Errorf("failed to create STDIO client: %w", err) + } + + return c, nil +} +``` + +### HTTP Client Creation + +```go +func createHTTPClient() client.Client { + // Basic HTTP client + c := client.NewHTTPClient("http://localhost:8080/mcp") + + // Set custom headers + c.SetHeader("Authorization", "Bearer your-token") + c.SetHeader("X-API-Version", "v1") + + // Set timeout + c.SetTimeout(30 * time.Second) + + return c +} + +// With custom HTTP client +func createCustomHTTPClient() client.Client { + httpClient := &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + }, + } + + return client.NewHTTPClientWithClient("http://localhost:8080/mcp", httpClient) +} +``` + +### SSE Client Creation + +```go +func createSSEClient() client.Client { + // Basic SSE client + c := client.NewSSEClient("http://localhost:8080/mcp/sse") + + // Set authentication + c.SetHeader("Authorization", "Bearer your-token") + + return c +} + +// With custom options +func createSSEClientWithOptions() client.Client { + options := client.SSEOptions{ + URL: "http://localhost:8080/mcp/sse", + Headers: map[string]string{ + "Authorization": "Bearer your-token", + "X-Client-ID": "my-app", + }, + ReconnectInterval: 5 * time.Second, + MaxReconnects: 10, + BufferSize: 1000, + } + + return client.NewSSEClientWithOptions(options) +} +``` + +## Client Lifecycle + +Understanding the client lifecycle is crucial for proper resource management and error handling. + +### Lifecycle Stages + +1. **Creation** - Instantiate the client +2. **Initialization** - Establish connection and exchange capabilities +3. **Operation** - Use tools, resources, and prompts +4. **Cleanup** - Close connections and free resources + +### Complete Lifecycle Example + +```go +func demonstrateClientLifecycle() error { + // 1. Creation + c, err := client.NewStdioClient("server-command") + if err != nil { + return fmt.Errorf("client creation failed: %w", err) + } + + // Ensure cleanup happens + defer func() { + if closeErr := c.Close(); closeErr != nil { + log.Printf("Error closing client: %v", closeErr) + } + }() + + ctx := context.Background() + + // 2. Initialization + if err := c.Initialize(ctx); err != nil { + return fmt.Errorf("client initialization failed: %w", err) + } + + // 3. Operation + if err := performClientOperations(ctx, c); err != nil { + return fmt.Errorf("client operations failed: %w", err) + } + + // 4. Cleanup (handled by defer) + return nil +} + +func performClientOperations(ctx context.Context, c client.Client) error { + // List available tools + tools, err := c.ListTools(ctx) + if err != nil { + return err + } + + log.Printf("Found %d tools", len(tools.Tools)) + + // Use the tools + for _, tool := range tools.Tools { + result, err := c.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolRequestParams{ + Name: tool.Name, + Arguments: map[string]interface{}{ + "input": "example input", + "format": "json", + }, + }, + }) + if err != nil { + log.Printf("Tool %s failed: %v", tool.Name, err) + continue + } + + log.Printf("Tool %s result: %+v", tool.Name, result) + } + + return nil +} +``` + +### Initialization Process + +The initialization process establishes the MCP connection and exchanges capabilities: + +```go +func initializeClientWithDetails(ctx context.Context, c client.Client) error { + // Initialize with custom client info + initReq := mcp.InitializeRequest{ + Params: mcp.InitializeRequestParams{ + ProtocolVersion: "2024-11-05", + Capabilities: mcp.ClientCapabilities{ + Tools: &mcp.ToolsCapability{}, + Resources: &mcp.ResourcesCapability{}, + Prompts: &mcp.PromptsCapability{}, + }, + ClientInfo: mcp.ClientInfo{ + Name: "My Application", + Version: "1.0.0", + }, + }, + } + + result, err := c.InitializeWithRequest(ctx, initReq) + if err != nil { + return fmt.Errorf("initialization failed: %w", err) + } + + log.Printf("Connected to server: %s v%s", + result.ServerInfo.Name, + result.ServerInfo.Version) + + log.Printf("Server capabilities: %+v", result.Capabilities) + + return nil +} +``` + +### Graceful Shutdown + +```go +type ManagedClient struct { + client client.Client + ctx context.Context + cancel context.CancelFunc + done chan struct{} +} + +func NewManagedClient(clientType, address string) (*ManagedClient, error) { + var c client.Client + var err error + + switch clientType { + case "stdio": + c, err = client.NewStdioClient("server-command") + case "http": + c = client.NewHTTPClient(address) + case "sse": + c = client.NewSSEClient(address) + default: + return nil, fmt.Errorf("unknown client type: %s", clientType) + } + + if err != nil { + return nil, err + } + + ctx, cancel := context.WithCancel(context.Background()) + + mc := &ManagedClient{ + client: c, + ctx: ctx, + cancel: cancel, + done: make(chan struct{}), + } + + // Initialize in background + go func() { + defer close(mc.done) + if err := c.Initialize(ctx); err != nil { + log.Printf("Client initialization failed: %v", err) + } + }() + + return mc, nil +} + +func (mc *ManagedClient) WaitForReady(timeout time.Duration) error { + select { + case <-mc.done: + return nil + case <-time.After(timeout): + return fmt.Errorf("client initialization timeout") + case <-mc.ctx.Done(): + return mc.ctx.Err() + } +} + +func (mc *ManagedClient) Close() error { + mc.cancel() + + // Wait for initialization to complete or timeout + select { + case <-mc.done: + case <-time.After(5 * time.Second): + log.Println("Timeout waiting for client shutdown") + } + + return mc.client.Close() +} +``` + +## Error Handling + +Proper error handling is essential for robust client applications. + +### Error Types + +```go +// Connection errors +var ( + ErrConnectionFailed = errors.New("connection failed") + ErrConnectionLost = errors.New("connection lost") + ErrTimeout = errors.New("operation timeout") +) + +// Protocol errors +var ( + ErrInvalidResponse = errors.New("invalid response") + ErrProtocolViolation = errors.New("protocol violation") + ErrUnsupportedVersion = errors.New("unsupported protocol version") +) + +// Operation errors +var ( + ErrToolNotFound = errors.New("tool not found") + ErrResourceNotFound = errors.New("resource not found") + ErrInvalidArguments = errors.New("invalid arguments") + ErrPermissionDenied = errors.New("permission denied") +) +``` + +### Comprehensive Error Handling + +```go +func handleClientErrors(ctx context.Context, c client.Client) { + result, err := c.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolRequestParams{ + Name: "example_tool", + Arguments: map[string]interface{}{ + "param": "value", + }, + }, + }) + + if err != nil { + switch { + // Connection errors - may be recoverable + case errors.Is(err, client.ErrConnectionLost): + log.Println("Connection lost, attempting reconnect...") + if reconnectErr := reconnectClient(c); reconnectErr != nil { + log.Printf("Reconnection failed: %v", reconnectErr) + return + } + // Retry the operation + return handleClientErrors(ctx, c) + + case errors.Is(err, client.ErrTimeout): + log.Println("Operation timed out, retrying with longer timeout...") + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + return handleClientErrors(ctx, c) + + // Protocol errors - usually not recoverable + case errors.Is(err, client.ErrProtocolViolation): + log.Printf("Protocol violation: %v", err) + return + + case errors.Is(err, client.ErrUnsupportedVersion): + log.Printf("Unsupported protocol version: %v", err) + return + + // Operation errors - check and fix request + case errors.Is(err, client.ErrToolNotFound): + log.Printf("Tool not found: %v", err) + // Maybe list available tools and suggest alternatives + suggestAlternativeTools(ctx, c) + return + + case errors.Is(err, client.ErrInvalidArguments): + log.Printf("Invalid arguments: %v", err) + // Maybe get tool schema and show required parameters + showToolSchema(ctx, c, "example_tool") + return + + case errors.Is(err, client.ErrPermissionDenied): + log.Printf("Permission denied: %v", err) + // Maybe prompt for authentication + return + + // Unknown errors + default: + log.Printf("Unexpected error: %v", err) + return + } + } + + // Process successful result + log.Printf("Tool result: %+v", result) +} + +func reconnectClient(c client.Client) error { + // Close existing connection + if err := c.Close(); err != nil { + log.Printf("Error closing client: %v", err) + } + + // Wait before reconnecting + time.Sleep(1 * time.Second) + + // Reinitialize + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + return c.Initialize(ctx) +} + +func suggestAlternativeTools(ctx context.Context, c client.Client) { + tools, err := c.ListTools(ctx) + if err != nil { + log.Printf("Failed to list tools: %v", err) + return + } + + log.Println("Available tools:") + for _, tool := range tools.Tools { + log.Printf("- %s: %s", tool.Name, tool.Description) + } +} + +func showToolSchema(ctx context.Context, c client.Client, toolName string) { + tools, err := c.ListTools(ctx) + if err != nil { + log.Printf("Failed to list tools: %v", err) + return + } + + for _, tool := range tools.Tools { + if tool.Name == toolName { + log.Printf("Tool schema for %s:", toolName) + log.Printf("Description: %s", tool.Description) + log.Printf("Input schema: %+v", tool.InputSchema) + return + } + } + + log.Printf("Tool %s not found", toolName) +} +``` + +### Retry Logic with Exponential Backoff + +```go +type RetryConfig struct { + MaxRetries int + InitialDelay time.Duration + MaxDelay time.Duration + BackoffFactor float64 + RetryableErrors []error +} + +func DefaultRetryConfig() RetryConfig { + return RetryConfig{ + MaxRetries: 3, + InitialDelay: 1 * time.Second, + MaxDelay: 30 * time.Second, + BackoffFactor: 2.0, + RetryableErrors: []error{ + client.ErrConnectionLost, + client.ErrTimeout, + client.ErrConnectionFailed, + }, + } +} + +func (rc RetryConfig) IsRetryable(err error) bool { + for _, retryableErr := range rc.RetryableErrors { + if errors.Is(err, retryableErr) { + return true + } + } + return false +} + +func WithRetry[T any](ctx context.Context, config RetryConfig, operation func() (T, error)) (T, error) { + var lastErr error + var zero T + + for attempt := 0; attempt <= config.MaxRetries; attempt++ { + result, err := operation() + if err == nil { + return result, nil + } + + lastErr = err + + // Don't retry non-retryable errors + if !config.IsRetryable(err) { + break + } + + // Don't retry on last attempt + if attempt == config.MaxRetries { + break + } + + // Calculate delay with exponential backoff + delay := time.Duration(float64(config.InitialDelay) * math.Pow(config.BackoffFactor, float64(attempt))) + if delay > config.MaxDelay { + delay = config.MaxDelay + } + + log.Printf("Attempt %d failed, retrying in %v: %v", attempt+1, delay, err) + + // Wait with context cancellation support + select { + case <-time.After(delay): + case <-ctx.Done(): + return zero, ctx.Err() + } + } + + return zero, fmt.Errorf("failed after %d attempts: %w", config.MaxRetries+1, lastErr) +} + +// Usage example +func callToolWithRetry(ctx context.Context, c client.Client, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + config := DefaultRetryConfig() + + return WithRetry(ctx, config, func() (*mcp.CallToolResult, error) { + return c.CallTool(ctx, req) + }) +} +``` + +### Context and Timeout Management + +```go +func demonstrateContextUsage(c client.Client) { + // Operation with timeout + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + result, err := c.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolRequestParams{ + Name: "long_running_tool", + Arguments: map[string]interface{}{ + "duration": 60, // seconds + }, + }, + }) + + if err != nil { + if errors.Is(err, context.DeadlineExceeded) { + log.Println("Tool call timed out") + } else { + log.Printf("Tool call failed: %v", err) + } + return + } + + log.Printf("Tool completed: %+v", result) +} + +func demonstrateCancellation(c client.Client) { + ctx, cancel := context.WithCancel(context.Background()) + + // Start operation in goroutine + go func() { + result, err := c.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolRequestParams{ + Name: "long_running_tool", + }, + }) + + if err != nil { + if errors.Is(err, context.Canceled) { + log.Println("Tool call was cancelled") + } else { + log.Printf("Tool call failed: %v", err) + } + return + } + + log.Printf("Tool completed: %+v", result) + }() + + // Cancel after 5 seconds + time.Sleep(5 * time.Second) + cancel() + + // Wait a bit to see the cancellation + time.Sleep(1 * time.Second) +} +``` + +## Connection Monitoring + +### Health Checks + +```go +type ClientHealthMonitor struct { + client client.Client + interval time.Duration + timeout time.Duration + healthy bool + mutex sync.RWMutex +} + +func NewClientHealthMonitor(c client.Client, interval, timeout time.Duration) *ClientHealthMonitor { + return &ClientHealthMonitor{ + client: c, + interval: interval, + timeout: timeout, + healthy: false, + } +} + +func (chm *ClientHealthMonitor) Start(ctx context.Context) { + ticker := time.NewTicker(chm.interval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + chm.checkHealth(ctx) + } + } +} + +func (chm *ClientHealthMonitor) checkHealth(ctx context.Context) { + ctx, cancel := context.WithTimeout(ctx, chm.timeout) + defer cancel() + + // Try to list tools as a health check + _, err := chm.client.ListTools(ctx) + + chm.mutex.Lock() + chm.healthy = (err == nil) + chm.mutex.Unlock() + + if err != nil { + log.Printf("Health check failed: %v", err) + } +} + +func (chm *ClientHealthMonitor) IsHealthy() bool { + chm.mutex.RLock() + defer chm.mutex.RUnlock() + return chm.healthy +} +``` + +### Connection Recovery + +```go +type ResilientClient struct { + factory func() (client.Client, error) + client client.Client + mutex sync.RWMutex + recovering bool +} + +func NewResilientClient(factory func() (client.Client, error)) *ResilientClient { + return &ResilientClient{ + factory: factory, + } +} + +func (rc *ResilientClient) ensureConnected(ctx context.Context) error { + rc.mutex.RLock() + if rc.client != nil && !rc.recovering { + rc.mutex.RUnlock() + return nil + } + rc.mutex.RUnlock() + + rc.mutex.Lock() + defer rc.mutex.Unlock() + + // Double-check after acquiring write lock + if rc.client != nil && !rc.recovering { + return nil + } + + rc.recovering = true + defer func() { rc.recovering = false }() + + // Close existing client if any + if rc.client != nil { + rc.client.Close() + } + + // Create new client + newClient, err := rc.factory() + if err != nil { + return fmt.Errorf("failed to create client: %w", err) + } + + // Initialize new client + if err := newClient.Initialize(ctx); err != nil { + newClient.Close() + return fmt.Errorf("failed to initialize client: %w", err) + } + + rc.client = newClient + return nil +} + +func (rc *ResilientClient) CallTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + if err := rc.ensureConnected(ctx); err != nil { + return nil, err + } + + rc.mutex.RLock() + client := rc.client + rc.mutex.RUnlock() + + result, err := client.CallTool(ctx, req) + if err != nil && isConnectionError(err) { + // Mark for recovery and retry once + rc.mutex.Lock() + rc.recovering = true + rc.mutex.Unlock() + + if retryErr := rc.ensureConnected(ctx); retryErr != nil { + return nil, fmt.Errorf("recovery failed: %w", retryErr) + } + + rc.mutex.RLock() + client = rc.client + rc.mutex.RUnlock() + + return client.CallTool(ctx, req) + } + + return result, err +} + +func isConnectionError(err error) bool { + return errors.Is(err, client.ErrConnectionLost) || + errors.Is(err, client.ErrConnectionFailed) +} +``` + +## Next Steps + +- **[Client Operations](/clients/operations)** - Learn to use tools, resources, and prompts +- **[Client Transports](/clients/transports)** - Explore transport-specific features \ No newline at end of file diff --git a/www/docs/pages/clients/index.mdx b/www/docs/pages/clients/index.mdx new file mode 100644 index 000000000..40882c4b6 --- /dev/null +++ b/www/docs/pages/clients/index.mdx @@ -0,0 +1,445 @@ +# Building MCP Clients + +Learn how to build MCP clients that connect to and interact with MCP servers. This section covers client creation, operations, and transport-specific implementations. + +## Overview + +MCP clients connect to servers to access tools, resources, and prompts. MCP-Go provides client implementations for all supported transports, making it easy to integrate MCP functionality into your applications. + +## What You'll Learn + +- **[Client Basics](/clients/basics)** - Creating and managing client lifecycle +- **[Client Operations](/clients/operations)** - Using tools, resources, and prompts +- **[Client Transports](/clients/transports)** - Transport-specific client implementations + +## Quick Example + +Here's a complete example showing how to create and use an MCP client: + +```go +package main + +import ( + "context" + "fmt" + "log" + + "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/mcp" +) + +func main() { + // Create STDIO client + c, err := client.NewStdioClient( + "go", "run", "/path/to/server/main.go", + ) + if err != nil { + log.Fatal(err) + } + defer c.Close() + + ctx := context.Background() + + // Initialize the connection + if err := c.Initialize(ctx); err != nil { + log.Fatal(err) + } + + // Discover available capabilities + if err := demonstrateClientOperations(ctx, c); err != nil { + log.Fatal(err) + } +} + +func demonstrateClientOperations(ctx context.Context, c client.Client) error { + // List available tools + tools, err := c.ListTools(ctx) + if err != nil { + return fmt.Errorf("failed to list tools: %w", err) + } + + fmt.Printf("Available tools: %d\n", len(tools.Tools)) + for _, tool := range tools.Tools { + fmt.Printf("- %s: %s\n", tool.Name, tool.Description) + } + + // List available resources + resources, err := c.ListResources(ctx) + if err != nil { + return fmt.Errorf("failed to list resources: %w", err) + } + + fmt.Printf("\nAvailable resources: %d\n", len(resources.Resources)) + for _, resource := range resources.Resources { + fmt.Printf("- %s: %s\n", resource.URI, resource.Name) + } + + // Call a tool if available + if len(tools.Tools) > 0 { + tool := tools.Tools[0] + fmt.Printf("\nCalling tool: %s\n", tool.Name) + + result, err := c.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolRequestParams{ + Name: tool.Name, + Arguments: map[string]interface{}{ + "input": "example input", + "format": "text", + }, + }, + }) + if err != nil { + return fmt.Errorf("tool call failed: %w", err) + } + + fmt.Printf("Tool result: %+v\n", result) + } + + // Read a resource if available + if len(resources.Resources) > 0 { + resource := resources.Resources[0] + fmt.Printf("\nReading resource: %s\n", resource.URI) + + content, err := c.ReadResource(ctx, mcp.ReadResourceRequest{ + Params: mcp.ReadResourceRequestParams{ + URI: resource.URI, + }, + }) + if err != nil { + return fmt.Errorf("resource read failed: %w", err) + } + + fmt.Printf("Resource content: %+v\n", content) + } + + return nil +} +``` + +## Client Types by Transport + +### STDIO Client +**Best for:** +- Command-line applications +- Desktop software integration +- Local development and testing +- Single-server connections + +```go +// Create STDIO client +client, err := client.NewStdioClient("server-command", "arg1", "arg2") +``` + +### HTTP Client +**Best for:** +- Web applications +- Microservice architectures +- Load-balanced deployments +- REST-like interactions + +```go +// Create HTTP client +client := client.NewHTTPClient("http://localhost:8080/mcp") +``` + +### SSE Client +**Best for:** +- Real-time web applications +- Browser-based interfaces +- Streaming data scenarios +- Multi-client environments + +```go +// Create SSE client +client := client.NewSSEClient("http://localhost:8080/mcp/sse") +``` + +### In-Process Client +**Best for:** +- Testing and development +- Embedded scenarios +- High-performance applications +- Library integrations + +```go +// Create in-process client +client := client.NewInProcessClient(server) +``` + +## Common Client Patterns + +### Connection Management + +```go +import ( + "context" + "errors" + "fmt" + "log" + "sync" + "time" + + "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/mcp" +) + +type MCPClientManager struct { + client client.Client + ctx context.Context + cancel context.CancelFunc +} + +func NewMCPClientManager(clientType, address string) (*MCPClientManager, error) { + var c client.Client + var err error + + switch clientType { + case "stdio": + c, err = client.NewStdioClient("server-command") + case "http": + c = client.NewHTTPClient(address) + case "sse": + c = client.NewSSEClient(address) + default: + return nil, fmt.Errorf("unknown client type: %s", clientType) + } + + if err != nil { + return nil, err + } + + ctx, cancel := context.WithCancel(context.Background()) + + manager := &MCPClientManager{ + client: c, + ctx: ctx, + cancel: cancel, + } + + // Initialize connection + if err := c.Initialize(ctx); err != nil { + cancel() + return nil, fmt.Errorf("failed to initialize client: %w", err) + } + + return manager, nil +} + +func (m *MCPClientManager) Close() error { + m.cancel() + return m.client.Close() +} +``` + +### Error Handling + +```go +func handleClientErrors(ctx context.Context, c client.Client) { + // Tool call with error handling + result, err := c.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolRequestParams{ + Name: "example_tool", + Arguments: map[string]interface{}{ + "param": "value", + }, + }, + }) + + if err != nil { + switch { + case errors.Is(err, client.ErrConnectionLost): + log.Println("Connection lost, attempting reconnect...") + // Implement reconnection logic + case errors.Is(err, client.ErrToolNotFound): + log.Printf("Tool not found: %v", err) + case errors.Is(err, client.ErrInvalidArguments): + log.Printf("Invalid arguments: %v", err) + default: + log.Printf("Unexpected error: %v", err) + } + return + } + + // Process successful result + processToolResult(result) +} +``` + +### Retry Logic + +```go +func callToolWithRetry(ctx context.Context, c client.Client, req mcp.CallToolRequest, maxRetries int) (*mcp.CallToolResult, error) { + var lastErr error + + for attempt := 0; attempt <= maxRetries; attempt++ { + result, err := c.CallTool(ctx, req) + if err == nil { + return result, nil + } + + lastErr = err + + // Don't retry certain errors + if errors.Is(err, client.ErrInvalidArguments) || + errors.Is(err, client.ErrToolNotFound) { + break + } + + // Exponential backoff + if attempt < maxRetries { + backoff := time.Duration(1< rc.ttl { + return nil, false + } + + return entry.result, true +} + +func (rc *ResourceCache) Set(uri string, result *mcp.ReadResourceResult) { + rc.mutex.Lock() + defer rc.mutex.Unlock() + + rc.cache[uri] = cacheEntry{ + result: result, + timestamp: time.Now(), + } +} + +func (rc *ResourceCache) ReadResource(ctx context.Context, c client.Client, uri string) (*mcp.ReadResourceResult, error) { + // Check cache first + if cached, found := rc.Get(uri); found { + return cached, nil + } + + // Read from server + result, err := readResource(ctx, c, uri) + if err != nil { + return nil, err + } + + // Cache the result + rc.Set(uri, result) + return result, nil +} +``` + +## Calling Tools + +Tools provide functionality that can be invoked with parameters. + +### Basic Tool Calling + +```go +func callTool(ctx context.Context, c client.Client, name string, args map[string]interface{}) (*mcp.CallToolResult, error) { + result, err := c.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolRequestParams{ + Name: name, + Arguments: args, + }, + }) + if err != nil { + return nil, fmt.Errorf("tool call failed: %w", err) + } + + return result, nil +} + +func demonstrateToolCalling(ctx context.Context, c client.Client) { + // List available tools + tools, err := c.ListTools(ctx) + if err != nil { + log.Printf("Failed to list tools: %v", err) + return + } + + fmt.Printf("Available tools: %d\n", len(tools.Tools)) + for _, tool := range tools.Tools { + fmt.Printf("- %s: %s\n", tool.Name, tool.Description) + } + + // Call a specific tool + if len(tools.Tools) > 0 { + tool := tools.Tools[0] + fmt.Printf("\nCalling tool: %s\n", tool.Name) + + result, err := callTool(ctx, c, tool.Name, map[string]interface{}{ + "input": "example input", + "format": "text", + }) + if err != nil { + log.Printf("Tool call failed: %v", err) + return + } + + fmt.Printf("Tool result:\n") + for i, content := range result.Content { + fmt.Printf("Content %d (%s): %s\n", i+1, content.Type, content.Text) + } + } +} +``` + +### Tool Schema Validation + +```go +func validateToolArguments(tool mcp.Tool, args map[string]interface{}) error { + schema := tool.InputSchema + + // Check required properties + if schema.Required != nil { + for _, required := range schema.Required { + if _, exists := args[required]; !exists { + return fmt.Errorf("missing required argument: %s", required) + } + } + } + + // Validate argument types + if schema.Properties != nil { + for name, value := range args { + propSchema, exists := schema.Properties[name] + if !exists { + return fmt.Errorf("unknown argument: %s", name) + } + + if err := validateValue(value, propSchema); err != nil { + return fmt.Errorf("invalid argument %s: %w", name, err) + } + } + } + + return nil +} + +func validateValue(value interface{}, schema mcp.Schema) error { + switch schema.Type { + case "string": + if _, ok := value.(string); !ok { + return fmt.Errorf("expected string, got %T", value) + } + case "number": + if _, ok := value.(float64); !ok { + return fmt.Errorf("expected number, got %T", value) + } + case "integer": + if _, ok := value.(float64); !ok { + return fmt.Errorf("expected integer, got %T", value) + } + case "boolean": + if _, ok := value.(bool); !ok { + return fmt.Errorf("expected boolean, got %T", value) + } + case "array": + if _, ok := value.([]interface{}); !ok { + return fmt.Errorf("expected array, got %T", value) + } + case "object": + if _, ok := value.(map[string]interface{}); !ok { + return fmt.Errorf("expected object, got %T", value) + } + } + + return nil +} + +func callToolWithValidation(ctx context.Context, c client.Client, toolName string, args map[string]interface{}) (*mcp.CallToolResult, error) { + // Get tool schema + tools, err := c.ListTools(ctx) + if err != nil { + return nil, fmt.Errorf("failed to list tools: %w", err) + } + + var tool *mcp.Tool + for _, t := range tools.Tools { + if t.Name == toolName { + tool = &t + break + } + } + + if tool == nil { + return nil, fmt.Errorf("tool not found: %s", toolName) + } + + // Validate arguments + if err := validateToolArguments(*tool, args); err != nil { + return nil, fmt.Errorf("argument validation failed: %w", err) + } + + // Call tool + return callTool(ctx, c, toolName, args) +} +``` + +### Batch Tool Operations + +```go +type ToolCall struct { + Name string + Arguments map[string]interface{} +} + +type ToolResult struct { + Call ToolCall + Result *mcp.CallToolResult + Error error +} + +func callToolsBatch(ctx context.Context, c client.Client, calls []ToolCall) []ToolResult { + results := make([]ToolResult, len(calls)) + + // Use goroutines for concurrent calls + var wg sync.WaitGroup + for i, call := range calls { + wg.Add(1) + go func(index int, toolCall ToolCall) { + defer wg.Done() + + result, err := callTool(ctx, c, toolCall.Name, toolCall.Arguments) + results[index] = ToolResult{ + Call: toolCall, + Result: result, + Error: err, + } + }(i, call) + } + + wg.Wait() + return results +} + +func demonstrateBatchToolCalls(ctx context.Context, c client.Client) { + calls := []ToolCall{ + { + Name: "get_weather", + Arguments: map[string]interface{}{ + "location": "New York", + }, + }, + { + Name: "get_weather", + Arguments: map[string]interface{}{ + "location": "London", + }, + }, + { + Name: "calculate", + Arguments: map[string]interface{}{ + "operation": "add", + "x": 10, + "y": 20, + }, + }, + } + + results := callToolsBatch(ctx, c, calls) + + for i, result := range results { + fmt.Printf("Call %d (%s):\n", i+1, result.Call.Name) + if result.Error != nil { + fmt.Printf(" Error: %v\n", result.Error) + } else { + fmt.Printf(" Success: %+v\n", result.Result) + } + } +} +``` + +## Using Prompts + +Prompts provide reusable templates for LLM interactions. + +### Basic Prompt Usage + +```go +func getPrompt(ctx context.Context, c client.Client, name string, args map[string]interface{}) (*mcp.GetPromptResult, error) { + result, err := c.GetPrompt(ctx, mcp.GetPromptRequest{ + Params: mcp.GetPromptRequestParams{ + Name: name, + Arguments: args, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to get prompt: %w", err) + } + + return result, nil +} + +func demonstratePromptUsage(ctx context.Context, c client.Client) { + // List available prompts + prompts, err := c.ListPrompts(ctx) + if err != nil { + log.Printf("Failed to list prompts: %v", err) + return + } + + fmt.Printf("Available prompts: %d\n", len(prompts.Prompts)) + for _, prompt := range prompts.Prompts { + fmt.Printf("- %s: %s\n", prompt.Name, prompt.Description) + + if len(prompt.Arguments) > 0 { + fmt.Printf(" Arguments:\n") + for _, arg := range prompt.Arguments { + fmt.Printf(" - %s: %s\n", arg.Name, arg.Description) + } + } + } + + // Use a specific prompt + if len(prompts.Prompts) > 0 { + prompt := prompts.Prompts[0] + fmt.Printf("\nUsing prompt: %s\n", prompt.Name) + + result, err := getPrompt(ctx, c, prompt.Name, map[string]interface{}{ + // Add appropriate arguments based on prompt schema + }) + if err != nil { + log.Printf("Failed to get prompt: %v", err) + return + } + + fmt.Printf("Prompt result:\n") + fmt.Printf("Description: %s\n", result.Description) + fmt.Printf("Messages: %d\n", len(result.Messages)) + + for i, message := range result.Messages { + fmt.Printf("Message %d (%s): %s\n", i+1, message.Role, message.Content.Text) + } + } +} +``` + +### Prompt Template Processing + +```go +type PromptProcessor struct { + client client.Client +} + +func NewPromptProcessor(c client.Client) *PromptProcessor { + return &PromptProcessor{client: c} +} + +func (pp *PromptProcessor) ProcessPrompt(ctx context.Context, name string, args map[string]interface{}) ([]mcp.PromptMessage, error) { + result, err := pp.client.GetPrompt(ctx, mcp.GetPromptRequest{ + Params: mcp.GetPromptRequestParams{ + Name: name, + Arguments: args, + }, + }) + if err != nil { + return nil, err + } + + return result.Messages, nil +} + +func (pp *PromptProcessor) BuildConversation(ctx context.Context, promptName string, args map[string]interface{}, userMessage string) ([]mcp.PromptMessage, error) { + // Get prompt template + messages, err := pp.ProcessPrompt(ctx, promptName, args) + if err != nil { + return nil, err + } + + // Add user message + messages = append(messages, mcp.PromptMessage{ + Role: "user", + Content: mcp.TextContent(userMessage), + }) + + return messages, nil +} + +func (pp *PromptProcessor) FormatForLLM(messages []mcp.PromptMessage) []map[string]interface{} { + formatted := make([]map[string]interface{}, len(messages)) + + for i, message := range messages { + formatted[i] = map[string]interface{}{ + "role": message.Role, + "content": message.Content.Text, + } + } + + return formatted +} +``` + +### Dynamic Prompt Generation + +```go +func generateCodeReviewPrompt(ctx context.Context, c client.Client, code, language string) ([]mcp.PromptMessage, error) { + processor := NewPromptProcessor(c) + + return processor.ProcessPrompt(ctx, "code_review", map[string]interface{}{ + "code": code, + "language": language, + "focus": "best-practices", + }) +} + +func generateDataAnalysisPrompt(ctx context.Context, c client.Client, datasetURI string, analysisType string) ([]mcp.PromptMessage, error) { + processor := NewPromptProcessor(c) + + return processor.ProcessPrompt(ctx, "analyze_data", map[string]interface{}{ + "dataset_uri": datasetURI, + "analysis_type": analysisType, + "focus_areas": []string{"trends", "outliers", "correlations"}, + }) +} + +func demonstrateDynamicPrompts(ctx context.Context, c client.Client) { + // Generate code review prompt + codeReviewMessages, err := generateCodeReviewPrompt(ctx, c, + "func main() { fmt.Println(\"Hello\") }", + "go") + if err != nil { + log.Printf("Failed to generate code review prompt: %v", err) + } else { + fmt.Printf("Code review prompt: %d messages\n", len(codeReviewMessages)) + } + + // Generate data analysis prompt + analysisMessages, err := generateDataAnalysisPrompt(ctx, c, + "dataset://sales_data", + "exploratory") + if err != nil { + log.Printf("Failed to generate analysis prompt: %v", err) + } else { + fmt.Printf("Data analysis prompt: %d messages\n", len(analysisMessages)) + } +} +``` + +## Subscriptions + +Some transports support subscriptions for receiving real-time notifications. + +### Basic Subscription Handling + +```go +func handleSubscriptions(ctx context.Context, c client.Client) { + // Check if client supports subscriptions + subscriber, ok := c.(client.Subscriber) + if !ok { + log.Println("Client does not support subscriptions") + return + } + + // Subscribe to notifications + notifications, err := subscriber.Subscribe(ctx) + if err != nil { + log.Printf("Failed to subscribe: %v", err) + return + } + + // Handle notifications + for { + select { + case notification := <-notifications: + handleNotification(notification) + case <-ctx.Done(): + log.Println("Subscription cancelled") + return + } + } +} + +func handleNotification(notification mcp.Notification) { + switch notification.Method { + case "notifications/progress": + handleProgressNotification(notification) + case "notifications/message": + handleMessageNotification(notification) + case "notifications/resources/updated": + handleResourceUpdateNotification(notification) + case "notifications/tools/updated": + handleToolUpdateNotification(notification) + default: + log.Printf("Unknown notification: %s", notification.Method) + } +} + +func handleProgressNotification(notification mcp.Notification) { + var progress mcp.ProgressNotification + if err := json.Unmarshal(notification.Params, &progress); err != nil { + log.Printf("Failed to parse progress notification: %v", err) + return + } + + fmt.Printf("Progress: %d/%d - %s\n", + progress.Progress, + progress.Total, + progress.Message) +} + +func handleMessageNotification(notification mcp.Notification) { + var message mcp.MessageNotification + if err := json.Unmarshal(notification.Params, &message); err != nil { + log.Printf("Failed to parse message notification: %v", err) + return + } + + fmt.Printf("Server message: %s\n", message.Text) +} + +func handleResourceUpdateNotification(notification mcp.Notification) { + log.Println("Resources updated, refreshing cache...") + // Invalidate resource cache or refresh resource list +} + +func handleToolUpdateNotification(notification mcp.Notification) { + log.Println("Tools updated, refreshing tool list...") + // Refresh tool list +} +``` + +### Advanced Subscription Management + +```go +type SubscriptionManager struct { + client client.Client + subscriber client.Subscriber + notifications chan mcp.Notification + handlers map[string][]NotificationHandler + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup + mutex sync.RWMutex +} + +type NotificationHandler func(mcp.Notification) error + +func NewSubscriptionManager(c client.Client) (*SubscriptionManager, error) { + subscriber, ok := c.(client.Subscriber) + if !ok { + return nil, fmt.Errorf("client does not support subscriptions") + } + + ctx, cancel := context.WithCancel(context.Background()) + + sm := &SubscriptionManager{ + client: c, + subscriber: subscriber, + handlers: make(map[string][]NotificationHandler), + ctx: ctx, + cancel: cancel, + } + + return sm, nil +} + +func (sm *SubscriptionManager) Start() error { + notifications, err := sm.subscriber.Subscribe(sm.ctx) + if err != nil { + return fmt.Errorf("failed to subscribe: %w", err) + } + + sm.notifications = notifications + + sm.wg.Add(1) + go sm.handleNotifications() + + return nil +} + +func (sm *SubscriptionManager) Stop() { + sm.cancel() + sm.wg.Wait() +} + +func (sm *SubscriptionManager) AddHandler(method string, handler NotificationHandler) { + sm.mutex.Lock() + defer sm.mutex.Unlock() + + sm.handlers[method] = append(sm.handlers[method], handler) +} + +func (sm *SubscriptionManager) RemoveHandler(method string, handler NotificationHandler) { + sm.mutex.Lock() + defer sm.mutex.Unlock() + + handlers := sm.handlers[method] + for i, h := range handlers { + if reflect.ValueOf(h).Pointer() == reflect.ValueOf(handler).Pointer() { + sm.handlers[method] = append(handlers[:i], handlers[i+1:]...) + break + } + } +} + +func (sm *SubscriptionManager) handleNotifications() { + defer sm.wg.Done() + + for { + select { + case notification := <-sm.notifications: + sm.processNotification(notification) + case <-sm.ctx.Done(): + return + } + } +} + +func (sm *SubscriptionManager) processNotification(notification mcp.Notification) { + sm.mutex.RLock() + handlers := sm.handlers[notification.Method] + sm.mutex.RUnlock() + + for _, handler := range handlers { + if err := handler(notification); err != nil { + log.Printf("Handler error for %s: %v", notification.Method, err) + } + } +} + +// Usage example +func demonstrateSubscriptionManager(c client.Client) { + sm, err := NewSubscriptionManager(c) + if err != nil { + log.Printf("Failed to create subscription manager: %v", err) + return + } + + // Add handlers + sm.AddHandler("notifications/progress", func(n mcp.Notification) error { + log.Printf("Progress notification: %+v", n) + return nil + }) + + sm.AddHandler("notifications/message", func(n mcp.Notification) error { + log.Printf("Message notification: %+v", n) + return nil + }) + + // Start handling + if err := sm.Start(); err != nil { + log.Printf("Failed to start subscription manager: %v", err) + return + } + + // Let it run for a while + time.Sleep(30 * time.Second) + + // Stop + sm.Stop() +} +``` + +## Next Steps + +- **[Client Transports](/clients/transports)** - Learn transport-specific client features +- **[Testing](/testing)** - Test client implementations thoroughly \ No newline at end of file diff --git a/www/docs/pages/clients/transports.mdx b/www/docs/pages/clients/transports.mdx new file mode 100644 index 000000000..e694bf4d1 --- /dev/null +++ b/www/docs/pages/clients/transports.mdx @@ -0,0 +1,1047 @@ +# Client Transports + +Learn about transport-specific client implementations and how to choose the right transport for your use case. + +## Transport Overview + +MCP-Go provides client implementations for all supported transports. Each transport has different characteristics and is optimized for specific scenarios. + +| Transport | Best For | Connection | Real-time | Multi-client | +|-----------|----------|------------|-----------|--------------| +| **STDIO** | CLI tools, desktop apps | Process pipes | No | No | +| **HTTP** | Web services, APIs | HTTP requests | No | Yes | +| **SSE** | Web apps, real-time | HTTP + EventSource | Yes | Yes | +| **In-Process** | Testing, embedded | Direct calls | Yes | No | + +## STDIO Client + +STDIO clients communicate with servers through standard input/output, typically by spawning a subprocess. + +### Basic STDIO Client + +```go +package main + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "log" + "net/http" + "os" + "sync" + "time" + + "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func createStdioClient() { + // Create client that spawns a subprocess + c, err := client.NewStdioClient( + "go", "run", "/path/to/server/main.go", + ) + if err != nil { + log.Fatal(err) + } + defer c.Close() + + ctx := context.Background() + + // Initialize connection + if err := c.Initialize(ctx); err != nil { + log.Fatal(err) + } + + // Use the client + tools, err := c.ListTools(ctx) + if err != nil { + log.Fatal(err) + } + + log.Printf("Available tools: %d", len(tools.Tools)) +} +``` + +### Advanced STDIO Configuration + +```go +func createAdvancedStdioClient() { + options := client.StdioOptions{ + Command: "python", + Args: []string{"/path/to/server.py"}, + Env: []string{ + "PYTHONPATH=/custom/path", + "LOG_LEVEL=debug", + "DATABASE_URL=sqlite:///data.db", + }, + WorkingDir: "/path/to/server", + Timeout: 30 * time.Second, + BufferSize: 8192, + RestartOnError: true, + MaxRestarts: 3, + RestartDelay: 1 * time.Second, + } + + c, err := client.NewStdioClientWithOptions(options) + if err != nil { + log.Fatal(err) + } + defer c.Close() + + // Use client... +} +``` + +### STDIO Error Handling + +```go +// Define error constants for STDIO client errors +var ( + ErrProcessExited = errors.New("process exited") + ErrProcessTimeout = errors.New("process timeout") + ErrBrokenPipe = errors.New("broken pipe") +) + +func handleStdioErrors(c *client.StdioClient) { + ctx := context.Background() + + result, err := c.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolRequestParams{ + Name: "example_tool", + }, + }) + + if err != nil { + switch { + case errors.Is(err, ErrProcessExited): + log.Println("Server process exited unexpectedly") + // Attempt to restart + if restartErr := c.Restart(); restartErr != nil { + log.Printf("Failed to restart: %v", restartErr) + } + + case errors.Is(err, ErrProcessTimeout): + log.Println("Server process timed out") + // Kill and restart process + c.Kill() + if restartErr := c.Restart(); restartErr != nil { + log.Printf("Failed to restart: %v", restartErr) + } + + case errors.Is(err, ErrBrokenPipe): + log.Println("Communication pipe broken") + // Process likely crashed, restart + if restartErr := c.Restart(); restartErr != nil { + log.Printf("Failed to restart: %v", restartErr) + } + + default: + log.Printf("Unexpected error: %v", err) + } + return + } + + log.Printf("Tool result: %+v", result) +} +``` + +### STDIO Process Management + +```go +type ManagedStdioClient struct { + client *client.StdioClient + options client.StdioOptions + restartChan chan struct{} + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup +} + +func NewManagedStdioClient(options client.StdioOptions) (*ManagedStdioClient, error) { + ctx, cancel := context.WithCancel(context.Background()) + + msc := &ManagedStdioClient{ + options: options, + restartChan: make(chan struct{}, 1), + ctx: ctx, + cancel: cancel, + } + + if err := msc.start(); err != nil { + cancel() + return nil, err + } + + msc.wg.Add(1) + go msc.monitorProcess() + + return msc, nil +} + +func (msc *ManagedStdioClient) start() error { + client, err := client.NewStdioClientWithOptions(msc.options) + if err != nil { + return err + } + + if err := client.Initialize(msc.ctx); err != nil { + client.Close() + return err + } + + msc.client = client + return nil +} + +func (msc *ManagedStdioClient) monitorProcess() { + defer msc.wg.Done() + + for { + select { + case <-msc.ctx.Done(): + return + case <-msc.restartChan: + log.Println("Restarting STDIO client...") + + if msc.client != nil { + msc.client.Close() + } + + // Wait before restarting + time.Sleep(1 * time.Second) + + if err := msc.start(); err != nil { + log.Printf("Failed to restart client: %v", err) + // Try again after delay + time.Sleep(5 * time.Second) + select { + case msc.restartChan <- struct{}{}: + default: + } + } else { + log.Println("Client restarted successfully") + } + } + } +} + +func (msc *ManagedStdioClient) CallTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + if msc.client == nil { + return nil, fmt.Errorf("client not available") + } + + result, err := msc.client.CallTool(ctx, req) + if err != nil && isProcessError(err) { + // Trigger restart + select { + case msc.restartChan <- struct{}{}: + default: + } + return nil, fmt.Errorf("process error, restarting: %w", err) + } + + return result, err +} + +func (msc *ManagedStdioClient) Close() error { + msc.cancel() + msc.wg.Wait() + + if msc.client != nil { + return msc.client.Close() + } + + return nil +} + +func isProcessError(err error) bool { + return errors.Is(err, ErrProcessExited) || + errors.Is(err, ErrBrokenPipe) || + errors.Is(err, ErrProcessTimeout) +} +``` + +```go +// Define connection error constants +var ( + ErrConnectionLost = errors.New("connection lost") + ErrConnectionFailed = errors.New("connection failed") + ErrUnauthorized = errors.New("unauthorized") + ErrForbidden = errors.New("forbidden") +) +``` + +## HTTP Client + +HTTP clients communicate with servers using traditional HTTP requests. + +### Basic HTTP Client + +```go +func createHTTPClient() { + // Create HTTP client + c := client.NewHTTPClient("http://localhost:8080/mcp") + defer c.Close() + + // Set authentication + c.SetHeader("Authorization", "Bearer your-token") + c.SetHeader("X-API-Version", "v1") + + // Set timeout + c.SetTimeout(30 * time.Second) + + ctx := context.Background() + + // Initialize + if err := c.Initialize(ctx); err != nil { + log.Fatal(err) + } + + // Use client + tools, err := c.ListTools(ctx) + if err != nil { + log.Fatal(err) + } + + log.Printf("Available tools: %d", len(tools.Tools)) +} +``` + +### HTTP Client with Custom Configuration + +```go +func createCustomHTTPClient() { + // Create custom HTTP client + httpClient := &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: false, + }, + }, + } + + // Create MCP client with custom HTTP client + c := client.NewHTTPClientWithClient("https://api.example.com/mcp", httpClient) + defer c.Close() + + // Set custom headers + c.SetHeader("User-Agent", "MyApp/1.0") + c.SetHeader("Accept", "application/json") + + ctx := context.Background() + + if err := c.Initialize(ctx); err != nil { + log.Fatal(err) + } + + // Use client... +} +``` + +### HTTP Authentication + +```go +type AuthenticatedHTTPClient struct { + client *client.HTTPClient + tokenSource TokenSource + mutex sync.RWMutex +} + +type TokenSource interface { + Token() (string, error) + Refresh() error +} + +func NewAuthenticatedHTTPClient(baseURL string, tokenSource TokenSource) *AuthenticatedHTTPClient { + return &AuthenticatedHTTPClient{ + client: client.NewHTTPClient(baseURL), + tokenSource: tokenSource, + } +} + +func (ahc *AuthenticatedHTTPClient) ensureValidToken() error { + ahc.mutex.Lock() + defer ahc.mutex.Unlock() + + token, err := ahc.tokenSource.Token() + if err != nil { + // Try to refresh token + if refreshErr := ahc.tokenSource.Refresh(); refreshErr != nil { + return fmt.Errorf("failed to refresh token: %w", refreshErr) + } + + token, err = ahc.tokenSource.Token() + if err != nil { + return fmt.Errorf("failed to get token after refresh: %w", err) + } + } + + ahc.client.SetHeader("Authorization", "Bearer "+token) + return nil +} + +func (ahc *AuthenticatedHTTPClient) CallTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + if err := ahc.ensureValidToken(); err != nil { + return nil, err + } + + result, err := ahc.client.CallTool(ctx, req) + if err != nil && isAuthError(err) { + // Token might be expired, refresh and retry + if refreshErr := ahc.tokenSource.Refresh(); refreshErr != nil { + return nil, fmt.Errorf("authentication failed: %w", err) + } + + if err := ahc.ensureValidToken(); err != nil { + return nil, err + } + + return ahc.client.CallTool(ctx, req) + } + + return result, err +} + +func isAuthError(err error) bool { + return errors.Is(err, ErrUnauthorized) || + errors.Is(err, ErrForbidden) +} +``` + +### HTTP Connection Pooling + +```go +type HTTPClientPool struct { + clients chan *client.HTTPClient + factory func() *client.HTTPClient + maxSize int +} + +func NewHTTPClientPool(baseURL string, maxSize int) *HTTPClientPool { + pool := &HTTPClientPool{ + clients: make(chan *client.HTTPClient, maxSize), + maxSize: maxSize, + factory: func() *client.HTTPClient { + return client.NewHTTPClient(baseURL) + }, + } + + // Pre-populate pool + for i := 0; i < maxSize; i++ { + pool.clients <- pool.factory() + } + + return pool +} + +func (pool *HTTPClientPool) Get() *client.HTTPClient { + select { + case c := <-pool.clients: + return c + default: + return pool.factory() + } +} + +func (pool *HTTPClientPool) Put(c *client.HTTPClient) { + select { + case pool.clients <- c: + default: + // Pool full, close client + c.Close() + } +} + +func (pool *HTTPClientPool) CallTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + c := pool.Get() + defer pool.Put(c) + + return c.CallTool(ctx, req) +} +``` + +## SSE Client + +SSE (Server-Sent Events) clients provide real-time communication with servers. + +### Basic SSE Client + +```go +func createSSEClient() { + // Create SSE client + c := client.NewSSEClient("http://localhost:8080/mcp/sse") + defer c.Close() + + // Set authentication + c.SetHeader("Authorization", "Bearer your-token") + + ctx := context.Background() + + // Initialize + if err := c.Initialize(ctx); err != nil { + log.Fatal(err) + } + + // Subscribe to notifications + notifications, err := c.Subscribe(ctx) + if err != nil { + log.Fatal(err) + } + + // Handle notifications in background + go func() { + for notification := range notifications { + log.Printf("Notification: %+v", notification) + } + }() + + // Use client for regular operations + tools, err := c.ListTools(ctx) + if err != nil { + log.Fatal(err) + } + + log.Printf("Available tools: %d", len(tools.Tools)) +} +``` + +### SSE Client with Reconnection + +```go +type ResilientSSEClient struct { + baseURL string + headers map[string]string + client *client.SSEClient + ctx context.Context + cancel context.CancelFunc + reconnectCh chan struct{} + mutex sync.RWMutex +} + +func NewResilientSSEClient(baseURL string) *ResilientSSEClient { + ctx, cancel := context.WithCancel(context.Background()) + + rsc := &ResilientSSEClient{ + baseURL: baseURL, + headers: make(map[string]string), + ctx: ctx, + cancel: cancel, + reconnectCh: make(chan struct{}, 1), + } + + go rsc.reconnectLoop() + return rsc +} + +func (rsc *ResilientSSEClient) SetHeader(key, value string) { + rsc.mutex.Lock() + defer rsc.mutex.Unlock() + rsc.headers[key] = value +} + +func (rsc *ResilientSSEClient) connect() error { + rsc.mutex.Lock() + defer rsc.mutex.Unlock() + + if rsc.client != nil { + rsc.client.Close() + } + + client := client.NewSSEClient(rsc.baseURL) + + // Set headers + for key, value := range rsc.headers { + client.SetHeader(key, value) + } + + if err := client.Initialize(rsc.ctx); err != nil { + return err + } + + rsc.client = client + return nil +} + +func (rsc *ResilientSSEClient) reconnectLoop() { + for { + select { + case <-rsc.ctx.Done(): + return + case <-rsc.reconnectCh: + log.Println("Reconnecting SSE client...") + + for attempt := 1; attempt <= 5; attempt++ { + if err := rsc.connect(); err != nil { + log.Printf("Reconnection attempt %d failed: %v", attempt, err) + + backoff := time.Duration(attempt) * time.Second + select { + case <-time.After(backoff): + case <-rsc.ctx.Done(): + return + } + } else { + log.Println("Reconnected successfully") + break + } + } + } + } +} + +func (rsc *ResilientSSEClient) CallTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + rsc.mutex.RLock() + client := rsc.client + rsc.mutex.RUnlock() + + if client == nil { + return nil, fmt.Errorf("client not connected") + } + + result, err := client.CallTool(ctx, req) + if err != nil && isConnectionError(err) { + // Trigger reconnection + select { + case rsc.reconnectCh <- struct{}{}: + default: + } + return nil, fmt.Errorf("connection error: %w", err) + } + + return result, err +} + +func (rsc *ResilientSSEClient) Subscribe(ctx context.Context) (<-chan mcp.Notification, error) { + rsc.mutex.RLock() + client := rsc.client + rsc.mutex.RUnlock() + + if client == nil { + return nil, fmt.Errorf("client not connected") + } + + return client.Subscribe(ctx) +} + +func (rsc *ResilientSSEClient) Close() error { + rsc.cancel() + + rsc.mutex.Lock() + defer rsc.mutex.Unlock() + + if rsc.client != nil { + return rsc.client.Close() + } + + return nil +} + +// Helper function to check if an error is a connection error +func isConnectionError(err error) bool { + return errors.Is(err, ErrConnectionLost) || + errors.Is(err, ErrConnectionFailed) +} +``` + +### SSE Event Handling + +```go +type SSEEventHandler struct { + client *client.SSEClient + handlers map[string][]func(mcp.Notification) + mutex sync.RWMutex + ctx context.Context + cancel context.CancelFunc + wg sync.WaitGroup +} + +func NewSSEEventHandler(c *client.SSEClient) *SSEEventHandler { + ctx, cancel := context.WithCancel(context.Background()) + + return &SSEEventHandler{ + client: c, + handlers: make(map[string][]func(mcp.Notification)), + ctx: ctx, + cancel: cancel, + } +} + +func (seh *SSEEventHandler) Start() error { + notifications, err := seh.client.Subscribe(seh.ctx) + if err != nil { + return err + } + + seh.wg.Add(1) + go func() { + defer seh.wg.Done() + + for { + select { + case notification := <-notifications: + seh.handleNotification(notification) + case <-seh.ctx.Done(): + return + } + } + }() + + return nil +} + +func (seh *SSEEventHandler) Stop() { + seh.cancel() + seh.wg.Wait() +} + +func (seh *SSEEventHandler) OnProgress(handler func(mcp.Notification)) { + seh.addHandler("notifications/progress", handler) +} + +func (seh *SSEEventHandler) OnMessage(handler func(mcp.Notification)) { + seh.addHandler("notifications/message", handler) +} + +func (seh *SSEEventHandler) OnResourceUpdate(handler func(mcp.Notification)) { + seh.addHandler("notifications/resources/updated", handler) +} + +func (seh *SSEEventHandler) OnToolUpdate(handler func(mcp.Notification)) { + seh.addHandler("notifications/tools/updated", handler) +} + +func (seh *SSEEventHandler) addHandler(method string, handler func(mcp.Notification)) { + seh.mutex.Lock() + defer seh.mutex.Unlock() + + seh.handlers[method] = append(seh.handlers[method], handler) +} + +func (seh *SSEEventHandler) handleNotification(notification mcp.Notification) { + seh.mutex.RLock() + handlers := seh.handlers[notification.Method] + seh.mutex.RUnlock() + + for _, handler := range handlers { + go handler(notification) + } +} +``` + +## In-Process Client + +In-process clients provide direct communication with servers in the same process. + +### Basic In-Process Client + +```go +func createInProcessClient() { + // Create server + s := server.NewMCPServer("Test Server", "1.0.0") + + // Add tools to server + s.AddTool( + mcp.NewTool("test_tool", + mcp.WithDescription("Test tool"), + mcp.WithString("input", mcp.Required()), + ), + func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + input := req.Params.Arguments["input"].(string) + return mcp.NewToolResultText("Processed: " + input), nil + }, + ) + + // Create in-process client + c := client.NewInProcessClient(s) + defer c.Close() + + ctx := context.Background() + + // Initialize (no network overhead) + if err := c.Initialize(ctx); err != nil { + log.Fatal(err) + } + + // Use client + result, err := c.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolRequestParams{ + Name: "test_tool", + Arguments: map[string]interface{}{ + "input": "test data", + }, + }, + }) + if err != nil { + log.Fatal(err) + } + + log.Printf("Tool result: %+v", result) +} +``` + +### In-Process Client for Testing + +```go +type TestClient struct { + server *server.MCPServer + client *client.InProcessClient +} + +func NewTestClient() *TestClient { + s := server.NewMCPServer("Test Server", "1.0.0", + server.WithAllCapabilities(), + ) + + return &TestClient{ + server: s, + client: client.NewInProcessClient(s), + } +} + +func (tc *TestClient) AddTool(name, description string, handler server.ToolHandler) { + tool := mcp.NewTool(name, mcp.WithDescription(description)) + tc.server.AddTool(tool, handler) +} + +func (tc *TestClient) AddResource(uri, name string, handler server.ResourceHandler) { + resource := mcp.NewResource(uri, name) + tc.server.AddResource(resource, handler) +} + +func (tc *TestClient) Initialize(ctx context.Context) error { + return tc.client.Initialize(ctx) +} + +func (tc *TestClient) CallTool(ctx context.Context, name string, args map[string]interface{}) (*mcp.CallToolResult, error) { + return tc.client.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolRequestParams{ + Name: name, + Arguments: args, + }, + }) +} + +func (tc *TestClient) ReadResource(ctx context.Context, uri string) (*mcp.ReadResourceResult, error) { + return tc.client.ReadResource(ctx, mcp.ReadResourceRequest{ + Params: mcp.ReadResourceRequestParams{ + URI: uri, + }, + }) +} + +func (tc *TestClient) Close() error { + return tc.client.Close() +} + +// Usage in tests +func TestWithInProcessClient(t *testing.T) { + tc := NewTestClient() + defer tc.Close() + + // Add test tool + tc.AddTool("echo", "Echo input", func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + input := req.Params.Arguments["input"].(string) + return mcp.NewToolResultText(input), nil + }) + + ctx := context.Background() + err := tc.Initialize(ctx) + require.NoError(t, err) + + // Test tool call + result, err := tc.CallTool(ctx, "echo", map[string]interface{}{ + "input": "hello world", + }) + require.NoError(t, err) + assert.Equal(t, "hello world", result.Content[0].Text) +} +``` + +## Transport Selection + +### Decision Matrix + +Choose your transport based on these factors: + +```go +type TransportRequirements struct { + RealTime bool + MultiClient bool + NetworkRequired bool + Performance string // "high", "medium", "low" + Complexity string // "low", "medium", "high" +} + +func SelectTransport(req TransportRequirements) string { + switch { + case !req.NetworkRequired && req.Performance == "high": + return "inprocess" + + case !req.NetworkRequired && !req.MultiClient: + return "stdio" + + case req.RealTime && req.MultiClient: + return "sse" + + case req.NetworkRequired && req.MultiClient: + return "http" + + default: + return "stdio" // Default fallback + } +} + +// Usage examples +func demonstrateTransportSelection() { + // High-performance testing + testReq := TransportRequirements{ + RealTime: false, + MultiClient: false, + NetworkRequired: false, + Performance: "high", + Complexity: "low", + } + fmt.Printf("Testing: %s\n", SelectTransport(testReq)) + + // Real-time web application + webReq := TransportRequirements{ + RealTime: true, + MultiClient: true, + NetworkRequired: true, + Performance: "medium", + Complexity: "medium", + } + fmt.Printf("Web app: %s\n", SelectTransport(webReq)) + + // CLI tool + cliReq := TransportRequirements{ + RealTime: false, + MultiClient: false, + NetworkRequired: false, + Performance: "medium", + Complexity: "low", + } + fmt.Printf("CLI tool: %s\n", SelectTransport(cliReq)) +} +``` + +### Multi-Transport Client Factory + +```go +type ClientFactory struct { + configs map[string]interface{} +} + +func NewClientFactory() *ClientFactory { + return &ClientFactory{ + configs: make(map[string]interface{}), + } +} + +func (cf *ClientFactory) SetStdioConfig(command string, args ...string) { + cf.configs["stdio"] = client.StdioOptions{ + Command: command, + Args: args, + } +} + +func (cf *ClientFactory) SetHTTPConfig(baseURL string, headers map[string]string) { + cf.configs["http"] = struct { + BaseURL string + Headers map[string]string + }{ + BaseURL: baseURL, + Headers: headers, + } +} + +func (cf *ClientFactory) SetSSEConfig(baseURL string, headers map[string]string) { + cf.configs["sse"] = struct { + BaseURL string + Headers map[string]string + }{ + BaseURL: baseURL, + Headers: headers, + } +} + +func (cf *ClientFactory) CreateClient(transport string) (client.Client, error) { + switch transport { + case "stdio": + config, ok := cf.configs["stdio"].(client.StdioOptions) + if !ok { + return nil, fmt.Errorf("stdio config not set") + } + return client.NewStdioClientWithOptions(config) + + case "http": + config, ok := cf.configs["http"].(struct { + BaseURL string + Headers map[string]string + }) + if !ok { + return nil, fmt.Errorf("http config not set") + } + + c := client.NewHTTPClient(config.BaseURL) + for key, value := range config.Headers { + c.SetHeader(key, value) + } + return c, nil + + case "sse": + config, ok := cf.configs["sse"].(struct { + BaseURL string + Headers map[string]string + }) + if !ok { + return nil, fmt.Errorf("sse config not set") + } + + c := client.NewSSEClient(config.BaseURL) + for key, value := range config.Headers { + c.SetHeader(key, value) + } + return c, nil + + default: + return nil, fmt.Errorf("unknown transport: %s", transport) + } +} + +// Usage +func demonstrateClientFactory() { + factory := NewClientFactory() + + // Configure transports + factory.SetStdioConfig("go", "run", "server.go") + factory.SetHTTPConfig("http://localhost:8080/mcp", map[string]string{ + "Authorization": "Bearer token", + }) + factory.SetSSEConfig("http://localhost:8080/mcp/sse", map[string]string{ + "Authorization": "Bearer token", + }) + + // Create client based on environment + transport := os.Getenv("MCP_TRANSPORT") + if transport == "" { + transport = "stdio" + } + + client, err := factory.CreateClient(transport) + if err != nil { + log.Fatal(err) + } + defer client.Close() + + // Use client... +} +``` + diff --git a/www/docs/pages/core-concepts.mdx b/www/docs/pages/core-concepts.mdx new file mode 100644 index 000000000..70345c2cf --- /dev/null +++ b/www/docs/pages/core-concepts.mdx @@ -0,0 +1,264 @@ +# Core Concepts + +Understanding the fundamental concepts of MCP and how MCP-Go implements them is essential for building effective MCP servers and clients. + +## MCP Protocol Fundamentals + +The Model Context Protocol defines four core concepts that enable LLMs to interact with external systems safely and effectively. + +### Resources + +Resources are like GET endpoints - they expose data to LLMs in a read-only manner. Think of them as files, database records, or API responses that an LLM can access. + +**Key characteristics:** +- **Read-only**: LLMs can fetch but not modify resources +- **URI-based**: Each resource has a unique identifier +- **Typed content**: Resources specify their MIME type (text, JSON, binary, etc.) +- **Dynamic or static**: Can be pre-defined or generated on-demand + +**Example use cases:** +- File system access (`file:///path/to/document.txt`) +- Database records (`db://users/123`) +- API data (`api://weather/current`) +- Configuration files (`config://app.json`) + +```go +// Static resource +resource := mcp.NewResource( + "docs://readme", + "Project README", + mcp.WithResourceDescription("The project's main documentation"), + mcp.WithMIMEType("text/markdown"), +) + +// Dynamic resource with template +userResource := mcp.NewResource( + "users://{user_id}", + "User Profile", + mcp.WithResourceDescription("User profile information"), + mcp.WithMIMEType("application/json"), +) +``` + +### Tools + +Tools are like POST endpoints - they provide functionality that LLMs can invoke to take actions or perform computations. + +**Key characteristics:** +- **Action-oriented**: Tools do things rather than just return data +- **Parameterized**: Accept structured input arguments +- **Typed schemas**: Define expected parameter types and constraints +- **Return results**: Provide structured output back to the LLM + +**Example use cases:** +- Calculations (`calculate`, `convert_units`) +- File operations (`create_file`, `search_files`) +- API calls (`send_email`, `create_ticket`) +- System commands (`run_command`, `check_status`) + +```go +// Simple calculation tool +calcTool := mcp.NewTool("calculate", + mcp.WithDescription("Perform arithmetic operations"), + mcp.WithString("operation", + mcp.Required(), + mcp.Enum("add", "subtract", "multiply", "divide"), + ), + mcp.WithNumber("x", mcp.Required()), + mcp.WithNumber("y", mcp.Required()), +) + +// File creation tool +fileTool := mcp.NewTool("create_file", + mcp.WithDescription("Create a new file with content"), + mcp.WithString("path", mcp.Required()), + mcp.WithString("content", mcp.Required()), + mcp.WithString("encoding", mcp.Default("utf-8")), +) +``` + +### Prompts + +Prompts are reusable interaction templates that help structure conversations between users and LLMs. + +**Key characteristics:** +- **Template-based**: Use placeholders for dynamic content +- **Reusable**: Can be invoked multiple times with different arguments +- **Structured**: Define clear input parameters and expected outputs +- **Context-aware**: Can include relevant resources or tool suggestions + +**Example use cases:** +- Code review templates +- Documentation generation +- Data analysis workflows +- Creative writing prompts + +```go +// Code review prompt +reviewPrompt := mcp.NewPrompt("code_review", + mcp.WithPromptDescription("Review code for best practices and issues"), + mcp.WithPromptArgument("code", + mcp.Required(), + mcp.Description("The code to review"), + ), + mcp.WithPromptArgument("language", + mcp.Description("Programming language"), + ), +) + +// Data analysis prompt +analysisPrompt := mcp.NewPrompt("analyze_data", + mcp.WithPromptDescription("Analyze dataset and provide insights"), + mcp.WithPromptArgument("dataset_uri", mcp.Required()), + mcp.WithPromptArgument("focus_areas", + mcp.Description("Specific areas to focus analysis on"), + ), +) +``` + +### Transports + +Transports define how MCP clients and servers communicate. MCP-Go supports multiple transport methods to fit different deployment scenarios. + +**Available transports:** + +1. **Stdio** - Standard input/output (most common) + - Best for: Local tools, CLI integration, desktop applications + - Pros: Simple, secure, no network setup + - Cons: Local only, single client + +2. **Server-Sent Events (SSE)** - HTTP-based streaming + - Best for: Web applications, real-time updates + - Pros: Web-friendly, real-time, multiple clients + - Cons: HTTP overhead, one-way streaming + +3. **HTTP** - Traditional request/response + - Best for: Web services, REST-like APIs + - Pros: Standard protocol, caching, load balancing + - Cons: No real-time updates, more complex + +```go +// Stdio transport (most common) +server.ServeStdio(s) + +// HTTP transport +server.ServeHTTP(s, ":8080") + +// SSE transport +server.ServeSSE(s, ":8080") +``` + +## SDK Architecture + +MCP-Go provides a clean architecture that abstracts the complexity of the MCP protocol while giving you full control when needed. + +### Server vs Client + +Understanding when to build servers versus clients is crucial for effective MCP integration. + +**MCP Servers:** +- **Purpose**: Expose tools, resources, and prompts to LLMs +- **Use cases**: + - Database access layers + - File system tools + - API integrations + - Custom business logic +- **Characteristics**: Passive, respond to requests, stateful + +```go +// Server example - exposes functionality +s := server.NewMCPServer("Database Tools", "1.0.0") +s.AddTool(queryTool, handleQuery) +s.AddResource(tableResource, handleTableAccess) +server.ServeStdio(s) +``` + +**MCP Clients:** +- **Purpose**: Connect to and use MCP servers +- **Use cases**: + - LLM applications + - Orchestration tools + - Testing and debugging + - Server composition +- **Characteristics**: Active, make requests, coordinate multiple servers + +```go +// Client example - uses functionality +client := client.NewStdioClient("database-server") +tools, _ := client.ListTools(ctx) +result, _ := client.CallTool(ctx, queryRequest) +``` + +### Transport Layer + +The transport layer abstracts communication protocols, allowing you to focus on business logic rather than protocol details. + +**Key benefits:** +- **Protocol agnostic**: Same server code works with any transport +- **Automatic serialization**: JSON marshaling/unmarshaling handled automatically +- **Error handling**: Transport-specific errors are normalized +- **Connection management**: Automatic reconnection and cleanup + +```go +// Same server works with any transport +s := server.NewMCPServer("My Server", "1.0.0") + +// Choose transport at runtime +switch transport { +case "stdio": + server.ServeStdio(s) +case "http": + server.ServeHTTP(s, ":8080") +case "sse": + server.ServeSSE(s, ":8080") +} +``` + +### Session Management + +MCP-Go handles session management automatically, supporting multiple concurrent clients with proper isolation. + +**Features:** +- **Multi-client support**: Multiple LLMs can connect simultaneously +- **Session isolation**: Each client has independent state +- **Resource cleanup**: Automatic cleanup when clients disconnect +- **Concurrent safety**: Thread-safe operations across all sessions + +**Session lifecycle:** +1. **Initialize**: Client connects and exchanges capabilities +2. **Active**: Client makes requests, server responds +3. **Cleanup**: Connection closes, resources are freed + +```go +// Server automatically handles multiple sessions +s := server.NewMCPServer("Multi-Client Server", "1.0.0", + server.WithHooks(&server.Hooks{ + OnSessionStart: func(sessionID string) { + log.Printf("Client %s connected", sessionID) + }, + OnSessionEnd: func(sessionID string) { + log.Printf("Client %s disconnected", sessionID) + }, + }), +) +``` + +**State management patterns:** + +```go +// Per-session state +type SessionState struct { + UserID string + Settings map[string]interface{} +} + +var sessions = make(map[string]*SessionState) + +func toolHandler(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + sessionID := server.GetSessionID(ctx) + state := sessions[sessionID] + + // Use session-specific state + return processWithState(state, req) +} +``` \ No newline at end of file diff --git a/www/docs/pages/example.mdx b/www/docs/pages/example.mdx deleted file mode 100644 index e7bd19c83..000000000 --- a/www/docs/pages/example.mdx +++ /dev/null @@ -1,3 +0,0 @@ -# Example - -This is an example page. \ No newline at end of file diff --git a/www/docs/pages/getting-started.mdx b/www/docs/pages/getting-started.mdx index 946b2f659..770f2d111 100644 --- a/www/docs/pages/getting-started.mdx +++ b/www/docs/pages/getting-started.mdx @@ -1,8 +1,28 @@ # Getting Started -MCP-Go makes it easy to build Model Context Protocol (MCP) servers in Go. This guide will help you create your first MCP server in just a few minutes. +## Introduction + +### What is MCP? + +The Model Context Protocol (MCP) is an open standard that enables secure, controlled connections between AI applications and external data sources and tools. It provides a standardized way for Large Language Models (LLMs) to access and interact with external systems while maintaining security and user control. + +### Why MCP Go? + +MCP-Go is designed to make building MCP servers in Go fast, simple, and complete: -## Installation +- **Fast**: Minimal overhead with efficient Go implementation +- **Simple**: Clean, intuitive API with minimal boilerplate +- **Complete**: Full support for the MCP specification including tools, resources, and prompts + +### Key Features + +- **High-level interface**: Focus on your business logic, not protocol details +- **Minimal boilerplate**: Get started with just a few lines of code +- **Full MCP spec support**: Tools, resources, prompts, and all transport methods +- **Type safety**: Leverage Go's type system for robust MCP servers +- **Multiple transports**: Stdio, HTTP, and Server-Sent Events support + +### Installation Add MCP-Go to your Go project: @@ -10,6 +30,8 @@ Add MCP-Go to your Go project: go get github.com/mark3labs/mcp-go ``` +MCP-Go makes it easy to build Model Context Protocol (MCP) servers in Go. This guide will help you create your first MCP server in just a few minutes. + ## Your First MCP Server Let's create a simple MCP server with a "hello world" tool: @@ -53,9 +75,9 @@ func main() { } func helloHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - name, ok := request.Params.Arguments["name"].(string) - if !ok { - return nil, errors.New("name must be a string") + name, err := request.RequireString("name") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil } return mcp.NewToolResultText(fmt.Sprintf("Hello, %s!", name)), nil diff --git a/www/docs/pages/quick-start.mdx b/www/docs/pages/quick-start.mdx new file mode 100644 index 000000000..f76417df2 --- /dev/null +++ b/www/docs/pages/quick-start.mdx @@ -0,0 +1,242 @@ +# Quick Start + +Get up and running with MCP-Go in minutes. This guide walks you through creating your first MCP server and client. + +## Hello World Server + +Let's start with the simplest possible MCP server - a "hello world" tool: + +```go +package main + +import ( + "context" + "errors" + "fmt" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func main() { + // Create a new MCP server + s := server.NewMCPServer( + "Hello World Server", + "1.0.0", + server.WithToolCapabilities(false), + ) + + // Define a simple tool + tool := mcp.NewTool("hello_world", + mcp.WithDescription("Say hello to someone"), + mcp.WithString("name", + mcp.Required(), + mcp.Description("Name of the person to greet"), + ), + ) + + // Add tool handler + s.AddTool(tool, helloHandler) + + // Start the stdio server + if err := server.ServeStdio(s); err != nil { + fmt.Printf("Server error: %v\n", err) + } +} + +func helloHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + name, err := request.RequireString("name") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + return mcp.NewToolResultText(fmt.Sprintf("Hello, %s! 👋", name)), nil +} +``` + +Save this as `hello-server/main.go` and run: + +```bash +cd hello-server +go mod init hello-server +go get github.com/mark3labs/mcp-go +go run main.go +``` + +## Running Your First Server + +### Testing with Claude Desktop + +1. **Install Claude Desktop** from [Anthropic's website](https://claude.ai/download) + +2. **Configure your server** by editing Claude's config file: + + **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` + **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` + + ```json + { + "mcpServers": { + "hello-world": { + "command": "go", + "args": ["run", "/path/to/your/hello-server/main.go"] + } + } + } + ``` + +3. **Restart Claude Desktop** and look for the 🔌 icon indicating MCP connection + +4. **Test your tool** by asking Claude: "Use the hello_world tool to greet Alice" + +### Testing with MCP Inspector + +For debugging and development, use the MCP Inspector: + +```bash +# Install the MCP Inspector +npm install -g @modelcontextprotocol/inspector + +# Run your server with the inspector +mcp-inspector go run main.go +``` + +This opens a web interface where you can test your tools interactively. + +## Basic Client Example + +You can also create MCP clients to connect to other servers: + +```go +package main + +import ( + "context" + "fmt" + "log" + + "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/mcp" +) + +func main() { + // Create a stdio client that connects to another MCP server + c, err := client.NewStdioClient( + "go", "run", "path/to/server/main.go", + ) + if err != nil { + log.Fatal(err) + } + defer c.Close() + + ctx := context.Background() + + // Initialize the connection + if err := c.Initialize(ctx); err != nil { + log.Fatal(err) + } + + // List available tools + tools, err := c.ListTools(ctx) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Available tools: %d\n", len(tools.Tools)) + for _, tool := range tools.Tools { + fmt.Printf("- %s: %s\n", tool.Name, tool.Description) + } + + // Call a tool + result, err := c.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolRequestParams{ + Name: "hello_world", + Arguments: map[string]interface{}{ + "name": "World", + }, + }, + }) + if err != nil { + log.Fatal(err) + } + + // Print the result + for _, content := range result.Content { + if content.Type == "text" { + fmt.Printf("Result: %s\n", content.Text) + } + } +} +``` + +### HTTP Client Example + +For HTTP-based servers, use the HTTP client: + +```go +package main + +import ( + "context" + "fmt" + "log" + + "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/mcp" +) + +func main() { + // Create an HTTP client + c := client.NewHTTPClient("http://localhost:8080/mcp") + defer c.Close() + + ctx := context.Background() + + // Initialize and use the client + if err := c.Initialize(ctx); err != nil { + log.Fatal(err) + } + + // Call a tool + result, err := c.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolRequestParams{ + Name: "hello_world", + Arguments: map[string]interface{}{ + "name": "HTTP World", + }, + }, + }) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Tool result: %+v\n", result) +} +``` + +## What's Next? + +Now that you have a working MCP server and client: + +- **Explore [Examples](/examples)** - See more complex use cases +- **Learn about [Tools](/tools)** - Create powerful tool interfaces +- **Add [Resources](/resources)** - Expose data sources to LLMs +- **Create [Prompts](/prompts)** - Build reusable prompt templates +- **Deploy your server** - Learn about production deployment options + +## Common Issues + +### Server Won't Start +- Check that the port isn't already in use +- Verify Go module dependencies are installed +- Ensure proper file permissions + +### Client Connection Failed +- Verify the server is running and accessible +- Check network connectivity for HTTP clients +- Validate stdio command paths for stdio clients + +### Tool Calls Failing +- Verify tool parameter types match the schema +- Check error handling in your tool functions +- Use the MCP Inspector for debugging \ No newline at end of file diff --git a/www/docs/pages/servers/advanced.mdx b/www/docs/pages/servers/advanced.mdx new file mode 100644 index 000000000..a49717da3 --- /dev/null +++ b/www/docs/pages/servers/advanced.mdx @@ -0,0 +1,828 @@ +# Advanced Server Features + +Explore powerful features that make MCP-Go servers production-ready: typed tools, session management, middleware, hooks, and more. + +## Typed Tools + +Typed tools provide compile-time type safety and automatic parameter validation, reducing boilerplate and preventing runtime errors. + +### Basic Typed Tool + +```go +package main + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +// Define input and output types +type CalculateInput struct { + Operation string `json:"operation" validate:"required,oneof=add subtract multiply divide"` + X float64 `json:"x" validate:"required"` + Y float64 `json:"y" validate:"required"` +} + +type CalculateOutput struct { + Result float64 `json:"result"` + Operation string `json:"operation"` +} + +func main() { + s := server.NewMCPServer("Typed Server", "1.0.0", + server.WithToolCapabilities(true), + ) + + // Create typed tool + tool := mcp.NewTool("calculate", + mcp.WithDescription("Perform arithmetic operations"), + mcp.WithString("operation", mcp.Required()), + mcp.WithNumber("x", mcp.Required()), + mcp.WithNumber("y", mcp.Required()), + ) + + // Add tool with typed handler + s.AddTool(tool, mcp.NewTypedToolHandler(handleCalculateTyped)) + + server.ServeStdio(s) +} + +func handleCalculateTyped(ctx context.Context, req mcp.CallToolRequest, input CalculateInput) (*mcp.CallToolResult, error) { + var result float64 + + switch input.Operation { + case "add": + result = input.X + input.Y + case "subtract": + result = input.X - input.Y + case "multiply": + result = input.X * input.Y + case "divide": + if input.Y == 0 { + return mcp.NewToolResultError("division by zero"), nil + } + result = input.X / input.Y + } + + output := CalculateOutput{ + Result: result, + Operation: input.Operation, + } + + jsonData, err := json.Marshal(output) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil +} +``` + +### Complex Typed Tool + +```go +type UserCreateInput struct { + Name string `json:"name" validate:"required,min=1,max=100"` + Email string `json:"email" validate:"required,email"` + Age int `json:"age" validate:"min=0,max=150"` + Tags []string `json:"tags" validate:"dive,min=1"` + Metadata map[string]string `json:"metadata"` + Active bool `json:"active"` +} + +type UserCreateOutput struct { + ID string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + CreatedAt time.Time `json:"created_at"` + Status string `json:"status"` +} + +func handleCreateUser(ctx context.Context, req mcp.CallToolRequest, input UserCreateInput) (*mcp.CallToolResult, error) { + // Validation is automatic based on struct tags + + // Create user in database + user := &User{ + ID: generateID(), + Name: input.Name, + Email: input.Email, + Age: input.Age, + Tags: input.Tags, + Metadata: input.Metadata, + Active: input.Active, + CreatedAt: time.Now(), + } + + if err := db.Create(user); err != nil { + return nil, fmt.Errorf("failed to create user: %w", err) + } + + output := &UserCreateOutput{ + ID: user.ID, + Name: user.Name, + Email: user.Email, + CreatedAt: user.CreatedAt, + Status: "created", + } + + return mcp.NewToolResultJSON(output), nil +} +``` + +### Custom Validation + +```go +import ( + "path/filepath" + "strings" + + "github.com/go-playground/validator/v10" +) + +type FileOperationInput struct { + Path string `json:"path" validate:"required,filepath"` + Operation string `json:"operation" validate:"required,oneof=read write delete"` + Content string `json:"content" validate:"required_if=Operation write"` +} + +// Custom validator +func init() { + validate := validator.New() + validate.RegisterValidation("filepath", validateFilePath) +} + +func validateFilePath(fl validator.FieldLevel) bool { + path := fl.Field().String() + + // Prevent directory traversal + if strings.Contains(path, "..") { + return false + } + + // Ensure path is within allowed directory + allowedDir := "/app/data" + absPath, err := filepath.Abs(path) + if err != nil { + return false + } + + return strings.HasPrefix(absPath, allowedDir) +} +``` + +## Session Management + +Handle multiple clients with per-session state and tools. + +### Per-Session State + +```go +type SessionState struct { + UserID string + Permissions []string + Settings map[string]interface{} + StartTime time.Time +} + +type SessionManager struct { + sessions map[string]*SessionState + mutex sync.RWMutex +} + +func NewSessionManager() *SessionManager { + return &SessionManager{ + sessions: make(map[string]*SessionState), + } +} + +func (sm *SessionManager) CreateSession(sessionID, userID string, permissions []string) { + sm.mutex.Lock() + defer sm.mutex.Unlock() + + sm.sessions[sessionID] = &SessionState{ + UserID: userID, + Permissions: permissions, + Settings: make(map[string]interface{}), + StartTime: time.Now(), + } +} + +func (sm *SessionManager) GetSession(sessionID string) (*SessionState, bool) { + sm.mutex.RLock() + defer sm.mutex.RUnlock() + + session, exists := sm.sessions[sessionID] + return session, exists +} + +func (sm *SessionManager) RemoveSession(sessionID string) { + sm.mutex.Lock() + defer sm.mutex.Unlock() + + delete(sm.sessions, sessionID) +} +``` + +### Session-Aware Tools + +```go +func main() { + sessionManager := NewSessionManager() + + hooks := &server.Hooks{} + + hooks.AddOnRegisterSession(func(ctx context.Context, session server.ClientSession) { + // Initialize session with default permissions + sessionManager.CreateSession(session.ID(), "anonymous", []string{"read"}) + log.Printf("Session %s started", session.ID()) + }) + + hooks.AddOnUnregisterSession(func(ctx context.Context, session server.ClientSession) { + sessionManager.RemoveSession(session.ID()) + log.Printf("Session %s ended", session.ID()) + }) + + s := server.NewMCPServer("Session Server", "1.0.0", + server.WithToolCapabilities(true), + server.WithHooks(hooks), + ) + + // Add session-aware tool + s.AddTool( + mcp.NewTool("get_user_data", + mcp.WithDescription("Get user-specific data"), + mcp.WithString("data_type", mcp.Required()), + ), + createSessionAwareTool(sessionManager), + ) + + server.ServeStdio(s) +} + +func createSessionAwareTool(sm *SessionManager) server.ToolHandler { + return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + sessionID := server.GetSessionID(ctx) + session, exists := sm.GetSession(sessionID) + if !exists { + return nil, fmt.Errorf("invalid session") + } + + dataType := req.Params.Arguments["data_type"].(string) + + // Check permissions + if !hasPermission(session.Permissions, "read") { + return nil, fmt.Errorf("insufficient permissions") + } + + // Get user-specific data + data, err := getUserData(session.UserID, dataType) + if err != nil { + return nil, err + } + + jsonData, err := json.Marshal(data) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil + } +} +``` + +## Middleware + +Add cross-cutting concerns like logging, authentication, and rate limiting. + +### Logging Middleware + +```go +type LoggingMiddleware struct { + logger *log.Logger +} + +func NewLoggingMiddleware(logger *log.Logger) *LoggingMiddleware { + return &LoggingMiddleware{logger: logger} +} + +func (m *LoggingMiddleware) ToolMiddleware(next server.ToolHandlerFunc) server.ToolHandlerFunc { + return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + start := time.Now() + sessionID := server.GetSessionID(ctx) + + m.logger.Printf("Tool call started: tool=%s", req.Params.Name) + + result, err := next(ctx, req) + + duration := time.Since(start) + if err != nil { + m.logger.Printf("Tool call failed: session=%s tool=%s duration=%v error=%v", + sessionID, req.Params.Name, duration, err) + } else { + m.logger.Printf("Tool call completed: session=%s tool=%s duration=%v", + sessionID, req.Params.Name, duration) + } + + return result, err + } +} + +func (m *LoggingMiddleware) ResourceMiddleware(next server.ResourceHandler) server.ResourceHandler { + return func(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + start := time.Now() + sessionID := server.GetSessionID(ctx) + + m.logger.Printf("Resource read started: session=%s uri=%s", sessionID, req.Params.URI) + + result, err := next(ctx, req) + + duration := time.Since(start) + if err != nil { + m.logger.Printf("Resource read failed: session=%s uri=%s duration=%v error=%v", + sessionID, req.Params.URI, duration, err) + } else { + m.logger.Printf("Resource read completed: session=%s uri=%s duration=%v", + sessionID, req.Params.URI, duration) + } + + return result, err + } +} +``` + +### Rate Limiting Middleware + +```go +type RateLimitMiddleware struct { + limiters map[string]*rate.Limiter + mutex sync.RWMutex + rate rate.Limit + burst int +} + +func NewRateLimitMiddleware(requestsPerSecond float64, burst int) *RateLimitMiddleware { + return &RateLimitMiddleware{ + limiters: make(map[string]*rate.Limiter), + rate: rate.Limit(requestsPerSecond), + burst: burst, + } +} + +func (m *RateLimitMiddleware) getLimiter(sessionID string) *rate.Limiter { + m.mutex.RLock() + limiter, exists := m.limiters[sessionID] + m.mutex.RUnlock() + + if !exists { + m.mutex.Lock() + limiter = rate.NewLimiter(m.rate, m.burst) + m.limiters[sessionID] = limiter + m.mutex.Unlock() + } + + return limiter +} + +func (m *RateLimitMiddleware) ToolMiddleware(next server.ToolHandler) server.ToolHandler { + return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + sessionID := server.GetSessionID(ctx) + limiter := m.getLimiter(sessionID) + + if !limiter.Allow() { + return nil, fmt.Errorf("rate limit exceeded for session %s", sessionID) + } + + return next(ctx, req) + } +} +``` + +### Authentication Middleware + +```go +type AuthMiddleware struct { + tokenValidator TokenValidator +} + +func NewAuthMiddleware(validator TokenValidator) *AuthMiddleware { + return &AuthMiddleware{tokenValidator: validator} +} + +func (m *AuthMiddleware) ToolMiddleware(next server.ToolHandler) server.ToolHandler { + return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Extract token from context or request + token := extractToken(ctx, req) + if token == "" { + return nil, fmt.Errorf("authentication required") + } + + // Validate token + user, err := m.tokenValidator.Validate(token) + if err != nil { + return nil, fmt.Errorf("invalid token: %w", err) + } + + // Add user to context + ctx = context.WithValue(ctx, "user", user) + + return next(ctx, req) + } +} +``` + +## Hooks + +Implement lifecycle callbacks for telemetry, logging, and custom behavior. + +### Comprehensive Hooks + +```go +type TelemetryHooks struct { + metrics MetricsCollector + logger *log.Logger +} + +func NewTelemetryHooks(metrics MetricsCollector, logger *log.Logger) *TelemetryHooks { + return &TelemetryHooks{ + metrics: metrics, + logger: logger, + } +} + +func (h *TelemetryHooks) OnServerStart() { + h.logger.Println("MCP Server starting") + h.metrics.Increment("server.starts") +} + +func (h *TelemetryHooks) OnServerStop() { + h.logger.Println("MCP Server stopping") + h.metrics.Increment("server.stops") +} + +func (h *TelemetryHooks) OnSessionStart(sessionID string) { + h.logger.Printf("Session started: %s", sessionID) + h.metrics.Increment("sessions.started") + h.metrics.Gauge("sessions.active", h.getActiveSessionCount()) +} + +func (h *TelemetryHooks) OnSessionEnd(sessionID string) { + h.logger.Printf("Session ended: %s", sessionID) + h.metrics.Increment("sessions.ended") + h.metrics.Gauge("sessions.active", h.getActiveSessionCount()) +} + +func (h *TelemetryHooks) OnToolCall(sessionID, toolName string, duration time.Duration, err error) { + h.metrics.Increment("tools.calls", map[string]string{ + "tool": toolName, + "session": sessionID, + }) + h.metrics.Histogram("tools.duration", duration.Seconds(), map[string]string{ + "tool": toolName, + }) + + if err != nil { + h.metrics.Increment("tools.errors", map[string]string{ + "tool": toolName, + }) + } +} + +func (h *TelemetryHooks) OnResourceRead(sessionID, uri string, duration time.Duration, err error) { + h.metrics.Increment("resources.reads", map[string]string{ + "session": sessionID, + }) + h.metrics.Histogram("resources.duration", duration.Seconds()) + + if err != nil { + h.metrics.Increment("resources.errors") + } +} +``` + +### Custom Business Logic Hooks + +```go +type BusinessHooks struct { + auditLogger AuditLogger + notifier Notifier +} + +func (h *BusinessHooks) OnToolCall(sessionID, toolName string, duration time.Duration, err error) { + // Audit sensitive operations + if isSensitiveTool(toolName) { + h.auditLogger.LogToolCall(sessionID, toolName, err) + } + + // Alert on errors + if err != nil { + h.notifier.SendAlert(fmt.Sprintf("Tool %s failed for session %s: %v", + toolName, sessionID, err)) + } + + // Monitor performance + if duration > 30*time.Second { + h.notifier.SendAlert(fmt.Sprintf("Slow tool execution: %s took %v", + toolName, duration)) + } +} + +func (h *BusinessHooks) OnSessionStart(sessionID string) { + // Initialize user-specific resources + h.initializeUserResources(sessionID) + + // Send welcome notification + h.notifier.SendWelcome(sessionID) +} + +func (h *BusinessHooks) OnSessionEnd(sessionID string) { + // Cleanup user resources + h.cleanupUserResources(sessionID) + + // Log session summary + h.auditLogger.LogSessionEnd(sessionID) +} +``` + +## Tool Filtering + +Conditionally expose tools based on context, permissions, or other criteria. + +### Permission-Based Filtering + +```go +type PermissionFilter struct { + sessionManager *SessionManager +} + +func NewPermissionFilter(sm *SessionManager) *PermissionFilter { + return &PermissionFilter{sessionManager: sm} +} + +func (f *PermissionFilter) FilterTools(ctx context.Context, tools []mcp.Tool) []mcp.Tool { + sessionID := server.GetSessionID(ctx) + session, exists := f.sessionManager.GetSession(sessionID) + if !exists { + return []mcp.Tool{} // No tools for invalid sessions + } + + var filtered []mcp.Tool + for _, tool := range tools { + if f.hasPermissionForTool(session, tool.Name) { + filtered = append(filtered, tool) + } + } + + return filtered +} + +func (f *PermissionFilter) hasPermissionForTool(session *SessionState, toolName string) bool { + requiredPermissions := map[string][]string{ + "delete_user": {"admin"}, + "modify_system": {"admin", "operator"}, + "read_data": {"admin", "operator", "user"}, + "create_report": {"admin", "operator", "user"}, + } + + required, exists := requiredPermissions[toolName] + if !exists { + return true // Allow tools without specific requirements + } + + for _, permission := range session.Permissions { + for _, req := range required { + if permission == req { + return true + } + } + } + + return false +} +``` + +### Context-Based Filtering + +```go +type ContextFilter struct{} + +func (f *ContextFilter) FilterTools(ctx context.Context, tools []mcp.Tool) []mcp.Tool { + timeOfDay := time.Now().Hour() + environment := os.Getenv("ENVIRONMENT") + + var filtered []mcp.Tool + for _, tool := range tools { + if f.shouldIncludeTool(tool, timeOfDay, environment) { + filtered = append(filtered, tool) + } + } + + return filtered +} + +func (f *ContextFilter) shouldIncludeTool(tool mcp.Tool, hour int, env string) bool { + // Maintenance tools only during off-hours + maintenanceTools := map[string]bool{ + "backup_database": true, + "cleanup_logs": true, + "restart_service": true, + } + + if maintenanceTools[tool.Name] { + return hour < 6 || hour > 22 // Only between 10 PM and 6 AM + } + + // Debug tools only in development + debugTools := map[string]bool{ + "debug_session": true, + "dump_state": true, + } + + if debugTools[tool.Name] { + return env == "development" + } + + return true +} +``` + +## Notifications + +Send server-to-client messages for real-time updates. + +### Custom Notifications + +```go +func handleLongRunningTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + srv := server.ServerFromContext(ctx) + + // Simulate long-running work + for i := 0; i < 100; i++ { + time.Sleep(100 * time.Millisecond) + + // Send custom notification to all clients + notification := map[string]interface{}{ + "type": "progress", + "progress": i + 1, + "total": 100, + "message": fmt.Sprintf("Processing step %d/100", i+1), + } + + srv.SendNotificationToAllClients("progress", notification) + } + + return mcp.NewToolResultText("Long operation completed successfully"), nil +} +``` + +### Custom Notifications + +```go +type CustomNotifier struct { + sessions map[string]chan mcp.Notification + mutex sync.RWMutex +} + +func NewCustomNotifier() *CustomNotifier { + return &CustomNotifier{ + sessions: make(map[string]chan mcp.Notification), + } +} + +func (n *CustomNotifier) RegisterSession(sessionID string) { + n.mutex.Lock() + defer n.mutex.Unlock() + + n.sessions[sessionID] = make(chan mcp.Notification, 100) +} + +func (n *CustomNotifier) UnregisterSession(sessionID string) { + n.mutex.Lock() + defer n.mutex.Unlock() + + if ch, exists := n.sessions[sessionID]; exists { + close(ch) + delete(n.sessions, sessionID) + } +} + +func (n *CustomNotifier) SendAlert(sessionID, message string, severity string) { + n.mutex.RLock() + defer n.mutex.RUnlock() + + if ch, exists := n.sessions[sessionID]; exists { + select { + case ch <- mcp.Notification{ + Type: "alert", + Data: map[string]interface{}{ + "message": message, + "severity": severity, + "timestamp": time.Now().Unix(), + }, + }: + default: + // Channel full, drop notification + } + } +} + +func (n *CustomNotifier) BroadcastSystemMessage(message string) { + n.mutex.RLock() + defer n.mutex.RUnlock() + + notification := mcp.Notification{ + Type: "system_message", + Data: map[string]interface{}{ + "message": message, + "timestamp": time.Now().Unix(), + }, + } + + for _, ch := range n.sessions { + select { + case ch <- notification: + default: + // Channel full, skip this session + } + } +} +``` + +## Production Configuration + +### Complete Production Server + +```go +func main() { + // Initialize components + logger := log.New(os.Stdout, "[MCP] ", log.LstdFlags) + metrics := NewPrometheusMetrics() + sessionManager := NewSessionManager() + notifier := NewCustomNotifier() + + // Create middleware + loggingMW := NewLoggingMiddleware(logger) + rateLimitMW := NewRateLimitMiddleware(10.0, 20) // 10 req/sec, burst 20 + authMW := NewAuthMiddleware(NewJWTValidator()) + + // Create hooks + telemetryHooks := NewTelemetryHooks(metrics, logger) + businessHooks := NewBusinessHooks(NewAuditLogger(), NewSlackNotifier()) + + // Create server with all features + s := server.NewMCPServer("Production Server", "1.0.0", + server.WithToolCapabilities(true), + server.WithResourceCapabilities(false, true), + server.WithPromptCapabilities(true), + server.WithRecovery(), + server.WithHooks(telemetryHooks), + server.WithToolHandlerMiddleware(loggingMW.ToolMiddleware), + server.WithToolFilter(NewPermissionFilter(sessionManager)), + ) + + // Add tools and resources + addProductionTools(s) + addProductionResources(s) + addProductionPrompts(s) + + // Start server with graceful shutdown + startWithGracefulShutdown(s) +} + +func startWithGracefulShutdown(s *server.MCPServer) { + // Setup signal handling + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + + // Start server in goroutine + go func() { + if err := server.ServeStdio(s); err != nil { + log.Printf("Server error: %v", err) + } + }() + + // Wait for shutdown signal + <-sigChan + log.Println("Shutting down server...") + + // Graceful shutdown with timeout + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := s.Shutdown(ctx); err != nil { + log.Printf("Shutdown error: %v", err) + } + + log.Println("Server stopped") +} +``` + +## Next Steps + +- **[Client Development](/clients)** - Learn to build MCP clients +- **[Testing](/testing)** - Comprehensive testing strategies +- **[Deployment](/deployment)** - Production deployment patterns \ No newline at end of file diff --git a/www/docs/pages/servers/basics.mdx b/www/docs/pages/servers/basics.mdx new file mode 100644 index 000000000..7b33f33ce --- /dev/null +++ b/www/docs/pages/servers/basics.mdx @@ -0,0 +1,304 @@ +# Server Basics + +Learn how to create, configure, and start MCP servers with different transport options. + +## Creating a Server + +The foundation of any MCP server is the `NewMCPServer()` function. This creates a server instance with basic metadata and optional configuration. + +### Basic Server Creation + +```go +package main + +import ( + "github.com/mark3labs/mcp-go/server" +) + +func main() { + // Create a basic server + s := server.NewMCPServer( + "My MCP Server", // Server name + "1.0.0", // Server version + ) + + // Start the server (stdio transport) + server.ServeStdio(s) +} +``` + +### Server with Options + +Use server options to configure capabilities and behavior: + +```go +s := server.NewMCPServer( + "Advanced Server", + "2.0.0", + server.WithToolCapabilities(true), // Enable tools + server.WithResourceCapabilities(true), // Enable resources + server.WithPromptCapabilities(true), // Enable prompts + server.WithRecovery(), // Add panic recovery + server.WithHooks(myHooks), // Add lifecycle hooks +) +``` + +## Server Configuration + +### Capabilities + +Capabilities tell clients what features your server supports: + +```go +// Enable specific capabilities +s := server.NewMCPServer( + "Specialized Server", + "1.0.0", + server.WithToolCapabilities(true), // Can execute tools + server.WithResourceCapabilities(true), // Can provide resources + server.WithPromptCapabilities(true), // Can provide prompts +) + +// Or enable all capabilities +s := server.NewMCPServer( + "Full-Featured Server", + "1.0.0", + server.WithToolCapabilities(true), + server.WithResourceCapabilities(true), + server.WithPromptCapabilities(true), +) +``` + +**Capability types:** +- **Tools**: Server can execute function calls from LLMs +- **Resources**: Server can provide data/content to LLMs +- **Prompts**: Server can provide prompt templates + +### Recovery Middleware + +Add automatic panic recovery to prevent server crashes: + +```go +s := server.NewMCPServer( + "Robust Server", + "1.0.0", + server.WithRecovery(), // Automatically recover from panics +) +``` + +This catches panics in handlers and returns proper error responses instead of crashing. + +### Custom Metadata + +Add additional server information: + +```go +s := server.NewMCPServer( + "My Server", + "1.0.0", + server.WithInstructions("A server that does amazing things"), +) +``` + +## Starting Servers + +MCP-Go supports multiple transport methods for different deployment scenarios. + +### Stdio Transport + +Standard input/output - most common for local tools: + +```go +func main() { + s := server.NewMCPServer("My Server", "1.0.0") + + // Start stdio server (blocks until terminated) + if err := server.ServeStdio(s); err != nil { + log.Fatal(err) + } +} +``` + +**Best for:** +- Local development tools +- CLI integrations +- Desktop applications +- Single-client scenarios + +### HTTP Transport + +Traditional HTTP request/response: + +```go +func main() { + s := server.NewMCPServer("HTTP Server", "1.0.0") + + // Create HTTP server + httpServer := server.NewStreamableHTTPServer(s) + + // Start HTTP server on port 8080 + if err := httpServer.Start(":8080"); err != nil { + log.Fatal(err) + } +} +``` + +**Best for:** +- Web services +- Load-balanced deployments +- REST-like APIs +- Caching scenarios + +### Server-Sent Events (SSE) + +HTTP-based streaming for real-time updates: + +```go +func main() { + s := server.NewMCPServer("SSE Server", "1.0.0") + + // Create SSE server + sseServer := server.NewSSEServer(s) + + // Start SSE server on port 8080 + if err := sseServer.Start(":8080"); err != nil { + log.Fatal(err) + } +} +``` + +**Best for:** +- Web applications +- Real-time notifications +- Multiple concurrent clients +- Browser-based tools + +### Custom Transport Options + +Configure transport-specific options: + +```go +// HTTP with custom options +httpServer := server.NewStreamableHTTPServer(s, + server.WithEndpointPath("/mcp"), + server.WithStateless(true), +) + +if err := httpServer.Start(":8080"); err != nil { + log.Fatal(err) +} + +// SSE with custom options +sseServer := server.NewSSEServer(s, + server.WithSSEEndpoint("/events"), + server.WithMessageEndpoint("/message"), + server.WithKeepAlive(true), +) + +if err := sseServer.Start(":8080"); err != nil { + log.Fatal(err) +} +``` + +## Environment-Based Configuration + +Configure servers based on environment variables: + +```go +func main() { + s := server.NewMCPServer("Configurable Server", "1.0.0") + + // Choose transport based on environment + transport := os.Getenv("MCP_TRANSPORT") + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + switch transport { + case "http": + httpServer := server.NewStreamableHTTPServer(s) + httpServer.Start(":"+port) + case "sse": + sseServer := server.NewSSEServer(s) + sseServer.Start(":"+port) + default: + server.ServeStdio(s) + } +} +} +``` + +## Server Lifecycle + +Understanding the server lifecycle helps with proper resource management: + +```go +func main() { + hooks := &server.Hooks{} + + // Add session lifecycle hooks + hooks.AddOnRegisterSession(func(ctx context.Context, session server.ClientSession) { + log.Printf("Client %s connected", session.ID()) + }) + + hooks.AddOnUnregisterSession(func(ctx context.Context, session server.ClientSession) { + log.Printf("Client %s disconnected", session.ID()) + }) + + // Add request hooks + hooks.AddBeforeAny(func(ctx context.Context, id any, method mcp.MCPMethod, message any) { + log.Printf("Processing %s request", method) + }) + + hooks.AddOnError(func(ctx context.Context, id any, method mcp.MCPMethod, message any, err error) { + log.Printf("Error in %s: %v", method, err) + }) + + s := server.NewMCPServer("Lifecycle Server", "1.0.0", + server.WithHooks(hooks), + ) + + // Graceful shutdown + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + + go func() { + <-c + log.Println("Shutting down server...") + s.Shutdown() + }() + + server.ServeStdio(s) +} +``` + +## Error Handling + +Proper error handling ensures robust server operation: + +```go +func main() { + s := server.NewMCPServer("Error-Safe Server", "1.0.0", + server.WithRecovery(), // Panic recovery + ) + + // Add error handling for server startup + if err := server.ServeStdio(s); err != nil { + if errors.Is(err, server.ErrServerClosed) { + log.Println("Server closed gracefully") + } else { + log.Fatalf("Server error: %v", err) + } + } +} +``` + +## Next Steps + +Now that you understand server basics, learn how to add functionality: + +- **[Resources](/servers/resources)** - Expose data to LLMs +- **[Tools](/servers/tools)** - Provide functionality to LLMs +- **[Prompts](/servers/prompts)** - Create reusable interaction templates +- **[Advanced Features](/servers/advanced)** - Hooks, middleware, and more \ No newline at end of file diff --git a/www/docs/pages/servers/index.mdx b/www/docs/pages/servers/index.mdx new file mode 100644 index 000000000..e0249172a --- /dev/null +++ b/www/docs/pages/servers/index.mdx @@ -0,0 +1,148 @@ +# Building MCP Servers + +Learn how to build powerful MCP servers with MCP-Go. This section covers everything from basic server setup to advanced features like typed tools and session management. + +## Overview + +MCP servers expose tools, resources, and prompts to LLM clients. MCP-Go makes it easy to build robust servers with minimal boilerplate while providing full control over advanced features. + +## What You'll Learn + +- **[Server Basics](/servers/basics)** - Creating and configuring servers +- **[Resources](/servers/resources)** - Exposing data to LLMs +- **[Tools](/servers/tools)** - Providing functionality to LLMs +- **[Prompts](/servers/prompts)** - Creating reusable interaction templates +- **[Advanced Features](/servers/advanced)** - Typed tools, middleware, hooks, and more + +## Quick Example + +Here's a complete MCP server that demonstrates the key concepts: + +```go +package main + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +var start time.Time + +func main() { + start = time.Now() + // Create server with capabilities + s := server.NewMCPServer( + "Demo Server", + "1.0.0", + server.WithToolCapabilities(true), + server.WithResourceCapabilities(false, true), + server.WithPromptCapabilities(true), + ) + + // Add a tool + s.AddTool( + mcp.NewTool("get_time", + mcp.WithDescription("Get the current time"), + mcp.WithString("format", + mcp.Description("Time format (RFC3339, Unix, etc.)"), + mcp.DefaultString("RFC3339"), + ), + ), + handleGetTime, + ) + + // Add a resource + s.AddResource( + mcp.NewResource( + "config://server", + "Server Configuration", + mcp.WithResourceDescription("Current server configuration"), + mcp.WithMIMEType("application/json"), + ), + handleConfig, + ) + + // Add a prompt + s.AddPrompt( + mcp.NewPrompt("analyze_logs", + mcp.WithPromptDescription("Analyze server logs for issues"), + mcp.WithArgument("log_level", + mcp.ArgumentDescription("Minimum log level to analyze"), + ), + ), + handleAnalyzeLogs, + ) + + // Start the server + if err := server.ServeStdio(s); err != nil { + fmt.Printf("Server error: %v\n", err) + } +} + +func handleGetTime(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + format := req.GetString("format", "RFC3339") + + var timeStr string + switch format { + case "Unix": + timeStr = fmt.Sprintf("%d", time.Now().Unix()) + default: + timeStr = time.Now().Format(time.RFC3339) + } + + return mcp.NewToolResultText(timeStr), nil +} + +func handleConfig(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { + config := map[string]interface{}{ + "name": "Demo Server", + "version": "1.0.0", + "uptime": time.Since(start).String(), + } + + configJSON, err := json.Marshal(config) + if err != nil { + return nil, err + } + + return []mcp.ResourceContents{ + mcp.TextResourceContents{ + URI: req.Params.URI, + MIMEType: "application/json", + Text: string(configJSON), + }, + }, nil +} + +func handleAnalyzeLogs(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + logLevel := "error" // default value + if args := req.Params.Arguments; args != nil { + if level, ok := args["log_level"].(string); ok { + logLevel = level + } + } + + return &mcp.GetPromptResult{ + Description: "Analyze server logs for potential issues", + Messages: []mcp.PromptMessage{ + { + Role: mcp.RoleUser, + Content: mcp.NewTextContent(fmt.Sprintf( + "Please analyze the server logs for entries at %s level or higher. "+ + "Look for patterns, errors, and potential issues that need attention.", + logLevel, + )), + }, + }, + }, nil +} +``` + +## Next Steps + +Start with [Server Basics](/servers/basics) to learn how to create and configure your first MCP server, then explore the other sections to add resources, tools, and advanced features. \ No newline at end of file diff --git a/www/docs/pages/servers/prompts.mdx b/www/docs/pages/servers/prompts.mdx new file mode 100644 index 000000000..be3f9b9b8 --- /dev/null +++ b/www/docs/pages/servers/prompts.mdx @@ -0,0 +1,564 @@ +# Implementing Prompts + +Prompts are reusable interaction templates that help structure conversations between users and LLMs. They provide context, instructions, and can include dynamic content from resources. + +## Prompt Fundamentals + +Prompts in MCP serve as templates that can be invoked by LLMs to generate structured interactions. They're particularly useful for complex workflows, analysis tasks, or any scenario where you want to provide consistent context and instructions. + +### Basic Prompt Structure + +```go +// Create a simple prompt +prompt := mcp.NewPrompt("code_review", + mcp.WithPromptDescription("Review code for best practices and issues"), + mcp.WithPromptArgument("code", + mcp.Required(), + mcp.Description("The code to review"), + ), + mcp.WithPromptArgument("language", + mcp.Description("Programming language"), + mcp.Default("auto-detect"), + ), +) +``` + +## Prompt Templates + +### Basic Code Review Prompt + +```go +import ( + "context" + "fmt" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func main() { + s := server.NewMCPServer("Code Assistant", "1.0.0", + server.WithPromptCapabilities(true), + ) + + // Code review prompt + codeReviewPrompt := mcp.NewPrompt("code_review", + mcp.WithPromptDescription("Review code for best practices, bugs, and improvements"), + mcp.WithPromptArgument("code", + mcp.Required(), + mcp.Description("The code to review"), + ), + mcp.WithPromptArgument("language", + mcp.Description("Programming language (auto-detected if not specified)"), + ), + mcp.WithPromptArgument("focus", + mcp.Description("Specific areas to focus on"), + mcp.Enum("security", "performance", "readability", "best-practices", "all"), + mcp.Default("all"), + ), + ) + + s.AddPrompt(codeReviewPrompt, handleCodeReview) + server.ServeStdio(s) +} + +func handleCodeReview(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + // Extract arguments safely + args := req.Params.Arguments + if args == nil { + return nil, fmt.Errorf("missing required arguments") + } + + code, ok := args["code"].(string) + if !ok { + return nil, fmt.Errorf("code argument is required and must be a string") + } + + language := getStringArg(args, "language", "auto-detect") + focus := getStringArg(args, "focus", "all") + + // Build the prompt based on focus area + var instructions string + switch focus { + case "security": + instructions = "Focus specifically on security vulnerabilities, input validation, and potential attack vectors." + case "performance": + instructions = "Focus on performance optimizations, algorithmic efficiency, and resource usage." + case "readability": + instructions = "Focus on code clarity, naming conventions, and maintainability." + case "best-practices": + instructions = "Focus on language-specific best practices and design patterns." + default: + instructions = "Provide a comprehensive review covering security, performance, readability, and best practices." + } + + return &mcp.GetPromptResult{ + Description: fmt.Sprintf("Code review for %s code", language), + Messages: []mcp.PromptMessage{ + { + Role: "user", + Content: mcp.NewTextContent(fmt.Sprintf( + "Please review the following %s code:\n\n%s\n\nInstructions: %s\n\nPlease provide:\n1. Overall assessment\n2. Specific issues found\n3. Suggested improvements\n4. Best practice recommendations\n\nCode:\n +``` + +### Data Analysis Prompt + +```go +func handleDataAnalysis(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + datasetURI := req.Params.Arguments["dataset_uri"].(string) + analysisType := getStringArg(req.Params.Arguments, "analysis_type", "exploratory") + focusAreas := getStringSliceArg(req.Params.Arguments, "focus_areas", []string{}) + + // Fetch the dataset (this would typically read from a resource) + dataset, err := fetchDataset(ctx, datasetURI) + if err != nil { + return nil, fmt.Errorf("failed to fetch dataset: %w", err) + } + + // Build analysis instructions + var instructions strings.Builder + instructions.WriteString("Please analyze the provided dataset. ") + + switch analysisType { + case "exploratory": + instructions.WriteString("Perform exploratory data analysis including summary statistics, distributions, and patterns.") + case "predictive": + instructions.WriteString("Focus on predictive modeling opportunities and feature relationships.") + case "diagnostic": + instructions.WriteString("Identify data quality issues, outliers, and potential problems.") + } + + if len(focusAreas) > 0 { + instructions.WriteString(fmt.Sprintf(" Pay special attention to: %s.", strings.Join(focusAreas, ", "))) + } + + return &mcp.GetPromptResult{ + Description: fmt.Sprintf("%s analysis of dataset", strings.Title(analysisType)), + Messages: []mcp.PromptMessage{ + { + Role: "user", + Content: mcp.NewTextContent(fmt.Sprintf(`%s + +Dataset Information: +- Source: %s +- Records: %d +- Columns: %s + +Dataset Preview: +%s + +Please provide a comprehensive analysis including: +1. Data overview and quality assessment +2. Key insights and patterns +3. Recommendations for further analysis +4. Potential issues or concerns`, + instructions.String(), + datasetURI, + dataset.RecordCount, + strings.Join(dataset.Columns, ", "), + dataset.Preview, + )), + }, + }, + }, nil +} +``` + +## Prompt Arguments + +### Flexible Parameter Handling + +```go +func handleFlexiblePrompt(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + // Required arguments + task := req.Params.Arguments["task"].(string) + + // Optional arguments with defaults + tone := getStringArg(req.Params.Arguments, "tone", "professional") + length := getStringArg(req.Params.Arguments, "length", "medium") + audience := getStringArg(req.Params.Arguments, "audience", "general") + + // Array arguments + keywords := getStringSliceArg(req.Params.Arguments, "keywords", []string{}) + + // Object arguments + var constraints map[string]interface{} + if c, exists := req.Params.Arguments["constraints"]; exists { + constraints = c.(map[string]interface{}) + } + + // Build prompt based on parameters + prompt := buildDynamicPrompt(task, tone, length, audience, keywords, constraints) + + return &mcp.GetPromptResult{ + Description: fmt.Sprintf("Generate %s content", task), + Messages: []mcp.PromptMessage{ + { + Role: "user", + Content: mcp.NewTextContent(prompt), + }, + }, + }, nil +} + +func getStringArg(args map[string]interface{}, key, defaultValue string) string { + if val, exists := args[key]; exists { + if str, ok := val.(string); ok { + return str + } + } + return defaultValue +} + +func getStringSliceArg(args map[string]interface{}, key string, defaultValue []string) []string { + if val, exists := args[key]; exists { + if slice, ok := val.([]interface{}); ok { + result := make([]string, len(slice)) + for i, v := range slice { + if str, ok := v.(string); ok { + result[i] = str + } + } + return result + } + } + return defaultValue +} +``` + +## Message Types + +### Multi-Message Conversations + +```go +func handleConversationPrompt(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + scenario := req.Params.Arguments["scenario"].(string) + userRole := getStringArg(req.Params.Arguments, "user_role", "customer") + + var messages []mcp.PromptMessage + + switch scenario { + case "customer_support": + messages = []mcp.PromptMessage{ + { + Role: "system", + Content: mcp.NewTextContent("You are a helpful customer support representative. Be polite, professional, and solution-oriented."), + }, + { + Role: "user", + Content: mcp.NewTextContent(fmt.Sprintf("I'm a %s with a question about your service.", userRole)), + }, + { + Role: "assistant", + Content: mcp.NewTextContent("Hello! I'm here to help. What can I assist you with today?"), + }, + { + Role: "user", + Content: mcp.NewTextContent("Please continue the conversation based on the customer's needs."), + }, + } + + case "technical_interview": + messages = []mcp.PromptMessage{ + { + Role: "system", + Content: mcp.NewTextContent("You are conducting a technical interview. Ask thoughtful questions and provide constructive feedback."), + }, + { + Role: "user", + Content: mcp.NewTextContent("Let's begin the technical interview. Please start with an appropriate question."), + }, + } + } + + return &mcp.GetPromptResult{ + Description: fmt.Sprintf("%s conversation scenario", strings.Title(scenario)), + Messages: messages, + }, nil +} +``` + +### System and User Roles + +```go +func handleRoleBasedPrompt(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + expertise := req.Params.Arguments["expertise"].(string) + task := req.Params.Arguments["task"].(string) + context := getStringArg(req.Params.Arguments, "context", "") + + // Define system message based on expertise + var systemMessage string + switch expertise { + case "software_engineer": + systemMessage = "You are an experienced software engineer with expertise in system design, code quality, and best practices." + case "data_scientist": + systemMessage = "You are a data scientist with expertise in statistical analysis, machine learning, and data visualization." + case "product_manager": + systemMessage = "You are a product manager with expertise in user experience, market analysis, and feature prioritization." + default: + systemMessage = fmt.Sprintf("You are an expert in %s.", expertise) + } + + messages := []mcp.PromptMessage{ + { + Role: "system", + Content: mcp.NewTextContent(systemMessage), + }, + } + + // Add context if provided + if context != "" { + messages = append(messages, mcp.PromptMessage{ + Role: "user", + Content: mcp.NewTextContent(fmt.Sprintf("Context: %s", context)), + }) + } + + // Add the main task + messages = append(messages, mcp.PromptMessage{ + Role: "user", + Content: mcp.NewTextContent(task), + }) + + return &mcp.GetPromptResult{ + Description: fmt.Sprintf("%s task", strings.Title(expertise)), + Messages: messages, + }, nil +} +``` + +## Embedded Resources + +### Including Resource Data + +```go +func handleResourceEmbeddedPrompt(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + documentURI := req.Params.Arguments["document_uri"].(string) + analysisType := getStringArg(req.Params.Arguments, "analysis_type", "summary") + + // Fetch the document content + document, err := fetchResource(ctx, documentURI) + if err != nil { + return nil, fmt.Errorf("failed to fetch document: %w", err) + } + + // Build analysis prompt with embedded content + var instructions string + switch analysisType { + case "summary": + instructions = "Please provide a concise summary of the key points in this document." + case "critique": + instructions = "Please provide a critical analysis of the arguments and evidence presented." + case "questions": + instructions = "Please generate thoughtful questions that this document raises or could be used to explore." + case "action_items": + instructions = "Please extract actionable items and recommendations from this document." + } + + return &mcp.GetPromptResult{ + Description: fmt.Sprintf("Document %s", analysisType), + Messages: []mcp.PromptMessage{ + { + Role: "user", + Content: mcp.NewTextContent(fmt.Sprintf(`%s + +Document: %s +Content: +--- +%s +--- + +Please provide your analysis following the instructions above.`, + instructions, + documentURI, + document.Content, + )), + }, + }, + }, nil +} +``` + +### Dynamic Resource Integration + +```go +func handleDynamicResourcePrompt(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + resourceURIs := req.Params.Arguments["resource_uris"].([]interface{}) + promptType := getStringArg(req.Params.Arguments, "prompt_type", "compare") + + // Fetch all resources + var resources []ResourceData + for _, uri := range resourceURIs { + if uriStr, ok := uri.(string); ok { + resource, err := fetchResource(ctx, uriStr) + if err != nil { + return nil, fmt.Errorf("failed to fetch resource %s: %w", uriStr, err) + } + resources = append(resources, resource) + } + } + + // Build prompt based on type and resources + var content strings.Builder + + switch promptType { + case "compare": + content.WriteString("Please compare and contrast the following documents:\n\n") + for i, resource := range resources { + content.WriteString(fmt.Sprintf("Document %d (%s):\n%s\n\n", i+1, resource.URI, resource.Content)) + } + content.WriteString("Please provide:\n1. Key similarities\n2. Important differences\n3. Overall assessment") + + case "synthesize": + content.WriteString("Please synthesize information from the following sources:\n\n") + for i, resource := range resources { + content.WriteString(fmt.Sprintf("Source %d (%s):\n%s\n\n", i+1, resource.URI, resource.Content)) + } + content.WriteString("Please create a unified analysis that incorporates insights from all sources.") + } + + return &mcp.GetPromptResult{ + Description: fmt.Sprintf("%s multiple resources", strings.Title(promptType)), + Messages: []mcp.PromptMessage{ + { + Role: "user", + Content: mcp.NewTextContent(content.String()), + }, + }, + }, nil +} +``` + +## Advanced Prompt Patterns + +### Conditional Prompts + +```go +func handleConditionalPrompt(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + userLevel := getStringArg(req.Params.Arguments, "user_level", "beginner") + topic := req.Params.Arguments["topic"].(string) + includeExamples := getBoolArg(req.Params.Arguments, "include_examples", true) + + var prompt strings.Builder + + // Adjust complexity based on user level + switch userLevel { + case "beginner": + prompt.WriteString(fmt.Sprintf("Please explain %s in simple terms suitable for someone new to the topic. ", topic)) + prompt.WriteString("Use clear language and avoid jargon. ") + case "intermediate": + prompt.WriteString(fmt.Sprintf("Please provide a detailed explanation of %s. ", topic)) + prompt.WriteString("Include technical details but ensure clarity. ") + case "advanced": + prompt.WriteString(fmt.Sprintf("Please provide an in-depth analysis of %s. ", topic)) + prompt.WriteString("Include advanced concepts, edge cases, and technical nuances. ") + } + + if includeExamples { + prompt.WriteString("Please include relevant examples and practical applications.") + } + + return &mcp.GetPromptResult{ + Description: fmt.Sprintf("%s explanation for %s level", topic, userLevel), + Messages: []mcp.PromptMessage{ + { + Role: "user", + Content: mcp.NewTextContent(prompt.String()), + }, + }, + }, nil +} + +func getBoolArg(args map[string]interface{}, key string, defaultValue bool) bool { + if val, exists := args[key]; exists { + if b, ok := val.(bool); ok { + return b + } + } + return defaultValue +} +``` + +### Template-Based Prompts + +```go +type PromptTemplate struct { + Name string + Description string + Template string + Variables []string +} + +var promptTemplates = map[string]PromptTemplate{ + "bug_report": { + Name: "Bug Report Analysis", + Description: "Analyze a bug report and suggest solutions", + Template: `Please analyze this bug report: + +**Bug Description:** {{.description}} +**Steps to Reproduce:** {{.steps}} +**Expected Behavior:** {{.expected}} +**Actual Behavior:** {{.actual}} +**Environment:** {{.environment}} + +Please provide: +1. Root cause analysis +2. Potential solutions +3. Prevention strategies +4. Priority assessment`, + Variables: []string{"description", "steps", "expected", "actual", "environment"}, + }, + "feature_request": { + Name: "Feature Request Evaluation", + Description: "Evaluate a feature request", + Template: `Please evaluate this feature request: + +**Feature:** {{.feature}} +**Use Case:** {{.use_case}} +**User Story:** {{.user_story}} +**Acceptance Criteria:** {{.criteria}} + +Please assess: +1. Business value and impact +2. Technical feasibility +3. Implementation complexity +4. Potential risks and considerations`, + Variables: []string{"feature", "use_case", "user_story", "criteria"}, + }, +} + +func handleTemplatePrompt(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + templateName := req.Params.Arguments["template"].(string) + variables := req.Params.Arguments["variables"].(map[string]interface{}) + + template, exists := promptTemplates[templateName] + if !exists { + return nil, fmt.Errorf("unknown template: %s", templateName) + } + + // Replace template variables + content := template.Template + for _, variable := range template.Variables { + if value, exists := variables[variable]; exists { + placeholder := fmt.Sprintf("{{.%s}}", variable) + content = strings.ReplaceAll(content, placeholder, fmt.Sprintf("%v", value)) + } + } + + return &mcp.GetPromptResult{ + Description: template.Description, + Messages: []mcp.PromptMessage{ + { + Role: "user", + Content: mcp.NewTextContent(content), + }, + }, + }, nil +} +``` + +## Next Steps + +- **[Advanced Features](/servers/advanced)** - Explore typed tools, middleware, and hooks +- **[Client Integration](/clients)** - Learn how to build MCP clients +- **[Testing](/testing)** - Comprehensive testing strategies \ No newline at end of file diff --git a/www/docs/pages/servers/resources.mdx b/www/docs/pages/servers/resources.mdx new file mode 100644 index 000000000..5950f2b25 --- /dev/null +++ b/www/docs/pages/servers/resources.mdx @@ -0,0 +1,550 @@ +# Implementing Resources + +Resources expose data to LLMs in a read-only manner. Think of them as GET endpoints that provide access to files, databases, APIs, or any other data source. + +## Resource Fundamentals + +Resources in MCP are identified by URIs and can be either static (fixed content) or dynamic (generated on-demand). They're perfect for giving LLMs access to documentation, configuration files, database records, or API responses. + +### Basic Resource Structure + +```go +// Create a simple resource +resource := mcp.NewResource( + "docs://readme", // URI - unique identifier + "Project README", // Name - human-readable + mcp.WithResourceDescription("Main project documentation"), + mcp.WithMIMEType("text/markdown"), +) +``` + +## Static Resources + +Static resources have fixed URIs and typically serve predetermined content. + +### File-Based Resources + +Expose files from your filesystem: + +```go +func main() { + s := server.NewMCPServer("File Server", "1.0.0", + server.WithResourceCapabilities(true), + ) + + // Add a static file resource + s.AddResource( + mcp.NewResource( + "file://README.md", + "Project README", + mcp.WithResourceDescription("Main project documentation"), + mcp.WithMIMEType("text/markdown"), + ), + handleReadmeFile, + ) + + server.ServeStdio(s) +} + +func handleReadmeFile(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + content, err := os.ReadFile("README.md") + if err != nil { + return nil, fmt.Errorf("failed to read README: %w", err) + } + + return &mcp.ReadResourceResult{ + Contents: []mcp.ResourceContent{ + { + URI: req.Params.URI, + MIMEType: "text/markdown", + Text: string(content), + }, + }, + }, nil +} +``` + +### Configuration Resources + +Expose application configuration: + +```go +// Configuration resource +s.AddResource( + mcp.NewResource( + "config://app", + "Application Configuration", + mcp.WithResourceDescription("Current application settings"), + mcp.WithMIMEType("application/json"), + ), + handleAppConfig, +) + +func handleAppConfig(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + config := map[string]interface{}{ + "database_url": os.Getenv("DATABASE_URL"), + "debug_mode": os.Getenv("DEBUG") == "true", + "version": "1.0.0", + "features": []string{ + "authentication", + "caching", + "logging", + }, + } + + configJSON, err := json.Marshal(config) + if err != nil { + return nil, err + } + + return &mcp.ReadResourceResult{ + Contents: []mcp.ResourceContent{ + mcp.TextResourceContent{ + URI: req.Params.URI, + MIMEType: "application/json", + Text: string(configJSON), + }, + }, + }, nil +} +``` + +## Dynamic Resources + +Dynamic resources use URI templates with parameters, allowing for flexible, parameterized access to data. + +### URI Templates + +Use `{parameter}` syntax for dynamic parts: + +```go +// User profile resource with dynamic user ID +s.AddResource( + mcp.NewResource( + "users://{user_id}", + "User Profile", + mcp.WithResourceDescription("User profile information"), + mcp.WithMIMEType("application/json"), + ), + handleUserProfile, +) + +func handleUserProfile(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + // Extract user_id from URI + userID := extractUserID(req.Params.URI) // "users://123" -> "123" + + // Fetch user data (from database, API, etc.) + user, err := getUserFromDB(userID) + if err != nil { + return nil, fmt.Errorf("user not found: %w", err) + } + + jsonData, err := json.Marshal(user) + if err != nil { + return nil, err + } + + return []mcp.ResourceContents{ + mcp.TextResourceContents{ + URI: req.Params.URI, + MIMEType: "application/json", + Text: string(jsonData), + }, + }, nil +} + +func extractUserID(uri string) string { + // Extract ID from "users://123" format + parts := strings.Split(uri, "://") + if len(parts) == 2 { + return parts[1] + } + return "" +} +``` + +### Database Resources + +Expose database records dynamically: + +```go +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +// Database table resource +s.AddResource( + mcp.NewResource( + "db://{table}/{id}", + "Database Record", + mcp.WithResourceDescription("Access database records by table and ID"), + mcp.WithMIMEType("application/json"), + ), + handleDatabaseRecord, +) + +func handleDatabaseRecord(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + table, id := parseDBURI(req.Params.URI) // "db://users/123" -> "users", "123" + + // Validate table name for security + allowedTables := map[string]bool{ + "users": true, + "products": true, + "orders": true, + } + + if !allowedTables[table] { + return nil, fmt.Errorf("table not accessible: %s", table) + } + + // Query database + query := fmt.Sprintf("SELECT * FROM %s WHERE id = ?", table) + row := db.QueryRowContext(ctx, query, id) + + var data map[string]interface{} + if err := scanRowToMap(row, &data); err != nil { + return nil, fmt.Errorf("record not found: %w", err) + } + + jsonData, err := json.Marshal(data) + if err != nil { + return nil, err + } + + return &mcp.ReadResourceResult{ + Contents: []mcp.ResourceContent{ + mcp.TextResourceContent{ + URI: req.Params.URI, + MIMEType: "application/json", + Text: string(jsonData), + }, + }, + }, nil +} +} +``` + +### API Resources + +Proxy external APIs through resources: + +```go +// Weather API resource +s.AddResource( + mcp.NewResource( + "weather://{location}", + "Weather Data", + mcp.WithResourceDescription("Current weather for a location"), + mcp.WithMIMEType("application/json"), + ), + handleWeatherData, +) + +func handleWeatherData(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + location := extractLocation(req.Params.URI) + + // Call external weather API + apiURL := fmt.Sprintf("https://api.weather.com/v1/current?location=%s&key=%s", + url.QueryEscape(location), os.Getenv("WEATHER_API_KEY")) + + resp, err := http.Get(apiURL) + if err != nil { + return nil, fmt.Errorf("weather API error: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + return &mcp.ReadResourceResult{ + Contents: []mcp.ResourceContent{ + { + URI: req.Params.URI, + MIMEType: "application/json", + Text: string(body), + }, + }, + }, nil +} +``` + +## Content Types + +Resources can serve different types of content with appropriate MIME types. + +### Text Content + +```go +func handleTextResource(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + content := "This is plain text content" + + return &mcp.ReadResourceResult{ + Contents: []mcp.ResourceContent{ + { + URI: req.Params.URI, + MIMEType: "text/plain", + Text: content, + }, + }, + }, nil +} +``` + +### JSON Content + +```go +func handleJSONResource(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { + data := map[string]interface{}{ + "message": "Hello, World!", + "timestamp": time.Now().Unix(), + "status": "success", + } + + jsonData, err := json.Marshal(data) + if err != nil { + return nil, err + } + + return []mcp.ResourceContents{ + mcp.TextResourceContents{ + URI: req.Params.URI, + MIMEType: "application/json", + Text: string(jsonData), + }, + }, nil +} +``` + +### Binary Content + +```go +func handleImageResource(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + imageData, err := os.ReadFile("logo.png") + if err != nil { + return nil, err + } + + // Encode binary data as base64 + encoded := base64.StdEncoding.EncodeToString(imageData) + + return &mcp.ReadResourceResult{ + Contents: []mcp.ResourceContent{ + { + URI: req.Params.URI, + MIMEType: "image/png", + Blob: encoded, + }, + }, + }, nil +} +``` + +### Multiple Content Types + +A single resource can return multiple content representations: + +```go +func handleMultiFormatResource(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + data := map[string]interface{}{ + "name": "John Doe", + "age": 30, + "city": "New York", + } + + // JSON representation + jsonData, _ := json.Marshal(data) + + // Text representation + textData := fmt.Sprintf("Name: %s\nAge: %d\nCity: %s", + data["name"], data["age"], data["city"]) + + return &mcp.ReadResourceResult{ + Contents: []mcp.ResourceContent{ + { + URI: req.Params.URI, + MIMEType: "application/json", + Text: string(jsonData), + }, + { + URI: req.Params.URI, + MIMEType: "text/plain", + Text: textData, + }, + }, + }, nil +} +``` + +## Error Handling + +Proper error handling ensures robust resource access: + +### Common Error Patterns + +```go +func handleResourceWithErrors(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + // Validate URI format + if !isValidURI(req.Params.URI) { + return nil, fmt.Errorf("invalid URI format: %s", req.Params.URI) + } + + // Check permissions + if !hasPermission(ctx, req.Params.URI) { + return nil, fmt.Errorf("access denied to resource: %s", req.Params.URI) + } + + // Handle resource not found + data, err := fetchResourceData(req.Params.URI) + if err != nil { + if errors.Is(err, ErrResourceNotFound) { + return nil, fmt.Errorf("resource not found: %s", req.Params.URI) + } + return nil, fmt.Errorf("failed to fetch resource: %w", err) + } + + jsonData, err := json.Marshal(data) + if err != nil { + return nil, err + } + + return &mcp.ReadResourceResult{ + Contents: []mcp.ResourceContents{ + mcp.TextResourceContents{ + URI: req.Params.URI, + MIMEType: "application/json", + Text: string(jsonData), + }, + }, + }, nil +} +``` + +### Timeout Handling + +```go +func handleResourceWithTimeout(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + // Create timeout context + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + // Use context in operations + data, err := fetchDataWithContext(ctx, req.Params.URI) + if err != nil { + if errors.Is(err, context.DeadlineExceeded) { + return nil, fmt.Errorf("resource fetch timeout: %s", req.Params.URI) + } + return nil, err + } + + jsonData, err := json.Marshal(data) + if err != nil { + return nil, err + } + + return &mcp.ReadResourceResult{ + Contents: []mcp.ResourceContent{ + mcp.TextResourceContent{ + URI: req.Params.URI, + MIMEType: "application/json", + Text: string(jsonData), + }, + }, + }, nil +} +} +``` + +## Resource Listing + +Implement resource discovery for clients: + +```go +func main() { + s := server.NewMCPServer("Resource Server", "1.0.0", + server.WithResourceCapabilities(true), + ) + + // Add multiple resources + resources := []struct { + uri string + name string + description string + mimeType string + handler server.ResourceHandler + }{ + {"docs://readme", "README", "Project documentation", "text/markdown", handleReadme}, + {"config://app", "App Config", "Application settings", "application/json", handleConfig}, + {"users://{id}", "User Profile", "User information", "application/json", handleUser}, + } + + for _, r := range resources { + s.AddResource( + mcp.NewResource(r.uri, r.name, + mcp.WithResourceDescription(r.description), + mcp.WithMIMEType(r.mimeType), + ), + r.handler, + ) + } + + server.ServeStdio(s) +} +``` + +## Caching Resources + +Implement caching for expensive resources: + +```go +type CachedResourceHandler struct { + cache map[string]cacheEntry + mutex sync.RWMutex + ttl time.Duration +} + +type cacheEntry struct { + data *mcp.ReadResourceResult + timestamp time.Time +} + +func (h *CachedResourceHandler) HandleResource(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + h.mutex.RLock() + if entry, exists := h.cache[req.Params.URI]; exists { + if time.Since(entry.timestamp) < h.ttl { + h.mutex.RUnlock() + return entry.data, nil + } + } + h.mutex.RUnlock() + + // Fetch fresh data + data, err := h.fetchFreshData(ctx, req) + if err != nil { + return nil, err + } + + // Cache the result + h.mutex.Lock() + h.cache[req.Params.URI] = cacheEntry{ + data: data, + timestamp: time.Now(), + } + h.mutex.Unlock() + + return data, nil +} +``` + +## Next Steps + +- **[Tools](/servers/tools)** - Learn to implement interactive functionality +- **[Prompts](/servers/prompts)** - Create reusable interaction templates +- **[Advanced Features](/servers/advanced)** - Explore hooks, middleware, and more \ No newline at end of file diff --git a/www/docs/pages/servers/tools.mdx b/www/docs/pages/servers/tools.mdx new file mode 100644 index 000000000..af8f0b778 --- /dev/null +++ b/www/docs/pages/servers/tools.mdx @@ -0,0 +1,657 @@ +# Implementing Tools + +Tools provide functionality that LLMs can invoke to take actions or perform computations. Think of them as function calls that extend the LLM's capabilities. + +## Tool Fundamentals + +Tools are the primary way LLMs interact with your server to perform actions. They have structured schemas that define parameters, types, and constraints, ensuring type-safe interactions. + +### Basic Tool Structure + +```go +// Create a simple tool +tool := mcp.NewTool("calculate", + mcp.WithDescription("Perform arithmetic operations"), + mcp.WithString("operation", + mcp.Required(), + mcp.Enum("add", "subtract", "multiply", "divide"), + mcp.Description("The arithmetic operation to perform"), + ), + mcp.WithNumber("x", mcp.Required(), mcp.Description("First number")), + mcp.WithNumber("y", mcp.Required(), mcp.Description("Second number")), +) +``` + +## Tool Definition + +### Parameter Types + +MCP-Go supports various parameter types with validation: + +```go +// String parameters +mcp.WithString("name", + mcp.Required(), + mcp.Description("User's name"), + mcp.MinLength(1), + mcp.MaxLength(100), +) + +// Number parameters +mcp.WithNumber("age", + mcp.Required(), + mcp.Description("User's age"), + mcp.Minimum(0), + mcp.Maximum(150), +) + +// Integer parameters +mcp.WithInteger("count", + mcp.Default(10), + mcp.Description("Number of items"), + mcp.Minimum(1), + mcp.Maximum(1000), +) + +// Boolean parameters +mcp.WithBoolean("enabled", + mcp.Default(true), + mcp.Description("Whether feature is enabled"), +) + +// Array parameters +mcp.WithArray("tags", + mcp.Description("List of tags"), + mcp.Items(mcp.StringSchema()), +) + +// Object parameters +mcp.WithObject("config", + mcp.Description("Configuration object"), + mcp.Properties(map[string]mcp.Schema{ + "timeout": mcp.NumberSchema(), + "retries": mcp.IntegerSchema(), + }), +) +``` + +### Enums and Constraints + +```go +// Enum values +mcp.WithString("priority", + mcp.Required(), + mcp.Enum("low", "medium", "high", "critical"), + mcp.Description("Task priority level"), +) + +// String constraints +mcp.WithString("email", + mcp.Required(), + mcp.Pattern(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`), + mcp.Description("Valid email address"), +) + +// Number constraints +mcp.WithNumber("price", + mcp.Required(), + mcp.Minimum(0), + mcp.ExclusiveMaximum(10000), + mcp.Description("Product price in USD"), +) +``` + +## Tool Handlers + +Tool handlers process the actual function calls from LLMs. MCP-Go provides convenient helper methods for safe parameter extraction. + +### Parameter Extraction Methods + +MCP-Go offers several helper methods on `CallToolRequest` for type-safe parameter access: + +```go +// Required parameters - return error if missing or wrong type +name, err := req.RequireString("name") +age, err := req.RequireInt("age") +price, err := req.RequireFloat("price") +enabled, err := req.RequireBool("enabled") + +// Optional parameters with defaults +name := req.GetString("name", "default") +count := req.GetInt("count", 10) +price := req.GetFloat("price", 0.0) +enabled := req.GetBool("enabled", false) + +// Structured data binding +type Config struct { + Timeout int `json:"timeout"` + Retries int `json:"retries"` + Debug bool `json:"debug"` +} +var config Config +if err := req.BindArguments(&config); err != nil { + return mcp.NewToolResultError(err.Error()), nil +} + +// Raw access (for backward compatibility) +args := req.GetArguments() // returns map[string]any +rawArgs := req.GetRawArguments() // returns any +``` + +### Basic Handler Pattern + +```go +func handleCalculate(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Extract parameters using helper methods + operation, err := req.RequireString("operation") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + x, err := req.RequireFloat("x") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + y, err := req.RequireFloat("y") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + // Perform calculation + var result float64 + switch operation { + case "add": + result = x + y + case "subtract": + result = x - y + case "multiply": + result = x * y + case "divide": + if y == 0 { + return mcp.NewToolResultError("division by zero"), nil + } + result = x / y + default: + return mcp.NewToolResultError(fmt.Sprintf("unknown operation: %s", operation)), nil + } + + // Return result + return mcp.NewToolResultText(fmt.Sprintf("%.2f", result)), nil +} +``` + +### File Operations Tool + +```go +func main() { + s := server.NewMCPServer("File Tools", "1.0.0", + server.WithToolCapabilities(true), + ) + + // File creation tool + createFileTool := mcp.NewTool("create_file", + mcp.WithDescription("Create a new file with content"), + mcp.WithString("path", + mcp.Required(), + mcp.Description("File path to create"), + ), + mcp.WithString("content", + mcp.Required(), + mcp.Description("File content"), + ), + mcp.WithString("encoding", + mcp.Default("utf-8"), + mcp.Enum("utf-8", "ascii", "base64"), + mcp.Description("File encoding"), + ), + ) + + s.AddTool(createFileTool, handleCreateFile) + server.ServeStdio(s) +} + +func handleCreateFile(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + path, err := req.RequireString("path") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + content, err := req.RequireString("content") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + encoding := req.GetString("encoding", "utf-8") + + // Validate path for security + if strings.Contains(path, "..") { + return mcp.NewToolResultError("invalid path: directory traversal not allowed"), nil + } + + // Handle different encodings + var data []byte + switch encoding { + case "utf-8": + data = []byte(content) + case "ascii": + data = []byte(content) + case "base64": + var err error + data, err = base64.StdEncoding.DecodeString(content) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("invalid base64 content: %v", err)), nil + } + } + + // Create file + if err := os.WriteFile(path, data, 0644); err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to create file: %v", err)), nil + } + + return mcp.NewToolResultText(fmt.Sprintf("File created successfully: %s", path)), nil +} +``` + +### Database Query Tool + +```go +func handleDatabaseQuery(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Define struct to bind both Query and Params + var args struct { + Query string `json:"query"` + Params []interface{} `json:"params"` + } + + // Bind arguments to the struct + if err := req.BindArguments(&args); err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + // Extract values from the bound struct + query := args.Query + params := args.Params + + // Validate query for security (basic example) + if !isSelectQuery(query) { + return mcp.NewToolResultError("only SELECT queries are allowed"), nil + } + + // Execute query with timeout + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + rows, err := db.QueryContext(ctx, query, params...) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("query failed: %v", err)), nil + } + defer rows.Close() + + // Convert results to JSON + results, err := rowsToJSON(rows) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to process results: %v", err)), nil + } + + resultData := map[string]interface{}{ + "query": query, + "results": results, + "count": len(results), + } + + jsonData, err := json.Marshal(resultData) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal results: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil +} + +func isSelectQuery(query string) bool { + trimmed := strings.TrimSpace(strings.ToUpper(query)) + return strings.HasPrefix(trimmed, "SELECT") +} +``` + +### HTTP Request Tool + +```go +func handleHTTPRequest(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + url, err := req.RequireString("url") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + method, err := req.RequireString("method") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + body := req.GetString("body", "") + + // Handle headers (optional object parameter) + var headers map[string]interface{} + if args := req.GetArguments(); args != nil { + if h, ok := args["headers"].(map[string]interface{}); ok { + headers = h + } + } + + // Create HTTP request + httpReq, err := http.NewRequestWithContext(ctx, method, url, strings.NewReader(body)) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to create request: %v", err)), nil + } + + // Add headers + for key, value := range headers { + httpReq.Header.Set(key, fmt.Sprintf("%v", value)) + } + + // Execute request with timeout + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(httpReq) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("request failed: %v", err)), nil + } + defer resp.Body.Close() + + // Read response + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to read response: %v", err)), nil + } + + resultData := map[string]interface{}{ + "status_code": resp.StatusCode, + "headers": resp.Header, + "body": string(respBody), + } + + jsonData, err := json.Marshal(resultData) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil +} +``` + +## Argument Validation + +### Type-Safe Parameter Extraction + +MCP-Go provides helper methods for safe parameter extraction: + +```go +func handleValidatedTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Required parameters with validation + name, err := req.RequireString("name") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + age, err := req.RequireFloat("age") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + // Optional parameter with default + enabled := req.GetBool("enabled", true) + + // Validate constraints + if len(name) == 0 { + return mcp.NewToolResultError("name cannot be empty"), nil + } + + if age < 0 || age > 150 { + return mcp.NewToolResultError("age must be between 0 and 150"), nil + } + + // Process with validated parameters + result := processUser(name, int(age), enabled) + + jsonData, err := json.Marshal(result) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil +} +``` + +### Available Helper Methods + +```go +// Required parameters (return error if missing or wrong type) +name, err := req.RequireString("name") +age, err := req.RequireInt("age") +price, err := req.RequireFloat("price") +enabled, err := req.RequireBool("enabled") + +// Optional parameters with defaults +name := req.GetString("name", "default") +count := req.GetInt("count", 10) +price := req.GetFloat("price", 0.0) +enabled := req.GetBool("enabled", false) + +// Structured data binding +type UserData struct { + Name string `json:"name"` + Age int `json:"age"` +} +var user UserData +if err := req.BindArguments(&user); err != nil { + return mcp.NewToolResultError(err.Error()), nil +} +``` +``` + +### Custom Validation Functions + +```go +func validateEmail(email string) error { + emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) + if !emailRegex.MatchString(email) { + return fmt.Errorf("invalid email format") + } + return nil +} + +func validateURL(url string) error { + parsed, err := url.Parse(url) + if err != nil { + return fmt.Errorf("invalid URL format: %w", err) + } + + if parsed.Scheme != "http" && parsed.Scheme != "https" { + return fmt.Errorf("URL must use http or https scheme") + } + + return nil +} +``` + +## Result Types + +### Text Results + +```go +func handleTextTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + message := "Operation completed successfully" + return mcp.NewToolResultText(message), nil +} +``` + +### JSON Results + +```go +func handleJSONTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + result := map[string]interface{}{ + "status": "success", + "timestamp": time.Now().Unix(), + "data": map[string]interface{}{ + "processed": 42, + "errors": 0, + }, + } + + jsonData, err := json.Marshal(result) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil +} +``` + +### Multiple Content Types + +```go +func handleMultiContentTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + data := map[string]interface{}{ + "name": "John Doe", + "age": 30, + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + { + Type: "text", + Text: "User information retrieved successfully", + }, + { + Type: "text", + Text: fmt.Sprintf("Name: %s, Age: %d", data["name"], data["age"]), + }, + }, + }, nil +} +``` + +### Error Results + +```go +func handleToolWithErrors(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // For validation errors, return error result (not Go error) + name, err := req.RequireString("name") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + // For business logic errors, also return error result + if someCondition { + return mcp.NewToolResultError("invalid input: " + reason), nil + } + + // For system errors, you can return Go errors + if systemError { + return nil, fmt.Errorf("system failure: %v", err) + } + + // Or return structured error information + return &mcp.CallToolResult{ + Content: []mcp.Content{ + { + Type: "text", + Text: "Operation failed", + }, + }, + IsError: true, + }, nil +} +``` + +## Tool Annotations + +Provide hints to help LLMs use your tools effectively: + +```go +tool := mcp.NewTool("search_database", + mcp.WithDescription("Search the product database"), + mcp.WithString("query", + mcp.Required(), + mcp.Description("Search query (supports wildcards with *)"), + ), + mcp.WithNumber("limit", + mcp.DefaultNumber(10), + mcp.Minimum(1), + mcp.Maximum(100), + mcp.Description("Maximum number of results to return"), + ), + mcp.WithArray("categories", + mcp.Description("Filter by product categories"), + mcp.Items(mcp.StringSchema()), + ), +) + +s.AddTool(tool, handleSearchDatabase) +``` + +## Advanced Tool Patterns + +### Streaming Results + +For long-running operations, consider streaming results: + +```go +func handleStreamingTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // For operations that take time, provide progress updates + results := []string{} + + for i := 0; i < 10; i++ { + // Simulate work + time.Sleep(100 * time.Millisecond) + + // Check for cancellation + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + results = append(results, fmt.Sprintf("Processed item %d", i+1)) + } + + resultData := map[string]interface{}{ + "status": "completed", + "results": results, + } + + jsonData, err := json.Marshal(resultData) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil + } + + return mcp.NewToolResultText(string(jsonData)), nil +} +``` + +### Conditional Tools + +Tools that are only available under certain conditions: + +```go +func addConditionalTools(s *server.MCPServer, userRole string) { + // Admin-only tools + if userRole == "admin" { + adminTool := mcp.NewTool("delete_user", + mcp.WithDescription("Delete a user account (admin only)"), + mcp.WithString("user_id", mcp.Required()), + ) + s.AddTool(adminTool, handleDeleteUser) + } + + // User tools available to all + userTool := mcp.NewTool("get_profile", + mcp.WithDescription("Get user profile information"), + ) + s.AddTool(userTool, handleGetProfile) +} +``` + +## Next Steps + +- **[Prompts](/servers/prompts)** - Learn to create reusable interaction templates +- **[Advanced Features](/servers/advanced)** - Explore typed tools, middleware, and hooks +- **[Testing](/testing)** - Learn comprehensive testing strategies \ No newline at end of file diff --git a/www/docs/pages/transports/http.mdx b/www/docs/pages/transports/http.mdx new file mode 100644 index 000000000..85f4d34d0 --- /dev/null +++ b/www/docs/pages/transports/http.mdx @@ -0,0 +1,1034 @@ +# HTTP Transport + +HTTP transport provides traditional request/response communication for MCP servers, perfect for REST-like interactions, stateless clients, and integration with existing web infrastructure. + +## Use Cases + +HTTP transport excels in scenarios requiring: + +- **Web services**: Traditional REST API patterns +- **Stateless interactions**: Each request is independent +- **Load balancing**: Distribute requests across multiple servers +- **Caching**: Leverage HTTP caching mechanisms +- **Integration**: Work with existing HTTP infrastructure +- **Public APIs**: Expose MCP functionality as web APIs + +**Example applications:** +- Microservice architectures +- Public API endpoints +- Integration with API gateways +- Cached data services +- Rate-limited services +- Multi-tenant applications + +## Implementation + +### Basic HTTP Server + +```go +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "strings" + "time" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func main() { + s := server.NewMCPServer("HTTP API Server", "1.0.0", + server.WithToolCapabilities(true), + server.WithResourceCapabilities(true), + ) + + // Add RESTful tools + s.AddTool( + mcp.NewTool("get_user", + mcp.WithDescription("Get user information"), + mcp.WithString("user_id", mcp.Required()), + ), + handleGetUser, + ) + + s.AddTool( + mcp.NewTool("create_user", + mcp.WithDescription("Create a new user"), + mcp.WithString("name", mcp.Required()), + mcp.WithString("email", mcp.Required()), + mcp.WithInteger("age", mcp.Minimum(0)), + ), + handleCreateUser, + ) + + s.AddTool( + mcp.NewTool("search_users", + mcp.WithDescription("Search users with filters"), + mcp.WithString("query", mcp.Description("Search query")), + mcp.WithInteger("limit", mcp.Default(10), mcp.Maximum(100)), + mcp.WithInteger("offset", mcp.Default(0), mcp.Minimum(0)), + ), + handleSearchUsers, + ) + + // Add resources + s.AddResource( + mcp.NewResource( + "users://{user_id}", + "User Profile", + mcp.WithResourceDescription("User profile data"), + mcp.WithMIMEType("application/json"), + ), + handleUserResource, + ) + + // Start HTTP server + log.Println("Starting HTTP server on :8080") + if err := server.ServeHTTP(s, ":8080"); err != nil { + log.Fatal(err) + } +} + +func handleGetUser(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + userID := req.Params.Arguments["user_id"].(string) + + // Simulate database lookup + user, err := getUserFromDB(userID) + if err != nil { + return nil, fmt.Errorf("user not found: %s", userID) + } + + return mcp.NewToolResultJSON(user), nil +} + +func handleCreateUser(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + name := req.Params.Arguments["name"].(string) + email := req.Params.Arguments["email"].(string) + age := int(req.Params.Arguments["age"].(float64)) + + // Validate input + if !isValidEmail(email) { + return nil, fmt.Errorf("invalid email format: %s", email) + } + + // Create user + user := &User{ + ID: generateID(), + Name: name, + Email: email, + Age: age, + CreatedAt: time.Now(), + } + + if err := saveUserToDB(user); err != nil { + return nil, fmt.Errorf("failed to create user: %w", err) + } + + return mcp.NewToolResultJSON(map[string]interface{}{ + "id": user.ID, + "message": "User created successfully", + "user": user, + }), nil +} + +// Helper functions and types for the examples +type User struct { + ID string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Age int `json:"age"` + CreatedAt time.Time `json:"created_at"` +} + +func getUserFromDB(userID string) (*User, error) { + // Placeholder implementation + return &User{ + ID: userID, + Name: "John Doe", + Email: "john@example.com", + Age: 30, + }, nil +} + +func isValidEmail(email string) bool { + // Simple email validation + return strings.Contains(email, "@") && strings.Contains(email, ".") +} + +func generateID() string { + // Placeholder implementation + return fmt.Sprintf("user_%d", time.Now().UnixNano()) +} + +func saveUserToDB(user *User) error { + // Placeholder implementation + return nil +} + +func handleSearchUsers(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + query := getStringParam(req.Params.Arguments, "query", "") + limit := int(getFloatParam(req.Params.Arguments, "limit", 10)) + offset := int(getFloatParam(req.Params.Arguments, "offset", 0)) + + // Search users with pagination + users, total, err := searchUsersInDB(query, limit, offset) + if err != nil { + return nil, fmt.Errorf("search failed: %w", err) + } + + return mcp.NewToolResultJSON(map[string]interface{}{ + "users": users, + "total": total, + "limit": limit, + "offset": offset, + "query": query, + }), nil +} + +func handleUserResource(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + userID := extractUserIDFromURI(req.Params.URI) + + user, err := getUserFromDB(userID) + if err != nil { + return nil, fmt.Errorf("user not found: %s", userID) + } + + return mcp.NewResourceResultJSON(user), nil +} + +// Additional helper functions for parameter handling +func getStringParam(args map[string]interface{}, key, defaultValue string) string { + if val, ok := args[key]; ok && val != nil { + if str, ok := val.(string); ok { + return str + } + } + return defaultValue +} + +func getFloatParam(args map[string]interface{}, key string, defaultValue float64) float64 { + if val, ok := args[key]; ok && val != nil { + if f, ok := val.(float64); ok { + return f + } + } + return defaultValue +} + +func searchUsersInDB(query string, limit, offset int) ([]*User, int, error) { + // Placeholder implementation + users := []*User{ + {ID: "1", Name: "John Doe", Email: "john@example.com", Age: 30}, + {ID: "2", Name: "Jane Smith", Email: "jane@example.com", Age: 25}, + } + return users, len(users), nil +} + +func extractUserIDFromURI(uri string) string { + // Extract user ID from URI like "users://123" + parts := strings.Split(uri, "://") + if len(parts) > 1 { + return parts[1] + } + return uri +} +``` + +### Advanced HTTP Configuration + +```go +func main() { + s := server.NewMCPServer("Advanced HTTP Server", "1.0.0", + server.WithAllCapabilities(), + server.WithRecovery(), + server.WithHooks(&server.Hooks{ + OnToolCall: logToolCall, + OnResourceRead: logResourceRead, + }), + ) + + // Configure HTTP-specific options + httpOptions := server.HTTPOptions{ + BasePath: "/api/v1/mcp", + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 60 * time.Second, + MaxBodySize: 10 * 1024 * 1024, // 10MB + EnableCORS: true, + AllowedOrigins: []string{"https://myapp.com", "http://localhost:3000"}, + AllowedMethods: []string{"GET", "POST", "OPTIONS"}, + AllowedHeaders: []string{"Content-Type", "Authorization"}, + EnableGzip: true, + TrustedProxies: []string{"10.0.0.0/8", "172.16.0.0/12"}, + } + + // Add middleware + addHTTPMiddleware(s) + + // Add comprehensive tools + addCRUDTools(s) + addBatchTools(s) + addAnalyticsTools(s) + + log.Println("Starting advanced HTTP server on :8080") + if err := server.ServeHTTPWithOptions(s, ":8080", httpOptions); err != nil { + log.Fatal(err) + } +} + +// Helper functions for the advanced example +func addCRUDTools(s *server.MCPServer) { + // Placeholder implementation - would add CRUD tools +} + +func addBatchTools(s *server.MCPServer) { + // Placeholder implementation - would add batch processing tools +} + +func addAnalyticsTools(s *server.MCPServer) { + // Placeholder implementation - would add analytics tools +} + +func logToolCall(sessionID, toolName string, duration time.Duration, err error) { + // Placeholder implementation + if err != nil { + log.Printf("Tool %s failed: %v", toolName, err) + } else { + log.Printf("Tool %s completed in %v", toolName, duration) + } +} + +func logResourceRead(sessionID, uri string, duration time.Duration, err error) { + // Placeholder implementation + if err != nil { + log.Printf("Resource read %s failed: %v", uri, err) + } else { + log.Printf("Resource read %s completed in %v", uri, duration) + } +} + +func addHTTPMiddleware(s *server.MCPServer) { + // Authentication middleware + s.AddToolMiddleware(func(next server.ToolHandler) server.ToolHandler { + return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Extract and validate auth token + token := extractAuthToken(ctx) + if token == "" { + return nil, fmt.Errorf("authentication required") + } + + user, err := validateToken(token) + if err != nil { + return nil, fmt.Errorf("invalid token: %w", err) + } + + // Add user to context + ctx = context.WithValue(ctx, "user", user) + return next(ctx, req) + } + }) + + // Rate limiting middleware + s.AddToolMiddleware(func(next server.ToolHandler) server.ToolHandler { + return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + clientIP := getClientIP(ctx) + if !rateLimiter.Allow(clientIP) { + return nil, fmt.Errorf("rate limit exceeded") + } + return next(ctx, req) + } + }) + + // Caching middleware + s.AddResourceMiddleware(func(next server.ResourceHandler) server.ResourceHandler { + return func(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + // Check cache first + if cached := getFromCache(req.Params.URI); cached != nil { + return cached, nil + } + + result, err := next(ctx, req) + if err == nil { + // Cache successful results + setCache(req.Params.URI, result, 5*time.Minute) + } + + return result, err + } + }) +} +``` + +## Endpoints + +### Standard MCP Endpoints + +When you start an HTTP MCP server, it automatically creates these endpoints: + +``` +POST /mcp/initialize - Initialize MCP session +POST /mcp/tools/list - List available tools +POST /mcp/tools/call - Call a tool +POST /mcp/resources/list - List available resources +POST /mcp/resources/read - Read a resource +POST /mcp/prompts/list - List available prompts +POST /mcp/prompts/get - Get a prompt +GET /mcp/health - Health check +GET /mcp/capabilities - Server capabilities +``` + +### Custom Endpoints + +Add custom HTTP endpoints alongside MCP: + +```go +func main() { + s := server.NewMCPServer("Custom HTTP Server", "1.0.0") + + // Create HTTP server with custom routes + mux := http.NewServeMux() + + // Add MCP endpoints + server.AddMCPRoutes(mux, s, "/mcp") + + // Add custom endpoints + mux.HandleFunc("/api/status", handleStatus) + mux.HandleFunc("/api/metrics", handleMetrics) + mux.HandleFunc("/api/users", handleUsersAPI) + mux.HandleFunc("/api/upload", handleFileUpload) + + // Add middleware + handler := addMiddleware(mux) + + log.Println("Starting custom HTTP server on :8080") + if err := http.ListenAndServe(":8080", handler); err != nil { + log.Fatal(err) + } +} + +func handleStatus(w http.ResponseWriter, r *http.Request) { + status := map[string]interface{}{ + "status": "healthy", + "timestamp": time.Now().Unix(), + "version": "1.0.0", + "uptime": time.Since(startTime).String(), + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(status) +} + +func handleMetrics(w http.ResponseWriter, r *http.Request) { + metrics := collectMetrics() + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(metrics) +} + +func handleUsersAPI(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + handleListUsers(w, r) + case "POST": + handleCreateUserAPI(w, r) + case "PUT": + handleUpdateUser(w, r) + case "DELETE": + handleDeleteUser(w, r) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} +``` + +### Request/Response Patterns + +#### Standard MCP Request + +```json +POST /mcp/tools/call +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "search_users", + "arguments": { + "query": "john", + "limit": 10, + "offset": 0 + } + } +} +``` + +#### Standard MCP Response + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "content": [ + { + "type": "text", + "text": "{\"users\":[...],\"total\":25,\"limit\":10,\"offset\":0}" + } + ] + } +} +``` + +#### Error Response + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32602, + "message": "Invalid params", + "data": { + "details": "user_id is required" + } + } +} +``` + +## Session Management + +### Stateful vs Stateless + +#### Stateless Design (Recommended) + +```go +// Each request is independent +func handleStatelessTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Extract all needed information from request + userID := extractUserFromToken(ctx) + params := req.Params.Arguments + + // Process without relying on server state + result, err := processRequest(userID, params) + if err != nil { + return nil, err + } + + return mcp.NewToolResultJSON(result), nil +} + +// Use external storage for persistence +func getUserPreferences(userID string) (map[string]interface{}, error) { + // Load from database, cache, etc. + return loadFromRedis(fmt.Sprintf("user:%s:prefs", userID)) +} +``` + +#### Stateful Design (When Needed) + +```go +type HTTPSessionManager struct { + sessions map[string]*HTTPSession + mutex sync.RWMutex + cleanup *time.Ticker +} + +type HTTPSession struct { + ID string + UserID string + CreatedAt time.Time + LastAccess time.Time + Data map[string]interface{} + ExpiresAt time.Time +} + +func NewHTTPSessionManager() *HTTPSessionManager { + sm := &HTTPSessionManager{ + sessions: make(map[string]*HTTPSession), + cleanup: time.NewTicker(1 * time.Minute), + } + + go sm.cleanupExpiredSessions() + return sm +} + +func (sm *HTTPSessionManager) CreateSession(userID string) *HTTPSession { + sm.mutex.Lock() + defer sm.mutex.Unlock() + + session := &HTTPSession{ + ID: generateSessionID(), + UserID: userID, + CreatedAt: time.Now(), + LastAccess: time.Now(), + Data: make(map[string]interface{}), + ExpiresAt: time.Now().Add(30 * time.Minute), + } + + sm.sessions[session.ID] = session + return session +} + +func (sm *HTTPSessionManager) GetSession(sessionID string) (*HTTPSession, bool) { + sm.mutex.RLock() + defer sm.mutex.RUnlock() + + session, exists := sm.sessions[sessionID] + if !exists || time.Now().After(session.ExpiresAt) { + return nil, false + } + + // Update last access + session.LastAccess = time.Now() + session.ExpiresAt = time.Now().Add(30 * time.Minute) + + return session, true +} + +func (sm *HTTPSessionManager) cleanupExpiredSessions() { + for range sm.cleanup.C { + sm.mutex.Lock() + now := time.Now() + + for id, session := range sm.sessions { + if now.After(session.ExpiresAt) { + delete(sm.sessions, id) + } + } + + sm.mutex.Unlock() + } +} +``` + +### Authentication and Authorization + +```go +type AuthMiddleware struct { + jwtSecret []byte + userStore UserStore +} + +func NewAuthMiddleware(secret []byte, store UserStore) *AuthMiddleware { + return &AuthMiddleware{ + jwtSecret: secret, + userStore: store, + } +} + +func (m *AuthMiddleware) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Extract token from Authorization header + authHeader := r.Header.Get("Authorization") + if !strings.HasPrefix(authHeader, "Bearer ") { + http.Error(w, "Missing or invalid authorization header", http.StatusUnauthorized) + return + } + + token := strings.TrimPrefix(authHeader, "Bearer ") + + // Validate JWT token + claims, err := m.validateJWT(token) + if err != nil { + http.Error(w, "Invalid token", http.StatusUnauthorized) + return + } + + // Load user information + user, err := m.userStore.GetUser(claims.UserID) + if err != nil { + http.Error(w, "User not found", http.StatusUnauthorized) + return + } + + // Add user to request context + ctx := context.WithValue(r.Context(), "user", user) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func (m *AuthMiddleware) validateJWT(tokenString string) (*Claims, error) { + // Note: This example uses a hypothetical JWT library + // In practice, you would use a real JWT library like github.com/golang-jwt/jwt + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return m.jwtSecret, nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + + return nil, fmt.Errorf("invalid token") +} + +type Claims struct { + UserID string `json:"user_id"` + Role string `json:"role"` + jwt.StandardClaims +} +``` + +## Client Integration + +### Go HTTP Client + +```go +package main + +import ( + "context" + "log" + + "github.com/mark3labs/mcp-go/client" +) + +func main() { + // Create HTTP client + c := client.NewHTTPClient("http://localhost:8080/mcp") + + // Add authentication + c.SetHeader("Authorization", "Bearer your-jwt-token") + + // Set custom headers + c.SetHeader("X-API-Version", "v1") + c.SetHeader("X-Client-ID", "my-app") + + ctx := context.Background() + + // Initialize connection + if err := c.Initialize(ctx); err != nil { + log.Fatal(err) + } + + // List tools + tools, err := c.ListTools(ctx) + if err != nil { + log.Fatal(err) + } + + log.Printf("Available tools: %d", len(tools.Tools)) + + // Call tool + result, err := c.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolRequestParams{ + Name: "search_users", + Arguments: map[string]interface{}{ + "query": "john", + "limit": 5, + }, + }, + }) + if err != nil { + log.Fatal(err) + } + + log.Printf("Tool result: %+v", result) +} +``` + +### JavaScript/Fetch Client + +```javascript +class MCPHTTPClient { + constructor(baseURL, options = {}) { + this.baseURL = baseURL; + this.headers = { + 'Content-Type': 'application/json', + ...options.headers + }; + this.requestId = 1; + } + + setAuthToken(token) { + this.headers['Authorization'] = `Bearer ${token}`; + } + + async sendRequest(method, params = {}) { + const id = this.requestId++; + const request = { + jsonrpc: '2.0', + id, + method, + params + }; + + const response = await fetch(`${this.baseURL}/${method.replace('/', '/')}`, { + method: 'POST', + headers: this.headers, + body: JSON.stringify(request) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const result = await response.json(); + + if (result.error) { + throw new Error(result.error.message); + } + + return result.result; + } + + async initialize() { + return this.sendRequest('initialize', { + protocolVersion: '2024-11-05', + capabilities: { tools: {} }, + clientInfo: { name: 'Web Client', version: '1.0.0' } + }); + } + + async listTools() { + return this.sendRequest('tools/list'); + } + + async callTool(name, arguments) { + return this.sendRequest('tools/call', { name, arguments }); + } + + async listResources() { + return this.sendRequest('resources/list'); + } + + async readResource(uri) { + return this.sendRequest('resources/read', { uri }); + } +} + +// Usage +const client = new MCPHTTPClient('http://localhost:8080/mcp'); +client.setAuthToken('your-jwt-token'); + +async function main() { + try { + await client.initialize(); + + const tools = await client.listTools(); + console.log('Available tools:', tools); + + const result = await client.callTool('search_users', { + query: 'john', + limit: 10 + }); + console.log('Search result:', result); + + } catch (error) { + console.error('Error:', error); + } +} + +main(); +``` + +### cURL Examples + +```bash +# Initialize session +curl -X POST http://localhost:8080/mcp/initialize \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-token" \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {"tools": {}}, + "clientInfo": {"name": "curl", "version": "1.0.0"} + } + }' + +# List tools +curl -X POST http://localhost:8080/mcp/tools/list \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-token" \ + -d '{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/list", + "params": {} + }' + +# Call tool +curl -X POST http://localhost:8080/mcp/tools/call \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-token" \ + -d '{ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "search_users", + "arguments": { + "query": "john", + "limit": 10 + } + } + }' + +# Read resource +curl -X POST http://localhost:8080/mcp/resources/read \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer your-token" \ + -d '{ + "jsonrpc": "2.0", + "id": 4, + "method": "resources/read", + "params": { + "uri": "users://123" + } + }' +``` + +## Performance and Scaling + +### Load Balancing + +```go +// Use with nginx or HAProxy +upstream mcp_servers { + server 127.0.0.1:8080; + server 127.0.0.1:8081; + server 127.0.0.1:8082; +} + +server { + listen 80; + server_name api.example.com; + + location /mcp/ { + proxy_pass http://mcp_servers; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +### Caching Strategies + +```go +type CacheMiddleware struct { + cache map[string]cacheEntry + mutex sync.RWMutex + ttl time.Duration +} + +type cacheEntry struct { + data *mcp.ReadResourceResult + timestamp time.Time + etag string +} + +func (m *CacheMiddleware) ResourceMiddleware(next server.ResourceHandler) server.ResourceHandler { + return func(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + // Check cache + m.mutex.RLock() + entry, exists := m.cache[req.Params.URI] + m.mutex.RUnlock() + + if exists && time.Since(entry.timestamp) < m.ttl { + // Add cache headers + if httpCtx := getHTTPContext(ctx); httpCtx != nil { + httpCtx.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", int(m.ttl.Seconds()))) + httpCtx.Header().Set("ETag", entry.etag) + } + return entry.data, nil + } + + // Fetch fresh data + result, err := next(ctx, req) + if err != nil { + return nil, err + } + + // Cache result + etag := generateETag(result) + m.mutex.Lock() + m.cache[req.Params.URI] = cacheEntry{ + data: result, + timestamp: time.Now(), + etag: etag, + } + m.mutex.Unlock() + + // Set cache headers + if httpCtx := getHTTPContext(ctx); httpCtx != nil { + httpCtx.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", int(m.ttl.Seconds()))) + httpCtx.Header().Set("ETag", etag) + } + + return result, nil + } +} +``` + +### Rate Limiting + +```go +type RateLimiter struct { + limiters map[string]*rate.Limiter + mutex sync.RWMutex + rate rate.Limit + burst int +} + +func NewRateLimiter(requestsPerSecond float64, burst int) *RateLimiter { + return &RateLimiter{ + limiters: make(map[string]*rate.Limiter), + rate: rate.Limit(requestsPerSecond), + burst: burst, + } +} + +func (rl *RateLimiter) getLimiter(clientIP string) *rate.Limiter { + rl.mutex.RLock() + limiter, exists := rl.limiters[clientIP] + rl.mutex.RUnlock() + + if !exists { + rl.mutex.Lock() + limiter = rate.NewLimiter(rl.rate, rl.burst) + rl.limiters[clientIP] = limiter + rl.mutex.Unlock() + } + + return limiter +} + +func (rl *RateLimiter) Allow(clientIP string) bool { + return rl.getLimiter(clientIP).Allow() +} + +// HTTP middleware +func (rl *RateLimiter) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + clientIP := getClientIP(r) + + if !rl.Allow(clientIP) { + http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests) + return + } + + next.ServeHTTP(w, r) + }) +} +``` + +## Next Steps + +- **[In-Process Transport](/transports/inprocess)** - Learn about embedded scenarios +- **[Client Development](/clients)** - Build MCP clients for HTTP transport +- **[Testing](/testing)** - Test HTTP MCP servers comprehensively \ No newline at end of file diff --git a/www/docs/pages/transports/index.mdx b/www/docs/pages/transports/index.mdx new file mode 100644 index 000000000..bdcd54e1f --- /dev/null +++ b/www/docs/pages/transports/index.mdx @@ -0,0 +1,264 @@ +# Transport Options + +MCP-Go supports multiple transport methods to fit different deployment scenarios and integration patterns. Choose the right transport based on your use case, performance requirements, and client capabilities. + +## Overview + +Transport layers handle the communication between MCP clients and servers. Each transport has different characteristics and is optimized for specific scenarios: + +- **[STDIO](/transports/stdio)** - Standard input/output for command-line tools +- **[SSE](/transports/sse)** - Server-Sent Events for web applications +- **[HTTP](/transports/http)** - Traditional HTTP for REST-like interactions +- **[In-Process](/transports/inprocess)** - Direct integration for embedded scenarios + +## Transport Comparison + +| Transport | Use Case | Pros | Cons | +|-----------|----------|------|------| +| **STDIO** | CLI tools, desktop apps | Simple, secure, no network | Single client, local only | +| **SSE** | Web apps, real-time | Multi-client, real-time, web-friendly | HTTP overhead, one-way streaming | +| **HTTP** | Web services, APIs | Standard protocol, caching, load balancing | No real-time, more complex | +| **In-Process** | Embedded, testing | No serialization, fastest | Same process only | + +## Quick Example + +The same server code works with any transport: + +```go +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + "github.com/mark3labs/mcp-go/client" +) + +func main() { + // Create server (transport-agnostic) + s := server.NewMCPServer("Multi-Transport Server", "1.0.0", + server.WithToolCapabilities(true), + ) + + // Add a simple tool + s.AddTool( + mcp.NewTool("echo", + mcp.WithDescription("Echo back the input"), + mcp.WithString("message", mcp.Required()), + ), + handleEcho, + ) + + // Choose transport based on environment + transport := os.Getenv("MCP_TRANSPORT") + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + switch transport { + case "sse": + fmt.Printf("Starting SSE server on port %s\n", port) + server.ServeSSE(s, ":"+port) + case "http": + fmt.Printf("Starting HTTP server on port %s\n", port) + server.ServeHTTP(s, ":"+port) + default: + fmt.Println("Starting STDIO server") + server.ServeStdio(s) + } +} + +func handleEcho(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + message, err := req.RequireString("message") + if err != nil { + return nil, err + } + return mcp.NewToolResultText(fmt.Sprintf("Echo: %s", message)), nil +} +``` + +## Choosing the Right Transport + +### STDIO Transport +**Best for:** +- Command-line tools and utilities +- Desktop application integrations +- Local development and testing +- Single-user scenarios + +**Example use cases:** +- File system tools for IDEs +- Local database utilities +- Development workflow automation +- System administration tools + +### SSE Transport +**Best for:** +- Web applications requiring real-time updates +- Browser-based LLM interfaces +- Multi-user collaborative tools +- Dashboard and monitoring applications + +**Example use cases:** +- Web-based chat interfaces +- Real-time data visualization +- Collaborative document editing +- Live system monitoring + +### HTTP Transport +**Best for:** +- Traditional web services +- REST API integrations +- Load-balanced deployments +- Stateless interactions + +**Example use cases:** +- Microservice architectures +- Public API endpoints +- Integration with existing HTTP infrastructure +- Cached or rate-limited services + +### In-Process Transport +**Best for:** +- Embedded MCP servers +- Testing and development +- High-performance scenarios +- Library integrations + +**Example use cases:** +- Testing MCP implementations +- Embedded analytics engines +- High-frequency trading systems +- Real-time game servers + +## Transport Configuration + +### Environment-Based Selection + +```go +func startServer(s *server.MCPServer) error { + switch os.Getenv("MCP_TRANSPORT") { + case "sse": + return server.ServeSSE(s, getPort()) + case "http": + return server.ServeHTTP(s, getPort()) + case "inprocess": + // Note: In-process transport doesn't use network ports + // This would typically be used differently in practice + client := client.NewInProcessClient(s) + defer client.Close() + // Keep the process running + select {} + default: + return server.ServeStdio(s) + } +} + +func getPort() string { + if port := os.Getenv("PORT"); port != "" { + return ":" + port + } + return ":8080" +} +``` + +### Multi-Transport Server + +```go +func main() { + s := server.NewMCPServer("Multi-Transport", "1.0.0") + + // Add your tools, resources, prompts... + setupServer(s) + + // Start multiple transports concurrently with proper error handling + errChan := make(chan error, 3) + + go func() { + log.Println("Starting STDIO server...") + if err := server.ServeStdio(s); err != nil { + log.Printf("STDIO server error: %v", err) + errChan <- fmt.Errorf("STDIO server failed: %w", err) + } + }() + + go func() { + log.Println("Starting SSE server on :8080...") + if err := server.ServeSSE(s, ":8080"); err != nil { + log.Printf("SSE server error: %v", err) + errChan <- fmt.Errorf("SSE server failed: %w", err) + } + }() + + log.Println("Starting HTTP server on :8081...") + if err := server.ServeHTTP(s, ":8081"); err != nil { + log.Printf("HTTP server error: %v", err) + errChan <- fmt.Errorf("HTTP server failed: %w", err) + } + + // Wait for any server to fail + select { + case err := <-errChan: + log.Printf("Server failed: %v", err) + return + } +} + +// Helper function for the multi-transport example +func setupServer(s *server.MCPServer) { + // Placeholder implementation - would add tools, resources, etc. +} +``` + +## Performance Considerations + +### Latency Comparison +- **In-Process**: ~1μs (no serialization) +- **STDIO**: ~100μs (local pipes) +- **HTTP/SSE**: ~1-10ms (network + HTTP overhead) + +### Throughput Comparison +- **In-Process**: Limited by CPU/memory +- **STDIO**: Limited by pipe buffers (~64KB) +- **HTTP/SSE**: Limited by network bandwidth + +### Memory Usage +- **In-Process**: Shared memory space +- **STDIO**: Minimal overhead +- **HTTP/SSE**: Connection pooling, request buffering + +## Security Considerations + +### STDIO Transport +- **Pros**: No network exposure, process isolation +- **Cons**: Inherits parent process permissions +- **Best practices**: Validate all inputs, use least privilege + +### Network Transports (SSE/HTTP) +- **Authentication**: Implement proper auth middleware +- **Authorization**: Validate permissions per request +- **Rate limiting**: Prevent abuse and DoS +- **HTTPS**: Always use TLS in production + +```go +// Example with security middleware +s := server.NewMCPServer("Secure Server", "1.0.0", + server.WithToolMiddleware(authMiddleware), + server.WithToolMiddleware(rateLimitMiddleware), + server.WithRecovery(), +) +``` + +## Next Steps + +Explore each transport in detail: + +- **[STDIO Transport](/transports/stdio)** - Command-line integration +- **[SSE Transport](/transports/sse)** - Real-time web applications +- **[HTTP Transport](/transports/http)** - Traditional web services +- **[In-Process Transport](/transports/inprocess)** - Embedded scenarios \ No newline at end of file diff --git a/www/docs/pages/transports/inprocess.mdx b/www/docs/pages/transports/inprocess.mdx new file mode 100644 index 000000000..64ce0bf3d --- /dev/null +++ b/www/docs/pages/transports/inprocess.mdx @@ -0,0 +1,801 @@ +# In-Process Transport + +In-process transport enables direct integration of MCP servers within the same process, eliminating serialization overhead and providing the highest performance option for embedded scenarios. + +## Use Cases + +In-process transport is perfect for: + +- **Embedded servers**: MCP functionality within existing applications +- **Testing and development**: Fast, reliable testing without network overhead +- **High-performance scenarios**: Microsecond latency requirements +- **Library integrations**: MCP as a library component +- **Single-process architectures**: Monolithic applications with MCP capabilities + +**Example applications:** +- Game engines with AI tool integration +- Real-time trading systems +- Embedded analytics engines +- Testing frameworks +- Desktop applications with plugin architectures +- High-frequency data processing + +## Implementation + +### Basic In-Process Server + +```go +package main + +import ( + "context" + "fmt" + "log" + "runtime" + "strings" + "time" + "sync" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" + "github.com/mark3labs/mcp-go/client" +) + +func main() { + // Create server + s := server.NewMCPServer("In-Process Server", "1.0.0", + server.WithToolCapabilities(true), + server.WithResourceCapabilities(true), + ) + + // Add high-performance tools + s.AddTool( + mcp.NewTool("fast_calculation", + mcp.WithDescription("Perform fast mathematical calculations"), + mcp.WithString("operation", mcp.Required()), + mcp.WithArray("numbers", mcp.Required()), + ), + handleFastCalculation, + ) + + s.AddTool( + mcp.NewTool("process_data", + mcp.WithDescription("Process data with zero-copy operations"), + mcp.WithObject("data", mcp.Required()), + mcp.WithString("algorithm", mcp.Default("default")), + ), + handleProcessData, + ) + + // Add in-memory resources + s.AddResource( + mcp.NewResource( + "memory://cache/{key}", + "Memory Cache", + mcp.WithResourceDescription("In-memory cached data"), + mcp.WithMIMEType("application/json"), + ), + handleMemoryCache, + ) + + // Create in-process client + client := client.NewInProcessClient(s) + defer client.Close() + + ctx := context.Background() + + // Initialize (no network overhead) + if err := client.Initialize(ctx); err != nil { + log.Fatal(err) + } + + // Demonstrate high-performance operations + demonstratePerformance(ctx, client) +} + +func handleFastCalculation(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + operation := req.Params.Arguments["operation"].(string) + numbers := req.Params.Arguments["numbers"].([]interface{}) + + // Convert to float64 slice + values := make([]float64, len(numbers)) + for i, n := range numbers { + values[i] = n.(float64) + } + + // Perform calculation + var result float64 + switch operation { + case "sum": + for _, v := range values { + result += v + } + case "product": + result = 1 + for _, v := range values { + result *= v + } + case "average": + sum := 0.0 + for _, v := range values { + sum += v + } + result = sum / float64(len(values)) + default: + return nil, fmt.Errorf("unknown operation: %s", operation) + } + + return mcp.NewToolResultJSON(map[string]interface{}{ + "operation": operation, + "result": result, + "count": len(values), + }), nil +} + +func handleProcessData(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + data := req.Params.Arguments["data"].(map[string]interface{}) + algorithm := req.Params.Arguments["algorithm"].(string) + + // Process data in-place (zero-copy when possible) + processed := processDataInPlace(data, algorithm) + + return mcp.NewToolResultJSON(map[string]interface{}{ + "algorithm": algorithm, + "processed": processed, + "timestamp": time.Now().UnixNano(), + }), nil +} + +var ( + memoryCache = make(map[string]interface{}) + cacheMutex sync.RWMutex +) + +func handleMemoryCache(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + key := extractKeyFromURI(req.Params.URI) + + cacheMutex.RLock() + value, exists := memoryCache[key] + cacheMutex.RUnlock() + + if !exists { + return nil, fmt.Errorf("key not found: %s", key) + } + + return mcp.NewResourceResultJSON(map[string]interface{}{ + "key": key, + "value": value, + "type": fmt.Sprintf("%T", value), + }), nil +} + +// Helper functions for the examples +func processDataInPlace(data map[string]interface{}, algorithm string) map[string]interface{} { + // Placeholder implementation - would process data according to algorithm + result := make(map[string]interface{}) + for k, v := range data { + result[k] = fmt.Sprintf("processed_%v", v) + } + return result +} + +func extractKeyFromURI(uri string) string { + // Extract key from URI like "memory://cache/mykey" + parts := strings.Split(uri, "/") + if len(parts) > 0 { + return parts[len(parts)-1] + } + return uri +} + +func demonstratePerformance(ctx context.Context, client *client.InProcessClient) { + // Benchmark tool calls + start := time.Now() + iterations := 10000 + + for i := 0; i < iterations; i++ { + _, err := client.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolRequestParams{ + Name: "fast_calculation", + Arguments: map[string]interface{}{ + "operation": "sum", + "numbers": []interface{}{1.0, 2.0, 3.0, 4.0, 5.0}, + }, + }, + }) + if err != nil { + log.Printf("Tool call failed: %v", err) + break + } + } + + duration := time.Since(start) + avgLatency := duration / time.Duration(iterations) + + log.Printf("Completed %d tool calls in %v", iterations, duration) + log.Printf("Average latency: %v", avgLatency) + log.Printf("Throughput: %.0f calls/second", float64(iterations)/duration.Seconds()) +} +``` + +### Advanced In-Process Integration + +```go +// Embedded MCP server in a larger application +type Application struct { + mcpServer *server.MCPServer + mcpClient *client.InProcessClient + dataStore *DataStore + config *Config + startTime time.Time +} + +func NewApplication(config *Config) *Application { + app := &Application{ + config: config, + dataStore: NewDataStore(), + startTime: time.Now(), + } + + // Create embedded MCP server + app.mcpServer = server.NewMCPServer("Embedded Server", "1.0.0", + server.WithAllCapabilities(), + server.WithRecovery(), + ) + + // Add application-specific tools + app.addApplicationTools() + app.addDataTools() + app.addAnalyticsTools() + + // Create in-process client for internal use + app.mcpClient = client.NewInProcessClient(app.mcpServer) + + return app +} + +// Helper types and functions for the application example +type DataStore struct { + // Placeholder implementation +} + +type Config struct { + // Placeholder implementation +} + +func NewDataStore() *DataStore { + return &DataStore{} +} + +func (ds *DataStore) Query(query string, params map[string]interface{}) (interface{}, error) { + // Placeholder implementation + return map[string]interface{}{"result": "data"}, nil +} + +func (c *Config) Public() map[string]interface{} { + // Placeholder implementation + return map[string]interface{}{"debug": true} +} + +func getMemoryUsage() map[string]interface{} { + var m runtime.MemStats + runtime.ReadMemStats(&m) + return map[string]interface{}{ + "alloc": m.Alloc, + "total_alloc": m.TotalAlloc, + "sys": m.Sys, + } +} + +func (app *Application) addDataTools() { + // Placeholder implementation +} + +func (app *Application) addAnalyticsTools() { + // Placeholder implementation +} + +func (app *Application) addApplicationTools() { + // Direct access to application state + app.mcpServer.AddTool( + mcp.NewTool("get_app_status", + mcp.WithDescription("Get current application status"), + ), + func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + status := map[string]interface{}{ + "uptime": time.Since(app.startTime).String(), + "memory": getMemoryUsage(), + "goroutines": runtime.NumGoroutine(), + "config": app.config.Public(), + } + return mcp.NewToolResultJSON(status), nil + }, + ) + + // Direct data store access + app.mcpServer.AddTool( + mcp.NewTool("query_data", + mcp.WithDescription("Query application data store"), + mcp.WithString("query", mcp.Required()), + mcp.WithObject("params", mcp.Description("Query parameters")), + ), + func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + query := req.Params.Arguments["query"].(string) + params := req.Params.Arguments["params"].(map[string]interface{}) + + // Direct access to data store (no serialization) + results, err := app.dataStore.Query(query, params) + if err != nil { + return nil, err + } + + return mcp.NewToolResultJSON(results), nil + }, + ) +} + +func (app *Application) ProcessWithMCP(ctx context.Context, input interface{}) (interface{}, error) { + // Use MCP tools internally for processing + result, err := app.mcpClient.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolRequestParams{ + Name: "process_data", + Arguments: map[string]interface{}{ + "data": input, + "algorithm": "optimized", + }, + }, + }) + if err != nil { + return nil, err + } + + // Extract result (no JSON parsing overhead) + return result.Content[0].Text, nil +} +``` + +### Zero-Copy Operations + +```go +// Shared data structures for zero-copy operations +type SharedBuffer struct { + data []byte + offset int + length int + mutex sync.RWMutex +} + +func NewSharedBuffer(size int) *SharedBuffer { + return &SharedBuffer{ + data: make([]byte, size), + } +} + +func (sb *SharedBuffer) Write(data []byte) error { + sb.mutex.Lock() + defer sb.mutex.Unlock() + + if len(data) > len(sb.data)-sb.offset { + return fmt.Errorf("buffer overflow") + } + + copy(sb.data[sb.offset:], data) + sb.length = len(data) + return nil +} + +func (sb *SharedBuffer) Read() []byte { + sb.mutex.RLock() + defer sb.mutex.RUnlock() + + // Return slice of underlying array (zero-copy) + return sb.data[sb.offset : sb.offset+sb.length] +} + +// Tool that operates on shared buffers +func handleZeroCopyOperation(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Get shared buffer from context (passed by reference) + buffer := getSharedBuffer(ctx) + if buffer == nil { + return nil, fmt.Errorf("no shared buffer available") + } + + // Operate directly on buffer data + data := buffer.Read() + processed := processInPlace(data) // Modifies data in-place + + return mcp.NewToolResultText(fmt.Sprintf("Processed %d bytes", len(processed))), nil +} + +// Context key for shared buffer +type sharedBufferKey struct{} + +// Helper functions for zero-copy operations +func getSharedBuffer(ctx context.Context) *SharedBuffer { + if buffer, ok := ctx.Value(sharedBufferKey{}).(*SharedBuffer); ok { + return buffer + } + return nil +} + +// Helper function to add shared buffer to context +func withSharedBuffer(ctx context.Context, buffer *SharedBuffer) context.Context { + return context.WithValue(ctx, sharedBufferKey{}, buffer) +} + +func processInPlace(data []byte) []byte { + // Placeholder implementation - would process data in-place + return data +} + +// Example usage of shared buffer with context +func exampleZeroCopyUsage() { + // Create a shared buffer + buffer := NewSharedBuffer(1024) + buffer.Write([]byte("example data")) + + // Create context with shared buffer + ctx := withSharedBuffer(context.Background(), buffer) + + // Call tool with shared buffer context + req := mcp.CallToolRequest{Name: "zero-copy-op"} + result, err := handleZeroCopyOperation(ctx, req) + if err != nil { + log.Printf("Error: %v", err) + return + } + + log.Printf("Result: %s", result.Content[0].Text) +} +``` + +## Direct Server Integration + +### Library-Style Integration + +```go +// MCP as a library component +type MCPEngine struct { + server *server.MCPServer + client *client.InProcessClient +} + +func NewMCPEngine() *MCPEngine { + s := server.NewMCPServer("Engine", "1.0.0") + + engine := &MCPEngine{ + server: s, + client: client.NewInProcessClient(s), + } + + engine.registerBuiltinTools() + return engine +} + +func (e *MCPEngine) RegisterTool(name, description string, handler server.ToolHandler) { + tool := mcp.NewTool(name, mcp.WithDescription(description)) + e.server.AddTool(tool, handler) +} + +func (e *MCPEngine) Execute(ctx context.Context, toolName string, args map[string]interface{}) (interface{}, error) { + result, err := e.client.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolRequestParams{ + Name: toolName, + Arguments: args, + }, + }) + if err != nil { + return nil, err + } + + // Return native Go types (no JSON overhead) + return parseResult(result), nil +} + +func (e *MCPEngine) registerBuiltinTools() { + // Math operations + e.RegisterTool("add", "Add two numbers", func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + a := req.Params.Arguments["a"].(float64) + b := req.Params.Arguments["b"].(float64) + return mcp.NewToolResultJSON(a + b), nil + }) + + // String operations + e.RegisterTool("concat", "Concatenate strings", func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + parts := req.Params.Arguments["parts"].([]interface{}) + var result strings.Builder + for _, part := range parts { + result.WriteString(fmt.Sprintf("%v", part)) + } + return mcp.NewToolResultText(result.String()), nil + }) +} + +// Usage in application +func main() { + engine := NewMCPEngine() + + // Register custom tools + engine.RegisterTool("custom_logic", "Custom business logic", handleCustomLogic) + + ctx := context.Background() + + // Execute tools directly + result, err := engine.Execute(ctx, "add", map[string]interface{}{ + "a": 10.0, + "b": 20.0, + }) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Result: %v\n", result) +} +``` + +### Plugin Architecture + +```go +// Plugin interface +type Plugin interface { + Name() string + Version() string + RegisterTools(server *server.MCPServer) error + RegisterResources(server *server.MCPServer) error +} + +// Plugin manager with in-process MCP +type PluginManager struct { + mcpServer *server.MCPServer + mcpClient *client.InProcessClient + plugins map[string]Plugin +} + +func NewPluginManager() *PluginManager { + s := server.NewMCPServer("Plugin Manager", "1.0.0", + server.WithAllCapabilities(), + ) + + return &PluginManager{ + mcpServer: s, + mcpClient: client.NewInProcessClient(s), + plugins: make(map[string]Plugin), + } +} + +func (pm *PluginManager) LoadPlugin(plugin Plugin) error { + name := plugin.Name() + + if _, exists := pm.plugins[name]; exists { + return fmt.Errorf("plugin %s already loaded", name) + } + + // Register plugin tools and resources + if err := plugin.RegisterTools(pm.mcpServer); err != nil { + return fmt.Errorf("failed to register tools for plugin %s: %w", name, err) + } + + if err := plugin.RegisterResources(pm.mcpServer); err != nil { + return fmt.Errorf("failed to register resources for plugin %s: %w", name, err) + } + + pm.plugins[name] = plugin + return nil +} + +func (pm *PluginManager) ExecutePluginTool(ctx context.Context, toolName string, args map[string]interface{}) (interface{}, error) { + return pm.mcpClient.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolRequestParams{ + Name: toolName, + Arguments: args, + }, + }) +} + +// Example plugin +type MathPlugin struct{} + +func (p *MathPlugin) Name() string { return "math" } +func (p *MathPlugin) Version() string { return "1.0.0" } + +func (p *MathPlugin) RegisterTools(s *server.MCPServer) error { + s.AddTool( + mcp.NewTool("math.factorial", + mcp.WithDescription("Calculate factorial"), + mcp.WithInteger("n", mcp.Required(), mcp.Minimum(0)), + ), + p.handleFactorial, + ) + + s.AddTool( + mcp.NewTool("math.fibonacci", + mcp.WithDescription("Calculate Fibonacci number"), + mcp.WithInteger("n", mcp.Required(), mcp.Minimum(0)), + ), + p.handleFibonacci, + ) + + return nil +} + +func (p *MathPlugin) RegisterResources(s *server.MCPServer) error { + s.AddResource( + mcp.NewResource( + "math://constants/{name}", + "Mathematical Constants", + mcp.WithResourceDescription("Common mathematical constants"), + mcp.WithMIMEType("application/json"), + ), + p.handleConstants, + ) + + return nil +} + +func (p *MathPlugin) handleFactorial(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + n := int(req.Params.Arguments["n"].(float64)) + + result := 1 + for i := 2; i <= n; i++ { + result *= i + } + + return mcp.NewToolResultJSON(map[string]interface{}{ + "input": n, + "result": result, + }), nil +} + +func (p *MathPlugin) handleFibonacci(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + n := int(req.Params.Arguments["n"].(float64)) + + if n <= 1 { + return mcp.NewToolResultJSON(n), nil + } + + a, b := 0, 1 + for i := 2; i <= n; i++ { + a, b = b, a+b + } + + return mcp.NewToolResultJSON(b), nil +} + +func (p *MathPlugin) handleConstants(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + name := extractNameFromURI(req.Params.URI) + + constants := map[string]float64{ + "pi": 3.141592653589793, + "e": 2.718281828459045, + "phi": 1.618033988749895, + } + + value, exists := constants[name] + if !exists { + return nil, fmt.Errorf("unknown constant: %s", name) + } + + return mcp.NewResourceResultJSON(map[string]interface{}{ + "name": name, + "value": value, + }), nil +} +``` + +## Performance Benefits + +### Latency Comparison + +```go +func benchmarkTransports() { + // In-process + inProcessServer := server.NewMCPServer("Benchmark", "1.0.0") + inProcessClient := client.NewInProcessClient(inProcessServer) + + // STDIO (for comparison) + stdioClient, _ := client.NewStdioClient("go", "run", "server.go") + + // HTTP (for comparison) + httpClient := client.NewHTTPClient("http://localhost:8080/mcp") + + ctx := context.Background() + iterations := 10000 + + // Benchmark in-process + start := time.Now() + for i := 0; i < iterations; i++ { + inProcessClient.CallTool(ctx, simpleToolRequest) + } + inProcessDuration := time.Since(start) + + // Benchmark STDIO + start = time.Now() + for i := 0; i < iterations; i++ { + stdioClient.CallTool(ctx, simpleToolRequest) + } + stdioDuration := time.Since(start) + + // Benchmark HTTP + start = time.Now() + for i := 0; i < iterations; i++ { + httpClient.CallTool(ctx, simpleToolRequest) + } + httpDuration := time.Since(start) + + fmt.Printf("In-Process: %v (%.2f μs/call)\n", + inProcessDuration, + float64(inProcessDuration.Nanoseconds())/float64(iterations)/1000) + + fmt.Printf("STDIO: %v (%.2f μs/call)\n", + stdioDuration, + float64(stdioDuration.Nanoseconds())/float64(iterations)/1000) + + fmt.Printf("HTTP: %v (%.2f μs/call)\n", + httpDuration, + float64(httpDuration.Nanoseconds())/float64(iterations)/1000) +} +``` + +### Memory Efficiency + +```go +// Shared data structures reduce memory allocation +type SharedDataPool struct { + buffers chan []byte + size int +} + +func NewSharedDataPool(poolSize, bufferSize int) *SharedDataPool { + pool := &SharedDataPool{ + buffers: make(chan []byte, poolSize), + size: bufferSize, + } + + // Pre-allocate buffers + for i := 0; i < poolSize; i++ { + pool.buffers <- make([]byte, bufferSize) + } + + return pool +} + +func (p *SharedDataPool) Get() []byte { + select { + case buffer := <-p.buffers: + return buffer[:0] // Reset length but keep capacity + default: + return make([]byte, 0, p.size) // Fallback allocation + } +} + +func (p *SharedDataPool) Put(buffer []byte) { + if cap(buffer) != p.size { + return // Don't pool buffers of wrong size + } + + select { + case p.buffers <- buffer: + default: + // Pool full, let GC handle it + } +} + +// Use shared pool in tools +var dataPool = NewSharedDataPool(100, 4096) + +func handlePooledOperation(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + buffer := dataPool.Get() + defer dataPool.Put(buffer) + + // Use buffer for processing + result := processWithBuffer(buffer, req.Params.Arguments) + + return mcp.NewToolResultJSON(result), nil +} +``` + +## Next Steps + +- **[Client Development](/clients)** - Build MCP clients for all transports +- **[Testing](/testing)** - Comprehensive testing strategies +- **[Performance](/performance)** - Optimization techniques and benchmarking \ No newline at end of file diff --git a/www/docs/pages/transports/sse.mdx b/www/docs/pages/transports/sse.mdx new file mode 100644 index 000000000..3e4b82e46 --- /dev/null +++ b/www/docs/pages/transports/sse.mdx @@ -0,0 +1,871 @@ +# SSE Transport + +Server-Sent Events (SSE) transport enables real-time, web-friendly communication between MCP clients and servers. Perfect for web applications that need live updates and multi-client support. + +## Use Cases + +SSE transport is ideal for: + +- **Web applications**: Browser-based LLM interfaces +- **Real-time dashboards**: Live data monitoring and visualization +- **Collaborative tools**: Multi-user environments with shared state +- **Streaming responses**: Long-running operations with progress updates +- **Event-driven systems**: Applications that need server-initiated communication + +**Example applications:** +- Web-based chat interfaces with LLMs +- Real-time analytics dashboards +- Collaborative document editing +- Live system monitoring tools +- Streaming data processing interfaces + +## Implementation + +### Basic SSE Server + +```go +package main + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func main() { + s := server.NewMCPServer("SSE Server", "1.0.0", + server.WithToolCapabilities(true), + server.WithResourceCapabilities(true), + ) + + // Add real-time tools + s.AddTool( + mcp.NewTool("stream_data", + mcp.WithDescription("Stream data with real-time updates"), + mcp.WithString("source", mcp.Required()), + mcp.WithInteger("count", mcp.Default(10)), + ), + handleStreamData, + ) + + s.AddTool( + mcp.NewTool("monitor_system", + mcp.WithDescription("Monitor system metrics in real-time"), + mcp.WithInteger("duration", mcp.Default(60)), + ), + handleSystemMonitor, + ) + + // Add dynamic resources + s.AddResource( + mcp.NewResource( + "metrics://current", + "Current System Metrics", + mcp.WithResourceDescription("Real-time system metrics"), + mcp.WithMIMEType("application/json"), + ), + handleCurrentMetrics, + ) + + // Start SSE server + log.Println("Starting SSE server on :8080") + if err := server.ServeSSE(s, ":8080"); err != nil { + log.Fatal(err) + } +} + +func handleStreamData(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + source := req.Params.Arguments["source"].(string) + count := int(req.Params.Arguments["count"].(float64)) + + // Get notifier for real-time updates (hypothetical functions) + // Note: These functions would be provided by the SSE transport implementation + notifier := getNotifierFromContext(ctx) // Hypothetical function + sessionID := getSessionIDFromContext(ctx) // Hypothetical function + + // Stream data with progress updates + var results []map[string]interface{} + for i := 0; i < count; i++ { + // Check for cancellation + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + // Simulate data processing + data := generateData(source, i) + results = append(results, data) + + // Send progress notification + if notifier != nil { + // Note: ProgressNotification would be defined by the MCP protocol + notifier.SendProgress(sessionID, map[string]interface{}{ + "progress": i + 1, + "total": count, + "message": fmt.Sprintf("Processed %d/%d items from %s", i+1, count, source), + }) + } + + time.Sleep(100 * time.Millisecond) + } + + return mcp.NewToolResultJSON(map[string]interface{}{ + "source": source, + "results": results, + "count": len(results), + }), nil +} + +// Helper functions for the examples +func generateData(source string, index int) map[string]interface{} { + return map[string]interface{}{ + "source": source, + "index": index, + "value": fmt.Sprintf("data_%d", index), + } +} + +func getNotifierFromContext(ctx context.Context) interface{} { + // Placeholder implementation - would be provided by SSE transport + return nil +} + +func getSessionIDFromContext(ctx context.Context) string { + // Placeholder implementation - would be provided by SSE transport + return "session_123" +} + +func handleSystemMonitor(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + duration := int(req.Params.Arguments["duration"].(float64)) + + notifier := getNotifierFromContext(ctx) + sessionID := getSessionIDFromContext(ctx) + + // Monitor system for specified duration + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + timeout := time.After(time.Duration(duration) * time.Second) + var metrics []map[string]interface{} + + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-timeout: + return mcp.NewToolResultJSON(map[string]interface{}{ + "duration": duration, + "metrics": metrics, + "samples": len(metrics), + }), nil + case <-ticker.C: + // Collect current metrics + currentMetrics := collectSystemMetrics() + metrics = append(metrics, currentMetrics) + + // Send real-time update + if notifier != nil { + // Note: SendCustom would be a method on the notifier interface + // notifier.SendCustom(sessionID, "system_metrics", currentMetrics) + } + } + } +} + +func collectSystemMetrics() map[string]interface{} { + // Placeholder implementation + return map[string]interface{}{ + "cpu": 50.5, + "memory": 75.2, + "disk": 30.1, + } +} + +func handleCurrentMetrics(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + metrics := collectSystemMetrics() + return mcp.NewResourceResultJSON(metrics), nil +} +``` + +### Advanced SSE Configuration + +```go +func main() { + s := server.NewMCPServer("Advanced SSE Server", "1.0.0", + server.WithAllCapabilities(), + server.WithRecovery(), + server.WithHooks(&server.Hooks{ + OnSessionStart: func(sessionID string) { + log.Printf("SSE client connected: %s", sessionID) + broadcastUserCount() + }, + OnSessionEnd: func(sessionID string) { + log.Printf("SSE client disconnected: %s", sessionID) + broadcastUserCount() + }, + }), + ) + + // Configure SSE-specific options + sseOptions := server.SSEOptions{ + BasePath: "/mcp", + AllowedOrigins: []string{"http://localhost:3000", "https://myapp.com"}, + HeartbeatInterval: 30 * time.Second, + MaxConnections: 100, + ConnectionTimeout: 5 * time.Minute, + EnableCompression: true, + } + + // Add collaborative tools + addCollaborativeTools(s) + addRealTimeResources(s) + + log.Println("Starting advanced SSE server on :8080") + if err := server.ServeSSEWithOptions(s, ":8080", sseOptions); err != nil { + log.Fatal(err) + } +} + +// Helper functions for the advanced example +func broadcastUserCount() { + // Placeholder implementation + log.Println("Broadcasting user count update") +} + +func addCollaborativeToolsPlaceholder(s *server.MCPServer) { + // Placeholder implementation - would add collaborative tools +} + +func addRealTimeResources(s *server.MCPServer) { + // Placeholder implementation - would add real-time resources +} + +func addCollaborativeTools(s *server.MCPServer) { + // Shared document editing + s.AddTool( + mcp.NewTool("edit_document", + mcp.WithDescription("Edit a shared document"), + mcp.WithString("doc_id", mcp.Required()), + mcp.WithString("operation", mcp.Required()), + mcp.WithObject("data", mcp.Required()), + ), + handleDocumentEdit, + ) + + // Real-time chat + s.AddTool( + mcp.NewTool("send_message", + mcp.WithDescription("Send a message to all connected clients"), + mcp.WithString("message", mcp.Required()), + mcp.WithString("channel", mcp.Default("general")), + ), + handleSendMessage, + ) + + // Live data updates + s.AddTool( + mcp.NewTool("subscribe_updates", + mcp.WithDescription("Subscribe to real-time data updates"), + mcp.WithString("topic", mcp.Required()), + mcp.WithArray("filters", mcp.Description("Optional filters")), + ), + handleSubscribeUpdates, + ) +} +``` + +## Configuration + +### Base URLs and Paths + +```go +// Custom SSE endpoint configuration +sseOptions := server.SSEOptions{ + BasePath: "/api/mcp", // SSE endpoint will be /api/mcp/sse + + // Additional HTTP endpoints + HealthPath: "/api/health", + MetricsPath: "/api/metrics", + StatusPath: "/api/status", +} + +// Start server with custom paths +server.ServeSSEWithOptions(s, ":8080", sseOptions) +``` + +**Resulting endpoints:** +- SSE stream: `http://localhost:8080/api/mcp/sse` +- Health check: `http://localhost:8080/api/health` +- Metrics: `http://localhost:8080/api/metrics` +- Status: `http://localhost:8080/api/status` + +### CORS Configuration + +```go +sseOptions := server.SSEOptions{ + // Allow specific origins + AllowedOrigins: []string{ + "http://localhost:3000", + "https://myapp.com", + "https://*.myapp.com", + }, + + // Allow all origins (development only) + AllowAllOrigins: true, + + // Custom CORS headers + AllowedHeaders: []string{ + "Authorization", + "Content-Type", + "X-API-Key", + }, + + // Allow credentials + AllowCredentials: true, +} +``` + +### Connection Management + +```go +sseOptions := server.SSEOptions{ + // Connection limits + MaxConnections: 100, + MaxConnectionsPerIP: 10, + + // Timeouts + ConnectionTimeout: 5 * time.Minute, + WriteTimeout: 30 * time.Second, + ReadTimeout: 30 * time.Second, + + // Heartbeat to keep connections alive + HeartbeatInterval: 30 * time.Second, + + // Buffer sizes + WriteBufferSize: 4096, + ReadBufferSize: 4096, + + // Compression + EnableCompression: true, + CompressionLevel: 6, +} +``` + +## Session Handling + +### Multi-Client State Management + +```go +type SessionManager struct { + sessions map[string]*ClientSession + mutex sync.RWMutex + notifier *SSENotifier +} + +type ClientSession struct { + ID string + UserID string + ConnectedAt time.Time + LastSeen time.Time + Subscriptions map[string]bool + Metadata map[string]interface{} +} + +func NewSessionManager() *SessionManager { + return &SessionManager{ + sessions: make(map[string]*ClientSession), + notifier: NewSSENotifier(), + } +} + +func (sm *SessionManager) OnSessionStart(sessionID string) { + sm.mutex.Lock() + defer sm.mutex.Unlock() + + session := &ClientSession{ + ID: sessionID, + ConnectedAt: time.Now(), + LastSeen: time.Now(), + Subscriptions: make(map[string]bool), + Metadata: make(map[string]interface{}), + } + + sm.sessions[sessionID] = session + + // Notify other clients + sm.notifier.BroadcastExcept(sessionID, "user_joined", map[string]interface{}{ + "session_id": sessionID, + "timestamp": time.Now().Unix(), + }) +} + +func (sm *SessionManager) OnSessionEnd(sessionID string) { + sm.mutex.Lock() + defer sm.mutex.Unlock() + + delete(sm.sessions, sessionID) + + // Notify other clients + sm.notifier.Broadcast("user_left", map[string]interface{}{ + "session_id": sessionID, + "timestamp": time.Now().Unix(), + }) +} + +func (sm *SessionManager) GetActiveSessions() []ClientSession { + sm.mutex.RLock() + defer sm.mutex.RUnlock() + + var sessions []ClientSession + for _, session := range sm.sessions { + sessions = append(sessions, *session) + } + + return sessions +} +``` + +### Real-Time Notifications + +```go +type SSENotifier struct { + clients map[string]chan mcp.Notification + mutex sync.RWMutex +} + +func NewSSENotifier() *SSENotifier { + return &SSENotifier{ + clients: make(map[string]chan mcp.Notification), + } +} + +func (n *SSENotifier) RegisterClient(sessionID string) <-chan mcp.Notification { + n.mutex.Lock() + defer n.mutex.Unlock() + + ch := make(chan mcp.Notification, 100) + n.clients[sessionID] = ch + return ch +} + +func (n *SSENotifier) UnregisterClient(sessionID string) { + n.mutex.Lock() + defer n.mutex.Unlock() + + if ch, exists := n.clients[sessionID]; exists { + close(ch) + delete(n.clients, sessionID) + } +} + +func (n *SSENotifier) SendToClient(sessionID string, notification mcp.Notification) { + n.mutex.RLock() + defer n.mutex.RUnlock() + + if ch, exists := n.clients[sessionID]; exists { + select { + case ch <- notification: + default: + // Channel full, drop notification + } + } +} + +func (n *SSENotifier) Broadcast(eventType string, data interface{}) { + notification := mcp.Notification{ + Type: eventType, + Data: data, + } + + n.mutex.RLock() + defer n.mutex.RUnlock() + + for _, ch := range n.clients { + select { + case ch <- notification: + default: + // Channel full, skip this client + } + } +} + +func (n *SSENotifier) BroadcastExcept(excludeSessionID, eventType string, data interface{}) { + notification := mcp.Notification{ + Type: eventType, + Data: data, + } + + n.mutex.RLock() + defer n.mutex.RUnlock() + + for sessionID, ch := range n.clients { + if sessionID == excludeSessionID { + continue + } + + select { + case ch <- notification: + default: + // Channel full, skip this client + } + } +} +``` + +## Client Integration + +### JavaScript/Browser Client + +```javascript +class MCPSSEClient { + constructor(baseURL) { + this.baseURL = baseURL; + this.eventSource = null; + this.requestId = 1; + this.pendingRequests = new Map(); + } + + async connect() { + // Initialize SSE connection + this.eventSource = new EventSource(`${this.baseURL}/sse`); + + this.eventSource.onopen = () => { + console.log('SSE connection opened'); + }; + + this.eventSource.onmessage = (event) => { + this.handleMessage(JSON.parse(event.data)); + }; + + this.eventSource.onerror = (error) => { + console.error('SSE error:', error); + }; + + // Send initialization request via separate HTTP endpoint + // Note: SSE is unidirectional - initialization would typically happen + // via a separate HTTP POST endpoint, not through the SSE stream + return this.sendInitRequest('initialize', { + protocolVersion: '2024-11-05', + capabilities: { tools: {} }, + clientInfo: { name: 'Web Client', version: '1.0.0' } + }); + } + + async sendInitRequest(method, params = {}) { + // Use a separate HTTP endpoint for initialization and commands + const response = await fetch(`${this.baseURL}/init`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: this.requestId++, + method, + params + }) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return response.json(); + } + + async sendRequest(method, params = {}) { + // For tool calls and other requests, use HTTP POST + // SSE is only for receiving server-initiated notifications + const id = this.requestId++; + const request = { + jsonrpc: '2.0', + id, + method, + params + }; + + const response = await fetch(`${this.baseURL}/request`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return response.json(); + } + + handleMessage(message) { + if (message.id && this.pendingRequests.has(message.id)) { + // Handle response to our request + const { resolve, reject } = this.pendingRequests.get(message.id); + this.pendingRequests.delete(message.id); + + if (message.error) { + reject(new Error(message.error.message)); + } else { + resolve(message.result); + } + } else if (message.method) { + // Handle server-initiated notification + this.handleNotification(message); + } + } + + handleNotification(notification) { + switch (notification.method) { + case 'notifications/progress': + this.onProgress(notification.params); + break; + case 'notifications/message': + this.onMessage(notification.params); + break; + default: + console.log('Unknown notification:', notification); + } + } + + onProgress(progress) { + console.log(`Progress: ${progress.progress}/${progress.total} - ${progress.message}`); + } + + onMessage(message) { + console.log('Server message:', message); + } + + async listTools() { + return this.sendRequest('tools/list'); + } + + async callTool(name, arguments) { + return this.sendRequest('tools/call', { name, arguments }); + } + + disconnect() { + if (this.eventSource) { + this.eventSource.close(); + this.eventSource = null; + } + } +} + +// Usage +const client = new MCPSSEClient('http://localhost:8080/mcp'); + +async function main() { + try { + await client.connect(); + + const tools = await client.listTools(); + console.log('Available tools:', tools); + + const result = await client.callTool('stream_data', { + source: 'api', + count: 5 + }); + console.log('Tool result:', result); + + } catch (error) { + console.error('Error:', error); + } +} + +main(); +``` + +### React Integration + +```jsx +import React, { useState, useEffect, useCallback } from 'react'; + +function MCPDashboard() { + const [client, setClient] = useState(null); + const [connected, setConnected] = useState(false); + const [tools, setTools] = useState([]); + const [messages, setMessages] = useState([]); + const [progress, setProgress] = useState(null); + + useEffect(() => { + const mcpClient = new MCPSSEClient('http://localhost:8080/mcp'); + + mcpClient.onProgress = (progressData) => { + setProgress(progressData); + }; + + mcpClient.onMessage = (message) => { + setMessages(prev => [...prev, message]); + }; + + mcpClient.connect() + .then(() => { + setConnected(true); + return mcpClient.listTools(); + }) + .then(toolList => { + setTools(toolList.tools); + }) + .catch(error => { + console.error('Connection failed:', error); + }); + + setClient(mcpClient); + + return () => { + mcpClient.disconnect(); + }; + }, []); + + const callTool = useCallback(async (toolName, args) => { + if (!client) return; + + try { + const result = await client.callTool(toolName, args); + setMessages(prev => [...prev, { + type: 'tool_result', + tool: toolName, + result, + timestamp: Date.now() + }]); + } catch (error) { + console.error('Tool call failed:', error); + } + }, [client]); + + return ( +
+
+ Status: {connected ? 'Connected' : 'Disconnected'} +
+ + {progress && ( +
+
+
+
+
+ {progress.message} ({progress.progress}/{progress.total}) +
+
+ )} + +
+

Available Tools

+ {tools.map(tool => ( +
+

{tool.name}

+

{tool.description}

+ +
+ ))} +
+ +
+

Messages

+ {messages.map((message, index) => ( +
+
{JSON.stringify(message, null, 2)}
+
+ ))} +
+
+ ); +} + +export default MCPDashboard; +``` + +## Performance and Scaling + +### Connection Pooling + +```go +type ConnectionPool struct { + connections map[string]*Connection + mutex sync.RWMutex + maxConns int + cleanup *time.Ticker +} + +func NewConnectionPool(maxConns int) *ConnectionPool { + pool := &ConnectionPool{ + connections: make(map[string]*Connection), + maxConns: maxConns, + cleanup: time.NewTicker(1 * time.Minute), + } + + go pool.cleanupLoop() + return pool +} + +func (p *ConnectionPool) cleanupLoop() { + for range p.cleanup.C { + p.mutex.Lock() + now := time.Now() + + for id, conn := range p.connections { + if now.Sub(conn.LastSeen) > 5*time.Minute { + conn.Close() + delete(p.connections, id) + } + } + + p.mutex.Unlock() + } +} +``` + +### Load Balancing + +```go +type LoadBalancer struct { + servers []string + current int + mutex sync.Mutex +} + +func NewLoadBalancer(servers []string) *LoadBalancer { + return &LoadBalancer{ + servers: servers, + } +} + +func (lb *LoadBalancer) NextServer() string { + lb.mutex.Lock() + defer lb.mutex.Unlock() + + server := lb.servers[lb.current] + lb.current = (lb.current + 1) % len(lb.servers) + return server +} + +// Use with reverse proxy +func setupReverseProxy() { + lb := NewLoadBalancer([]string{ + "http://server1:8080", + "http://server2:8080", + "http://server3:8080", + }) + + http.HandleFunc("/mcp/", func(w http.ResponseWriter, r *http.Request) { + target := lb.NextServer() + proxy := httputil.NewSingleHostReverseProxy(mustParseURL(target)) + proxy.ServeHTTP(w, r) + }) +} +``` + +## Next Steps + +- **[HTTP Transport](/transports/http)** - Learn about traditional web service patterns +- **[In-Process Transport](/transports/inprocess)** - Explore embedded scenarios +- **[Client Development](/clients)** - Build MCP clients for different transports \ No newline at end of file diff --git a/www/docs/pages/transports/stdio.mdx b/www/docs/pages/transports/stdio.mdx new file mode 100644 index 000000000..6dea7adcf --- /dev/null +++ b/www/docs/pages/transports/stdio.mdx @@ -0,0 +1,684 @@ +# STDIO Transport + +STDIO (Standard Input/Output) transport is the most common MCP transport method, perfect for command-line tools, desktop applications, and local integrations. + +## Use Cases + +STDIO transport excels in scenarios where: + +- **Command-line tools**: CLI utilities that LLMs can invoke +- **Desktop applications**: IDE plugins, text editors, local tools +- **Subprocess communication**: Parent processes managing MCP servers +- **Local development**: Testing and debugging MCP implementations +- **Single-user scenarios**: Personal productivity tools + +**Example applications:** +- File system browsers for IDEs +- Local database query tools +- Git repository analyzers +- System monitoring utilities +- Development workflow automation + +## Implementation + +### Basic STDIO Server + +```go +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func main() { + s := server.NewMCPServer("File Tools", "1.0.0", + server.WithToolCapabilities(true), + server.WithResourceCapabilities(true), + ) + + // Add file listing tool + s.AddTool( + mcp.NewTool("list_files", + mcp.WithDescription("List files in a directory"), + mcp.WithString("path", + mcp.Required(), + mcp.Description("Directory path to list"), + ), + mcp.WithBoolean("recursive", + mcp.Default(false), + mcp.Description("List files recursively"), + ), + ), + handleListFiles, + ) + + // Add file content resource + s.AddResource( + mcp.NewResource( + "file://{path}", + "File Content", + mcp.WithResourceDescription("Read file contents"), + mcp.WithMIMEType("text/plain"), + ), + handleFileContent, + ) + + // Start STDIO server + if err := server.ServeStdio(s); err != nil { + fmt.Fprintf(os.Stderr, "Server error: %v\n", err) + os.Exit(1) + } +} + +func handleListFiles(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + path, err := req.RequireString("path") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + recursive, err := req.RequireBool("recursive") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + + // Security: validate path + if !isValidPath(path) { + return mcp.NewToolResultError(fmt.Sprintf("invalid path: %s", path)), nil + } + + files, err := listFiles(path, recursive) + if err != nil { + return mcp.NewToolResultError(fmt.Sprintf("failed to list files: %v", err)), nil + } + + return mcp.NewToolResultJSON(map[string]interface{}{ + "path": path, + "files": files, + "count": len(files), + "recursive": recursive, + }), nil +} + +func handleFileContent(ctx context.Context, req mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + // Extract path from URI: "file:///path/to/file" -> "/path/to/file" + path := extractPathFromURI(req.Params.URI) + + if !isValidPath(path) { + return nil, fmt.Errorf("invalid path: %s", path) + } + + content, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + + return &mcp.ReadResourceResult{ + Contents: []mcp.ResourceContent{ + { + URI: req.Params.URI, + MIMEType: detectMIMEType(path), + Text: string(content), + }, + }, + }, nil +} + +func isValidPath(path string) bool { + // Clean the path to resolve any . or .. components + clean := filepath.Clean(path) + + // Check for directory traversal patterns + if strings.Contains(clean, "..") { + return false + } + + // For absolute paths, ensure they're within a safe base directory + if filepath.IsAbs(clean) { + // Define safe base directories (adjust as needed for your use case) + safeBaseDirs := []string{ + "/tmp", + "/var/tmp", + "/home", + "/Users", // macOS + } + + // Check if the path starts with any safe base directory + for _, baseDir := range safeBaseDirs { + if strings.HasPrefix(clean, baseDir) { + return true + } + } + return false + } + + // For relative paths, ensure they don't escape the current directory + return !strings.HasPrefix(clean, "..") +} + +// Helper functions for the examples +func listFiles(path string, recursive bool) ([]string, error) { + // Placeholder implementation + return []string{"file1.txt", "file2.txt"}, nil +} + +func extractPathFromURI(uri string) string { + // Extract path from URI: "file:///path/to/file" -> "/path/to/file" + if strings.HasPrefix(uri, "file://") { + return strings.TrimPrefix(uri, "file://") + } + return uri +} + +func detectMIMEType(path string) string { + // Simple MIME type detection based on extension + ext := filepath.Ext(path) + switch ext { + case ".txt": + return "text/plain" + case ".json": + return "application/json" + case ".html": + return "text/html" + default: + return "application/octet-stream" + } +} +``` + +### Advanced STDIO Server + +```go +package main +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "syscall" + "time" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func main() { + s := server.NewMCPServer("Advanced CLI Tool", "1.0.0", + server.WithAllCapabilities(), + server.WithRecovery(), + server.WithHooks(&server.Hooks{ + OnSessionStart: func(sessionID string) { + logToFile(fmt.Sprintf("Session started: %s", sessionID)) + }, + OnSessionEnd: func(sessionID string) { + logToFile(fmt.Sprintf("Session ended: %s", sessionID)) + }, + }), + ) + + // Add comprehensive tools + addSystemTools(s) + addFileTools(s) + addGitTools(s) + addDatabaseTools(s) + + // Handle graceful shutdown + setupGracefulShutdown(s) + + // Start with error handling + if err := server.ServeStdio(s); err != nil { + logError(fmt.Sprintf("Server error: %v", err)) + os.Exit(1) + } +} + +// Helper functions for the advanced example +func logToFile(message string) { + // Placeholder implementation + log.Println(message) +} + +func logError(message string) { + // Placeholder implementation + log.Printf("ERROR: %s", message) +} + +func addSystemTools(s *server.MCPServer) { + // Placeholder implementation +} + +func addFileTools(s *server.MCPServer) { + // Placeholder implementation +} + +func addGitTools(s *server.MCPServer) { + // Placeholder implementation +} + +func addDatabaseTools(s *server.MCPServer) { + // Placeholder implementation +} + +func setupGracefulShutdown(s *server.MCPServer) { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + + go func() { + <-c + logToFile("Received shutdown signal") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := s.Shutdown(ctx); err != nil { + logError(fmt.Sprintf("Shutdown error: %v", err)) + } + + os.Exit(0) + }() +} +``` + +## Client Integration + +### How LLM Applications Connect + +LLM applications typically connect to STDIO MCP servers by: + +1. **Spawning the process**: Starting your server as a subprocess +2. **Pipe communication**: Using stdin/stdout for JSON-RPC messages +3. **Lifecycle management**: Handling process startup, shutdown, and errors + +### Claude Desktop Integration + +Configure your STDIO server in Claude Desktop: + +```json +{ + "mcpServers": { + "file-tools": { + "command": "go", + "args": ["run", "/path/to/your/server/main.go"], + "env": { + "LOG_LEVEL": "info" + } + } + } +} +``` + +**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` +**Windows**: `%APPDATA%\Claude\claude_desktop_config.json` + +### Custom Client Integration + +```go +package main + +import ( + "context" + "log" + + "github.com/mark3labs/mcp-go/client" +) + +func main() { + // Create STDIO client + c, err := client.NewStdioClient( + "go", "run", "/path/to/server/main.go", + ) + if err != nil { + log.Fatal(err) + } + defer c.Close() + + ctx := context.Background() + + // Initialize connection + if err := c.Initialize(ctx); err != nil { + log.Fatal(err) + } + + // List available tools + tools, err := c.ListTools(ctx) + if err != nil { + log.Fatal(err) + } + + log.Printf("Available tools: %d", len(tools.Tools)) + for _, tool := range tools.Tools { + log.Printf("- %s: %s", tool.Name, tool.Description) + } + + // Call a tool + result, err := c.CallTool(ctx, mcp.CallToolRequest{ + Params: mcp.CallToolRequestParams{ + Name: "list_files", + Arguments: map[string]interface{}{ + "path": ".", + "recursive": false, + }, + }, + }) + if err != nil { + log.Fatal(err) + } + + log.Printf("Tool result: %+v", result) +} +``` + +## Debugging + +### Command Line Testing + +Test your STDIO server directly from the command line: + +```bash +# Start your server +go run main.go + +# Send initialization request +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"clientInfo":{"name":"test","version":"1.0.0"}}}' | go run main.go + +# List tools +echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | go run main.go + +# Call a tool +echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_files","arguments":{"path":".","recursive":false}}}' | go run main.go +``` + +### Interactive Testing Script + +```bash +#!/bin/bash + +# interactive_test.sh +SERVER_CMD="go run main.go" + +echo "Starting MCP STDIO server test..." + +# Function to send JSON-RPC request +send_request() { + local request="$1" + echo "Sending: $request" + echo "$request" | $SERVER_CMD + echo "---" +} + +# Initialize +send_request '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"clientInfo":{"name":"test","version":"1.0.0"}}}' + +# List tools +send_request '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' + +# List resources +send_request '{"jsonrpc":"2.0","id":3,"method":"resources/list","params":{}}' + +# Call tool +send_request '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"list_files","arguments":{"path":".","recursive":false}}}' + +echo "Test completed." +``` + +### Debug Logging + +Add debug logging to your STDIO server: + +```go +func main() { + // Setup debug logging + logFile, err := os.OpenFile("mcp-server.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + log.Fatal(err) + } + defer logFile.Close() + + logger := log.New(logFile, "[MCP] ", log.LstdFlags|log.Lshortfile) + + s := server.NewMCPServer("Debug Server", "1.0.0", + server.WithToolCapabilities(true), + server.WithHooks(&server.Hooks{ + OnSessionStart: func(sessionID string) { + logger.Printf("Session started: %s", sessionID) + }, + OnToolCall: func(sessionID, toolName string, duration time.Duration, err error) { + if err != nil { + logger.Printf("Tool %s failed: %v", toolName, err) + } else { + logger.Printf("Tool %s completed in %v", toolName, duration) + } + }, + }), + ) + + // Add tools with debug logging + s.AddTool( + mcp.NewTool("debug_echo", + mcp.WithDescription("Echo with debug logging"), + mcp.WithString("message", mcp.Required()), + ), + func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + message := req.Params.Arguments["message"].(string) + logger.Printf("Echo tool called with message: %s", message) + return mcp.NewToolResultText(fmt.Sprintf("Echo: %s", message)), nil + }, + ) + + logger.Println("Starting STDIO server...") + if err := server.ServeStdio(s); err != nil { + logger.Printf("Server error: %v", err) + } +} +``` + +### MCP Inspector Integration + +Use the MCP Inspector for visual debugging: + +```bash +# Install MCP Inspector +npm install -g @modelcontextprotocol/inspector + +# Run your server with inspector +mcp-inspector go run main.go +``` + +This opens a web interface where you can: +- View available tools and resources +- Test tool calls interactively +- Inspect request/response messages +- Debug protocol issues + +## Error Handling + +### Robust Error Handling + +```go +func handleToolWithErrors(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Validate required parameters + path, ok := req.Params.Arguments["path"].(string) + if !ok { + return nil, fmt.Errorf("path parameter is required and must be a string") + } + + // Validate path security + if !isValidPath(path) { + return nil, fmt.Errorf("invalid or unsafe path: %s", path) + } + + // Check if path exists + if _, err := os.Stat(path); os.IsNotExist(err) { + return nil, fmt.Errorf("path does not exist: %s", path) + } + + // Handle context cancellation + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + // Perform operation with timeout + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + result, err := performOperation(ctx, path) + if err != nil { + // Log error for debugging + logError(fmt.Sprintf("Operation failed for path %s: %v", path, err)) + + // Return user-friendly error + if errors.Is(err, context.DeadlineExceeded) { + return nil, fmt.Errorf("operation timed out") + } + + return nil, fmt.Errorf("operation failed: %w", err) + } + + return mcp.NewToolResultJSON(result), nil +} +``` + +### Process Management + +```go +func main() { + // Handle panics gracefully + defer func() { + if r := recover(); r != nil { + logError(fmt.Sprintf("Server panic: %v", r)) + os.Exit(1) + } + }() + + s := server.NewMCPServer("Robust Server", "1.0.0", + server.WithRecovery(), // Built-in panic recovery + ) + + // Setup signal handling + setupSignalHandling() + + // Start server with retry logic + for attempts := 0; attempts < 3; attempts++ { + if err := server.ServeStdio(s); err != nil { + logError(fmt.Sprintf("Server attempt %d failed: %v", attempts+1, err)) + if attempts == 2 { + os.Exit(1) + } + time.Sleep(time.Second * time.Duration(attempts+1)) + } else { + break + } + } +} + +func setupSignalHandling() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + + go func() { + sig := <-c + logToFile(fmt.Sprintf("Received signal: %v", sig)) + os.Exit(0) + }() +} +``` + +## Performance Optimization + +### Efficient Resource Usage + +```go +// Use connection pooling for database tools +var dbPool *sql.DB + +func init() { + var err error + dbPool, err = sql.Open("sqlite3", "data.db") + if err != nil { + log.Fatal(err) + } + + dbPool.SetMaxOpenConns(10) + dbPool.SetMaxIdleConns(5) + dbPool.SetConnMaxLifetime(time.Hour) +} + +// Cache frequently accessed data +var fileCache = make(map[string]cacheEntry) +var cacheMutex sync.RWMutex + +type cacheEntry struct { + content string + timestamp time.Time +} + +func getCachedFile(path string) (string, bool) { + cacheMutex.RLock() + defer cacheMutex.RUnlock() + + entry, exists := fileCache[path] + if !exists || time.Since(entry.timestamp) > 5*time.Minute { + return "", false + } + + return entry.content, true +} +``` + +### Memory Management + +```go +func handleLargeFile(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + path := req.Params.Arguments["path"].(string) + + // Stream large files instead of loading into memory + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + // Process in chunks + const chunkSize = 64 * 1024 + buffer := make([]byte, chunkSize) + + var result strings.Builder + for { + n, err := file.Read(buffer) + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + // Process chunk + processed := processChunk(buffer[:n]) + result.WriteString(processed) + + // Check for cancellation + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + } + + return mcp.NewToolResultText(result.String()), nil +} +``` + +## Next Steps + +- **[SSE Transport](/transports/sse)** - Learn about real-time web communication +- **[HTTP Transport](/transports/http)** - Explore traditional web service patterns +- **[In-Process Transport](/transports/inprocess)** - Understand embedded scenarios \ No newline at end of file diff --git a/www/vocs.config.ts b/www/vocs.config.ts index f54e73a44..38717cd16 100644 --- a/www/vocs.config.ts +++ b/www/vocs.config.ts @@ -10,8 +10,90 @@ export default defineConfig({ link: '/getting-started', }, { - text: 'Example', - link: '/example', + text: 'Quick Start', + link: '/quick-start', + }, + { + text: 'Core Concepts', + link: '/core-concepts', + }, + { + text: 'Building MCP Servers', + collapsed: false, + items: [ + { + text: 'Overview', + link: '/servers', + }, + { + text: 'Server Basics', + link: '/servers/basics', + }, + { + text: 'Resources', + link: '/servers/resources', + }, + { + text: 'Tools', + link: '/servers/tools', + }, + { + text: 'Prompts', + link: '/servers/prompts', + }, + { + text: 'Advanced Features', + link: '/servers/advanced', + }, + ], + }, + { + text: 'Transport Options', + collapsed: false, + items: [ + { + text: 'Overview', + link: '/transports', + }, + { + text: 'STDIO Transport', + link: '/transports/stdio', + }, + { + text: 'SSE Transport', + link: '/transports/sse', + }, + { + text: 'HTTP Transport', + link: '/transports/http', + }, + { + text: 'In-Process Transport', + link: '/transports/inprocess', + }, + ], + }, + { + text: 'Building MCP Clients', + collapsed: false, + items: [ + { + text: 'Overview', + link: '/clients', + }, + { + text: 'Client Basics', + link: '/clients/basics', + }, + { + text: 'Client Operations', + link: '/clients/operations', + }, + { + text: 'Client Transports', + link: '/clients/transports', + }, + ], }, ], socials: [