Repository: mark3labs/mcp-go

Issue:
SSEOption should not have negative effects when used repeatedly but should always remain consistent.
The current SSEOption causes abnormal configuration changes when called multiple times. SSEOption should not have negative effects when used repeatedly but should always remain consistent.  https://github.com/mark3labs/mcp-go/pull/63

Discussion:

Solution patch:
diff --git a/README.md b/README.md
index c23c712..d179694 100644
--- a/README.md
+++ b/README.md
@@ -509,6 +509,18 @@ Prompts can include:
 
 For examples, see the `examples/` directory.
 
+## Extras
+
+### Request Hooks
+
+Hook into the request lifecycle by creating a `Hooks` object with your
+selection among the possible callbacks.  This enables telemetry across all
+functionality, and observability of various facts, for example the ability
+to count improperly-formatted requests, or to log the agent identity during
+initialization.
+
+Add the `Hooks` to the server at the time of creation using the
+`server.WithHooks` option.
 
 ## Contributing
 
diff --git a/client/sse.go b/client/sse.go
index 2cc6635..20886d3 100644
--- a/client/sse.go
+++ b/client/sse.go
@@ -99,39 +99,48 @@ func (c *SSEMCPClient) Start(ctx context.Context) error {
 // readSSE continuously reads the SSE stream and processes events.
 // It runs until the connection is closed or an error occurs.
 func (c *SSEMCPClient) readSSE(reader io.ReadCloser) {
-	defer reader.Close()
-
-	scanner := bufio.NewScanner(reader)
-	var event, data string
-
-	for scanner.Scan() {
-		line := scanner.Text()
-
-		if line == "" {
-			// Empty line means end of event
-			if event != "" && data != "" {
-				c.handleSSEEvent(event, data)
-				event = ""
-				data = ""
-			}
-			continue
-		}
-
-		if strings.HasPrefix(line, "event:") {
-			event = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
-		} else if strings.HasPrefix(line, "data:") {
-			data = strings.TrimSpace(strings.TrimPrefix(line, "data:"))
-		}
-	}
-
-	if err := scanner.Err(); err != nil {
-		select {
-		case <-c.done:
-			return
-		default:
-			fmt.Printf("SSE stream error: %v\n", err)
-		}
-	}
+    defer reader.Close()
+
+    br := bufio.NewReader(reader)
+    var event, data string
+
+    for {
+        line, err := br.ReadString('\n')
+        if err != nil {
+            if err == io.EOF {
+                // Process any pending event before exit
+                if event != "" && data != "" {
+                    c.handleSSEEvent(event, data)
+                }
+                break
+            }
+            select {
+            case <-c.done:
+                return
+            default:
+                fmt.Printf("SSE stream error: %v\n", err)
+                return
+            }
+        }
+
+        // Remove only newline markers
+        line = strings.TrimRight(line, "\r\n")
+        if line == "" {
+            // Empty line means end of event
+            if event != "" && data != "" {
+                c.handleSSEEvent(event, data)
+                event = ""
+                data = ""
+            }
+            continue
+        }
+
+        if strings.HasPrefix(line, "event:") {
+            event = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
+        } else if strings.HasPrefix(line, "data:") {
+            data = strings.TrimSpace(strings.TrimPrefix(line, "data:"))
+        }
+    }
 }
 
 // handleSSEEvent processes SSE events based on their type.
diff --git a/examples/everything/main.go b/examples/everything/main.go
index c2fcc7b..2fad3d4 100644
--- a/examples/everything/main.go
+++ b/examples/everything/main.go
@@ -29,12 +29,38 @@ const (
 )
 
 func NewMCPServer() *server.MCPServer {
+
+	hooks := &server.Hooks{}
+
+	hooks.AddBeforeAny(func(id any, method mcp.MCPMethod, message any) {
+		fmt.Printf("beforeAny: %s, %v, %v\n", method, id, message)
+	})
+	hooks.AddOnSuccess(func(id any, method mcp.MCPMethod, message any, result any) {
+		fmt.Printf("onSuccess: %s, %v, %v, %v\n", method, id, message, result)
+	})
+	hooks.AddOnError(func(id any, method mcp.MCPMethod, message any, err error) {
+		fmt.Printf("onError: %s, %v, %v, %v\n", method, id, message, err)
+	})
+	hooks.AddBeforeInitialize(func(id any, message *mcp.InitializeRequest) {
+		fmt.Printf("beforeInitialize: %v, %v\n", id, message)
+	})
+	hooks.AddAfterInitialize(func(id any, message *mcp.InitializeRequest, result *mcp.InitializeResult) {
+		fmt.Printf("afterInitialize: %v, %v, %v\n", id, message, result)
+	})
+	hooks.AddAfterCallTool(func(id any, message *mcp.CallToolRequest, result *mcp.CallToolResult) {
+		fmt.Printf("afterCallTool: %v, %v, %v\n", id, message, result)
+	})
+	hooks.AddBeforeCallTool(func(id any, message *mcp.CallToolRequest) {
+		fmt.Printf("beforeCallTool: %v, %v\n", id, message)
+	})
+
 	mcpServer := server.NewMCPServer(
 		"example-servers/everything",
 		"1.0.0",
 		server.WithResourceCapabilities(true, true),
 		server.WithPromptCapabilities(true),
 		server.WithLogging(),
+		server.WithHooks(hooks),
 	)
 
 	mcpServer.AddResource(mcp.NewResource("test://static/resource",
diff --git a/go.mod b/go.mod
index 289df77..940f05d 100644
--- a/go.mod
+++ b/go.mod
@@ -5,11 +5,11 @@ go 1.23
 require (
 	github.com/google/uuid v1.6.0
 	github.com/stretchr/testify v1.9.0
+	github.com/yosida95/uritemplate/v3 v3.0.2
 )
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/mcp/types.go b/mcp/types.go
index c94d58e..a3ad817 100644
--- a/mcp/types.go
+++ b/mcp/types.go
@@ -8,6 +8,46 @@ import (
 	"github.com/yosida95/uritemplate/v3"
 )
 
+type MCPMethod string
+
+const (
+	// Initiates connection and negotiates protocol capabilities.
+	// https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle/#initialization
+	MethodInitialize MCPMethod = "initialize"
+
+	// Verifies connection liveness between client and server.
+	// https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/utilities/ping/
+	MethodPing MCPMethod = "ping"
+
+	// Lists all available server resources.
+	// https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/
+	MethodResourcesList MCPMethod = "resources/list"
+
+	// Provides URI templates for constructing resource URIs.
+	// https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/
+	MethodResourcesTemplatesList MCPMethod = "resources/templates/list"
+
+	// Retrieves content of a specific resource by URI.
+	// https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/
+	MethodResourcesRead MCPMethod = "resources/read"
+
+	// Lists all available prompt templates.
+	// https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/
+	MethodPromptsList MCPMethod = "prompts/list"
+
+	// Retrieves a specific prompt template with filled parameters.
+	// https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/
+	MethodPromptsGet MCPMethod = "prompts/get"
+
+	// Lists all available executable tools.
+	// https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/
+	MethodToolsList MCPMethod = "tools/list"
+
+	// Invokes a specific tool with provided parameters.
+	// https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/
+	MethodToolsCall MCPMethod = "tools/call"
+)
+
 type URITemplate struct {
 	*uritemplate.Template
 }
diff --git a/server/hooks.go b/server/hooks.go
new file mode 100644
index 0000000..075fe9f
--- /dev/null
+++ b/server/hooks.go
@@ -0,0 +1,442 @@
+// Code generated by `go generate`. DO NOT EDIT.
+// source: server/internal/gen/hooks.go.tmpl
+package server
+
+import (
+	"github.com/mark3labs/mcp-go/mcp"
+)
+
+// BeforeAnyHookFunc is a function that is called after the request is
+// parsed but before the method is called.
+type BeforeAnyHookFunc func(id any, method mcp.MCPMethod, message any)
+
+// OnSuccessHookFunc is a hook that will be called after the request
+// successfully generates a result, but before the result is sent to the client.
+type OnSuccessHookFunc func(id any, method mcp.MCPMethod, message any, result any)
+
+// OnErrorHookFunc is a hook that will be called when an error occurs,
+// either during the request parsing or the method execution.
+//
+// Example usage:
+// ```
+//
+//	hooks.AddOnError(func(id any, method mcp.MCPMethod, message any, err error) {
+//	  // Check for specific error types using errors.Is
+//	  if errors.Is(err, ErrUnsupported) {
+//	    // Handle capability not supported errors
+//	    log.Printf("Capability not supported: %v", err)
+//	  }
+//
+//	  // Use errors.As to get specific error types
+//	  var parseErr = &UnparseableMessageError{}
+//	  if errors.As(err, &parseErr) {
+//	    // Access specific methods/fields of the error type
+//	    log.Printf("Failed to parse message for method %s: %v",
+//	               parseErr.GetMethod(), parseErr.Unwrap())
+//	    // Access the raw message that failed to parse
+//	    rawMsg := parseErr.GetMessage()
+//	  }
+//
+//	  // Check for specific resource/prompt/tool errors
+//	  switch {
+//	  case errors.Is(err, ErrResourceNotFound):
+//	    log.Printf("Resource not found: %v", err)
+//	  case errors.Is(err, ErrPromptNotFound):
+//	    log.Printf("Prompt not found: %v", err)
+//	  case errors.Is(err, ErrToolNotFound):
+//	    log.Printf("Tool not found: %v", err)
+//	  }
+//	})
+type OnErrorHookFunc func(id any, method mcp.MCPMethod, message any, err error)
+
+type OnBeforeInitializeFunc func(id any, message *mcp.InitializeRequest)
+type OnAfterInitializeFunc func(id any, message *mcp.InitializeRequest, result *mcp.InitializeResult)
+
+type OnBeforePingFunc func(id any, message *mcp.PingRequest)
+type OnAfterPingFunc func(id any, message *mcp.PingRequest, result *mcp.EmptyResult)
+
+type OnBeforeListResourcesFunc func(id any, message *mcp.ListResourcesRequest)
+type OnAfterListResourcesFunc func(id any, message *mcp.ListResourcesRequest, result *mcp.ListResourcesResult)
+
+type OnBeforeListResourceTemplatesFunc func(id any, message *mcp.ListResourceTemplatesRequest)
+type OnAfterListResourceTemplatesFunc func(id any, message *mcp.ListResourceTemplatesRequest, result *mcp.ListResourceTemplatesResult)
+
+type OnBeforeReadResourceFunc func(id any, message *mcp.ReadResourceRequest)
+type OnAfterReadResourceFunc func(id any, message *mcp.ReadResourceRequest, result *mcp.ReadResourceResult)
+
+type OnBeforeListPromptsFunc func(id any, message *mcp.ListPromptsRequest)
+type OnAfterListPromptsFunc func(id any, message *mcp.ListPromptsRequest, result *mcp.ListPromptsResult)
+
+type OnBeforeGetPromptFunc func(id any, message *mcp.GetPromptRequest)
+type OnAfterGetPromptFunc func(id any, message *mcp.GetPromptRequest, result *mcp.GetPromptResult)
+
+type OnBeforeListToolsFunc func(id any, message *mcp.ListToolsRequest)
+type OnAfterListToolsFunc func(id any, message *mcp.ListToolsRequest, result *mcp.ListToolsResult)
+
+type OnBeforeCallToolFunc func(id any, message *mcp.CallToolRequest)
+type OnAfterCallToolFunc func(id any, message *mcp.CallToolRequest, result *mcp.CallToolResult)
+
+type Hooks struct {
+	OnBeforeAny                   []BeforeAnyHookFunc
+	OnSuccess                     []OnSuccessHookFunc
+	OnError                       []OnErrorHookFunc
+	OnBeforeInitialize            []OnBeforeInitializeFunc
+	OnAfterInitialize             []OnAfterInitializeFunc
+	OnBeforePing                  []OnBeforePingFunc
+	OnAfterPing                   []OnAfterPingFunc
+	OnBeforeListResources         []OnBeforeListResourcesFunc
+	OnAfterListResources          []OnAfterListResourcesFunc
+	OnBeforeListResourceTemplates []OnBeforeListResourceTemplatesFunc
+	OnAfterListResourceTemplates  []OnAfterListResourceTemplatesFunc
+	OnBeforeReadResource          []OnBeforeReadResourceFunc
+	OnAfterReadResource           []OnAfterReadResourceFunc
+	OnBeforeListPrompts           []OnBeforeListPromptsFunc
+	OnAfterListPrompts            []OnAfterListPromptsFunc
+	OnBeforeGetPrompt             []OnBeforeGetPromptFunc
+	OnAfterGetPrompt              []OnAfterGetPromptFunc
+	OnBeforeListTools             []OnBeforeListToolsFunc
+	OnAfterListTools              []OnAfterListToolsFunc
+	OnBeforeCallTool              []OnBeforeCallToolFunc
+	OnAfterCallTool               []OnAfterCallToolFunc
+}
+
+func (c *Hooks) AddBeforeAny(hook BeforeAnyHookFunc) {
+	c.OnBeforeAny = append(c.OnBeforeAny, hook)
+}
+
+func (c *Hooks) AddOnSuccess(hook OnSuccessHookFunc) {
+	c.OnSuccess = append(c.OnSuccess, hook)
+}
+
+// AddOnError registers a hook function that will be called when an error occurs.
+// The error parameter contains the actual error object, which can be interrogated
+// using Go's error handling patterns like errors.Is and errors.As.
+//
+// Example:
+// ```
+// // Create a channel to receive errors for testing
+// errChan := make(chan error, 1)
+//
+// // Register hook to capture and inspect errors
+// hooks := &Hooks{}
+//
+//	hooks.AddOnError(func(id any, method mcp.MCPMethod, message any, err error) {
+//	    // For capability-related errors
+//	    if errors.Is(err, ErrUnsupported) {
+//	        // Handle capability not supported
+//	        errChan <- err
+//	        return
+//	    }
+//
+//	    // For parsing errors
+//	    var parseErr = &UnparseableMessageError{}
+//	    if errors.As(err, &parseErr) {
+//	        // Handle unparseable message errors
+//	        fmt.Printf("Failed to parse %s request: %v\n",
+//	                   parseErr.GetMethod(), parseErr.Unwrap())
+//	        errChan <- parseErr
+//	        return
+//	    }
+//
+//	    // For resource/prompt/tool not found errors
+//	    if errors.Is(err, ErrResourceNotFound) ||
+//	       errors.Is(err, ErrPromptNotFound) ||
+//	       errors.Is(err, ErrToolNotFound) {
+//	        // Handle not found errors
+//	        errChan <- err
+//	        return
+//	    }
+//
+//	    // For other errors
+//	    errChan <- err
+//	})
+//
+// server := NewMCPServer("test-server", "1.0.0", WithHooks(hooks))
+// ```
+func (c *Hooks) AddOnError(hook OnErrorHookFunc) {
+	c.OnError = append(c.OnError, hook)
+}
+
+func (c *Hooks) beforeAny(id any, method mcp.MCPMethod, message any) {
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnBeforeAny {
+		hook(id, method, message)
+	}
+}
+
+func (c *Hooks) onSuccess(id any, method mcp.MCPMethod, message any, result any) {
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnSuccess {
+		hook(id, method, message, result)
+	}
+}
+
+// onError calls all registered error hooks with the error object.
+// The err parameter contains the actual error that occurred, which implements
+// the standard error interface and may be a wrapped error or custom error type.
+//
+// This allows consumer code to use Go's error handling patterns:
+// - errors.Is(err, ErrUnsupported) to check for specific sentinel errors
+// - errors.As(err, &customErr) to extract custom error types
+//
+// Common error types include:
+// - ErrUnsupported: When a capability is not enabled
+// - UnparseableMessageError: When request parsing fails
+// - ErrResourceNotFound: When a resource is not found
+// - ErrPromptNotFound: When a prompt is not found
+// - ErrToolNotFound: When a tool is not found
+func (c *Hooks) onError(id any, method mcp.MCPMethod, message any, err error) {
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnError {
+		hook(id, method, message, err)
+	}
+}
+func (c *Hooks) AddBeforeInitialize(hook OnBeforeInitializeFunc) {
+	c.OnBeforeInitialize = append(c.OnBeforeInitialize, hook)
+}
+
+func (c *Hooks) AddAfterInitialize(hook OnAfterInitializeFunc) {
+	c.OnAfterInitialize = append(c.OnAfterInitialize, hook)
+}
+
+func (c *Hooks) beforeInitialize(id any, message *mcp.InitializeRequest) {
+	c.beforeAny(id, mcp.MethodInitialize, message)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnBeforeInitialize {
+		hook(id, message)
+	}
+}
+
+func (c *Hooks) afterInitialize(id any, message *mcp.InitializeRequest, result *mcp.InitializeResult) {
+	c.onSuccess(id, mcp.MethodInitialize, message, result)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnAfterInitialize {
+		hook(id, message, result)
+	}
+}
+func (c *Hooks) AddBeforePing(hook OnBeforePingFunc) {
+	c.OnBeforePing = append(c.OnBeforePing, hook)
+}
+
+func (c *Hooks) AddAfterPing(hook OnAfterPingFunc) {
+	c.OnAfterPing = append(c.OnAfterPing, hook)
+}
+
+func (c *Hooks) beforePing(id any, message *mcp.PingRequest) {
+	c.beforeAny(id, mcp.MethodPing, message)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnBeforePing {
+		hook(id, message)
+	}
+}
+
+func (c *Hooks) afterPing(id any, message *mcp.PingRequest, result *mcp.EmptyResult) {
+	c.onSuccess(id, mcp.MethodPing, message, result)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnAfterPing {
+		hook(id, message, result)
+	}
+}
+func (c *Hooks) AddBeforeListResources(hook OnBeforeListResourcesFunc) {
+	c.OnBeforeListResources = append(c.OnBeforeListResources, hook)
+}
+
+func (c *Hooks) AddAfterListResources(hook OnAfterListResourcesFunc) {
+	c.OnAfterListResources = append(c.OnAfterListResources, hook)
+}
+
+func (c *Hooks) beforeListResources(id any, message *mcp.ListResourcesRequest) {
+	c.beforeAny(id, mcp.MethodResourcesList, message)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnBeforeListResources {
+		hook(id, message)
+	}
+}
+
+func (c *Hooks) afterListResources(id any, message *mcp.ListResourcesRequest, result *mcp.ListResourcesResult) {
+	c.onSuccess(id, mcp.MethodResourcesList, message, result)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnAfterListResources {
+		hook(id, message, result)
+	}
+}
+func (c *Hooks) AddBeforeListResourceTemplates(hook OnBeforeListResourceTemplatesFunc) {
+	c.OnBeforeListResourceTemplates = append(c.OnBeforeListResourceTemplates, hook)
+}
+
+func (c *Hooks) AddAfterListResourceTemplates(hook OnAfterListResourceTemplatesFunc) {
+	c.OnAfterListResourceTemplates = append(c.OnAfterListResourceTemplates, hook)
+}
+
+func (c *Hooks) beforeListResourceTemplates(id any, message *mcp.ListResourceTemplatesRequest) {
+	c.beforeAny(id, mcp.MethodResourcesTemplatesList, message)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnBeforeListResourceTemplates {
+		hook(id, message)
+	}
+}
+
+func (c *Hooks) afterListResourceTemplates(id any, message *mcp.ListResourceTemplatesRequest, result *mcp.ListResourceTemplatesResult) {
+	c.onSuccess(id, mcp.MethodResourcesTemplatesList, message, result)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnAfterListResourceTemplates {
+		hook(id, message, result)
+	}
+}
+func (c *Hooks) AddBeforeReadResource(hook OnBeforeReadResourceFunc) {
+	c.OnBeforeReadResource = append(c.OnBeforeReadResource, hook)
+}
+
+func (c *Hooks) AddAfterReadResource(hook OnAfterReadResourceFunc) {
+	c.OnAfterReadResource = append(c.OnAfterReadResource, hook)
+}
+
+func (c *Hooks) beforeReadResource(id any, message *mcp.ReadResourceRequest) {
+	c.beforeAny(id, mcp.MethodResourcesRead, message)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnBeforeReadResource {
+		hook(id, message)
+	}
+}
+
+func (c *Hooks) afterReadResource(id any, message *mcp.ReadResourceRequest, result *mcp.ReadResourceResult) {
+	c.onSuccess(id, mcp.MethodResourcesRead, message, result)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnAfterReadResource {
+		hook(id, message, result)
+	}
+}
+func (c *Hooks) AddBeforeListPrompts(hook OnBeforeListPromptsFunc) {
+	c.OnBeforeListPrompts = append(c.OnBeforeListPrompts, hook)
+}
+
+func (c *Hooks) AddAfterListPrompts(hook OnAfterListPromptsFunc) {
+	c.OnAfterListPrompts = append(c.OnAfterListPrompts, hook)
+}
+
+func (c *Hooks) beforeListPrompts(id any, message *mcp.ListPromptsRequest) {
+	c.beforeAny(id, mcp.MethodPromptsList, message)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnBeforeListPrompts {
+		hook(id, message)
+	}
+}
+
+func (c *Hooks) afterListPrompts(id any, message *mcp.ListPromptsRequest, result *mcp.ListPromptsResult) {
+	c.onSuccess(id, mcp.MethodPromptsList, message, result)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnAfterListPrompts {
+		hook(id, message, result)
+	}
+}
+func (c *Hooks) AddBeforeGetPrompt(hook OnBeforeGetPromptFunc) {
+	c.OnBeforeGetPrompt = append(c.OnBeforeGetPrompt, hook)
+}
+
+func (c *Hooks) AddAfterGetPrompt(hook OnAfterGetPromptFunc) {
+	c.OnAfterGetPrompt = append(c.OnAfterGetPrompt, hook)
+}
+
+func (c *Hooks) beforeGetPrompt(id any, message *mcp.GetPromptRequest) {
+	c.beforeAny(id, mcp.MethodPromptsGet, message)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnBeforeGetPrompt {
+		hook(id, message)
+	}
+}
+
+func (c *Hooks) afterGetPrompt(id any, message *mcp.GetPromptRequest, result *mcp.GetPromptResult) {
+	c.onSuccess(id, mcp.MethodPromptsGet, message, result)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnAfterGetPrompt {
+		hook(id, message, result)
+	}
+}
+func (c *Hooks) AddBeforeListTools(hook OnBeforeListToolsFunc) {
+	c.OnBeforeListTools = append(c.OnBeforeListTools, hook)
+}
+
+func (c *Hooks) AddAfterListTools(hook OnAfterListToolsFunc) {
+	c.OnAfterListTools = append(c.OnAfterListTools, hook)
+}
+
+func (c *Hooks) beforeListTools(id any, message *mcp.ListToolsRequest) {
+	c.beforeAny(id, mcp.MethodToolsList, message)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnBeforeListTools {
+		hook(id, message)
+	}
+}
+
+func (c *Hooks) afterListTools(id any, message *mcp.ListToolsRequest, result *mcp.ListToolsResult) {
+	c.onSuccess(id, mcp.MethodToolsList, message, result)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnAfterListTools {
+		hook(id, message, result)
+	}
+}
+func (c *Hooks) AddBeforeCallTool(hook OnBeforeCallToolFunc) {
+	c.OnBeforeCallTool = append(c.OnBeforeCallTool, hook)
+}
+
+func (c *Hooks) AddAfterCallTool(hook OnAfterCallToolFunc) {
+	c.OnAfterCallTool = append(c.OnAfterCallTool, hook)
+}
+
+func (c *Hooks) beforeCallTool(id any, message *mcp.CallToolRequest) {
+	c.beforeAny(id, mcp.MethodToolsCall, message)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnBeforeCallTool {
+		hook(id, message)
+	}
+}
+
+func (c *Hooks) afterCallTool(id any, message *mcp.CallToolRequest, result *mcp.CallToolResult) {
+	c.onSuccess(id, mcp.MethodToolsCall, message, result)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnAfterCallTool {
+		hook(id, message, result)
+	}
+}
diff --git a/server/internal/gen/README.md b/server/internal/gen/README.md
new file mode 100644
index 0000000..e7fc890
--- /dev/null
+++ b/server/internal/gen/README.md
@@ -0,0 +1,24 @@
+# Readme for Codegen
+
+This internal module contains code generation for producing a few repetitive
+constructs, namely:
+
+- The switch statement that handles the request dispatch
+- The hook function types and the methods on the Hook struct
+
+To invoke the code generation:
+
+```
+go generate ./...
+```
+
+## Development
+
+- `request_handler.go.tmpl` generates `server/request_handler.go`, and
+- `hooks.go.tmpl` generates `server/hooks.go`
+
+Inside of `data.go` there is a struct with the inputs to both templates.
+
+Note that the driver in `main.go` generates code and also pipes it through
+`goimports` for formatting and imports cleanup.
+
diff --git a/server/internal/gen/data.go b/server/internal/gen/data.go
new file mode 100644
index 0000000..50fd70c
--- /dev/null
+++ b/server/internal/gen/data.go
@@ -0,0 +1,101 @@
+package main
+
+type MCPRequestType struct {
+	MethodName     string
+	ParamType      string
+	ResultType     string
+	HookName       string
+	Group          string
+	GroupName      string
+	GroupHookName  string
+	UnmarshalError string
+	HandlerFunc    string
+}
+
+var MCPRequestTypes = []MCPRequestType{
+	{
+		MethodName:     "MethodInitialize",
+		ParamType:      "InitializeRequest",
+		ResultType:     "InitializeResult",
+		HookName:       "Initialize",
+		UnmarshalError: "invalid initialize request",
+		HandlerFunc:    "handleInitialize",
+	}, {
+		MethodName:     "MethodPing",
+		ParamType:      "PingRequest",
+		ResultType:     "EmptyResult",
+		HookName:       "Ping",
+		UnmarshalError: "invalid ping request",
+		HandlerFunc:    "handlePing",
+	}, {
+		MethodName:     "MethodResourcesList",
+		ParamType:      "ListResourcesRequest",
+		ResultType:     "ListResourcesResult",
+		Group:          "resources",
+		GroupName:      "Resources",
+		GroupHookName:  "Resource",
+		HookName:       "ListResources",
+		UnmarshalError: "invalid list resources request",
+		HandlerFunc:    "handleListResources",
+	}, {
+		MethodName:     "MethodResourcesTemplatesList",
+		ParamType:      "ListResourceTemplatesRequest",
+		ResultType:     "ListResourceTemplatesResult",
+		Group:          "resources",
+		GroupName:      "Resources",
+		GroupHookName:  "Resource",
+		HookName:       "ListResourceTemplates",
+		UnmarshalError: "invalid list resource templates request",
+		HandlerFunc:    "handleListResourceTemplates",
+	}, {
+		MethodName:     "MethodResourcesRead",
+		ParamType:      "ReadResourceRequest",
+		ResultType:     "ReadResourceResult",
+		Group:          "resources",
+		GroupName:      "Resources",
+		GroupHookName:  "Resource",
+		HookName:       "ReadResource",
+		UnmarshalError: "invalid read resource request",
+		HandlerFunc:    "handleReadResource",
+	}, {
+		MethodName:     "MethodPromptsList",
+		ParamType:      "ListPromptsRequest",
+		ResultType:     "ListPromptsResult",
+		Group:          "prompts",
+		GroupName:      "Prompts",
+		GroupHookName:  "Prompt",
+		HookName:       "ListPrompts",
+		UnmarshalError: "invalid list prompts request",
+		HandlerFunc:    "handleListPrompts",
+	}, {
+		MethodName:     "MethodPromptsGet",
+		ParamType:      "GetPromptRequest",
+		ResultType:     "GetPromptResult",
+		Group:          "prompts",
+		GroupName:      "Prompts",
+		GroupHookName:  "Prompt",
+		HookName:       "GetPrompt",
+		UnmarshalError: "invalid get prompt request",
+		HandlerFunc:    "handleGetPrompt",
+	}, {
+		MethodName:     "MethodToolsList",
+		ParamType:      "ListToolsRequest",
+		ResultType:     "ListToolsResult",
+		Group:          "tools",
+		GroupName:      "Tools",
+		GroupHookName:  "Tool",
+		HookName:       "ListTools",
+		UnmarshalError: "invalid list tools request",
+		HandlerFunc:    "handleListTools",
+	}, {
+		MethodName:     "MethodToolsCall",
+		ParamType:      "CallToolRequest",
+		ResultType:     "CallToolResult",
+		Group:          "tools",
+		GroupName:      "Tools",
+		GroupHookName:  "Tool",
+		HookName:       "CallTool",
+		UnmarshalError: "invalid call tool request",
+		HandlerFunc:    "handleToolCall",
+	},
+}
diff --git a/server/internal/gen/hooks.go.tmpl b/server/internal/gen/hooks.go.tmpl
new file mode 100644
index 0000000..240ab03
--- /dev/null
+++ b/server/internal/gen/hooks.go.tmpl
@@ -0,0 +1,196 @@
+// Code generated by `go generate`. DO NOT EDIT.
+// source: server/internal/gen/hooks.go.tmpl
+package server
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+
+	"github.com/mark3labs/mcp-go/mcp"
+)
+
+// BeforeAnyHookFunc is a function that is called after the request is
+// parsed but before the method is called.
+type BeforeAnyHookFunc func(id any, method mcp.MCPMethod, message any)
+
+// OnSuccessHookFunc is a hook that will be called after the request
+// successfully generates a result, but before the result is sent to the client.
+type OnSuccessHookFunc func(id any, method mcp.MCPMethod, message any, result any)
+
+// OnErrorHookFunc is a hook that will be called when an error occurs,
+// either during the request parsing or the method execution.
+// 
+// Example usage:
+// ```
+// hooks.AddOnError(func(id any, method mcp.MCPMethod, message any, err error) {
+//   // Check for specific error types using errors.Is
+//   if errors.Is(err, ErrUnsupported) {
+//     // Handle capability not supported errors
+//     log.Printf("Capability not supported: %v", err)
+//   }
+//
+//   // Use errors.As to get specific error types
+//   var parseErr = &UnparseableMessageError{}
+//   if errors.As(err, &parseErr) {
+//     // Access specific methods/fields of the error type
+//     log.Printf("Failed to parse message for method %s: %v", 
+//                parseErr.GetMethod(), parseErr.Unwrap())
+//     // Access the raw message that failed to parse
+//     rawMsg := parseErr.GetMessage()
+//   }
+//
+//   // Check for specific resource/prompt/tool errors
+//   switch {
+//   case errors.Is(err, ErrResourceNotFound):
+//     log.Printf("Resource not found: %v", err)
+//   case errors.Is(err, ErrPromptNotFound):
+//     log.Printf("Prompt not found: %v", err)
+//   case errors.Is(err, ErrToolNotFound):
+//     log.Printf("Tool not found: %v", err)
+//   }
+// })
+type OnErrorHookFunc func(id any, method mcp.MCPMethod, message any, err error)
+
+{{range .}}
+type OnBefore{{.HookName}}Func func(id any, message *mcp.{{.ParamType}})
+type OnAfter{{.HookName}}Func func(id any, message *mcp.{{.ParamType}}, result *mcp.{{.ResultType}})
+{{end}}
+
+type Hooks struct {
+	OnBeforeAny      []BeforeAnyHookFunc
+	OnSuccess        []OnSuccessHookFunc
+	OnError          []OnErrorHookFunc
+{{- range .}}
+	OnBefore{{.HookName}} []OnBefore{{.HookName}}Func
+	OnAfter{{.HookName}}  []OnAfter{{.HookName}}Func
+{{- end}}
+}
+
+func (c *Hooks) AddBeforeAny(hook BeforeAnyHookFunc) {
+	c.OnBeforeAny = append(c.OnBeforeAny, hook)
+}
+
+func (c *Hooks) AddOnSuccess(hook OnSuccessHookFunc) {
+	c.OnSuccess = append(c.OnSuccess, hook)
+}
+
+// AddOnError registers a hook function that will be called when an error occurs.
+// The error parameter contains the actual error object, which can be interrogated
+// using Go's error handling patterns like errors.Is and errors.As.
+//
+// Example:
+// ```
+// // Create a channel to receive errors for testing
+// errChan := make(chan error, 1)
+// 
+// // Register hook to capture and inspect errors
+// hooks := &Hooks{}
+// hooks.AddOnError(func(id any, method mcp.MCPMethod, message any, err error) {
+//     // For capability-related errors
+//     if errors.Is(err, ErrUnsupported) {
+//         // Handle capability not supported
+//         errChan <- err
+//         return
+//     }
+//
+//     // For parsing errors
+//     var parseErr = &UnparseableMessageError{}
+//     if errors.As(err, &parseErr) {
+//         // Handle unparseable message errors
+//         fmt.Printf("Failed to parse %s request: %v\n", 
+//                    parseErr.GetMethod(), parseErr.Unwrap())
+//         errChan <- parseErr
+//         return
+//     }
+//
+//     // For resource/prompt/tool not found errors
+//     if errors.Is(err, ErrResourceNotFound) ||
+//        errors.Is(err, ErrPromptNotFound) ||
+//        errors.Is(err, ErrToolNotFound) {
+//         // Handle not found errors
+//         errChan <- err
+//         return
+//     }
+//
+//     // For other errors
+//     errChan <- err
+// })
+//
+// server := NewMCPServer("test-server", "1.0.0", WithHooks(hooks))
+// ```
+func (c *Hooks) AddOnError(hook OnErrorHookFunc) {
+	c.OnError = append(c.OnError, hook)
+}
+
+func (c *Hooks) beforeAny(id any, method mcp.MCPMethod, message any) {
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnBeforeAny {
+		hook(id, method, message)
+	}
+}
+
+func (c *Hooks) onSuccess(id any, method mcp.MCPMethod, message any, result any) {
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnSuccess {
+		hook(id, method, message, result)
+	}
+}
+
+// onError calls all registered error hooks with the error object.
+// The err parameter contains the actual error that occurred, which implements
+// the standard error interface and may be a wrapped error or custom error type.
+//
+// This allows consumer code to use Go's error handling patterns:
+// - errors.Is(err, ErrUnsupported) to check for specific sentinel errors
+// - errors.As(err, &customErr) to extract custom error types
+//
+// Common error types include:
+// - ErrUnsupported: When a capability is not enabled
+// - UnparseableMessageError: When request parsing fails
+// - ErrResourceNotFound: When a resource is not found
+// - ErrPromptNotFound: When a prompt is not found
+// - ErrToolNotFound: When a tool is not found
+func (c *Hooks) onError(id any, method mcp.MCPMethod, message any, err error) {
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnError {
+		hook(id, method, message, err)
+	}
+}
+
+{{- range .}}
+func (c *Hooks) AddBefore{{.HookName}}(hook OnBefore{{.HookName}}Func) {
+	c.OnBefore{{.HookName}} = append(c.OnBefore{{.HookName}}, hook)
+}
+
+func (c *Hooks) AddAfter{{.HookName}}(hook OnAfter{{.HookName}}Func) {
+	c.OnAfter{{.HookName}} = append(c.OnAfter{{.HookName}}, hook)
+}
+
+func (c *Hooks) before{{.HookName}}(id any, message *mcp.{{.ParamType}}) {
+	c.beforeAny(id, mcp.{{.MethodName}}, message)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnBefore{{.HookName}} {
+		hook(id, message)
+	}
+}
+
+func (c *Hooks) after{{.HookName}}(id any, message *mcp.{{.ParamType}}, result *mcp.{{.ResultType}}) {
+	c.onSuccess(id, mcp.{{.MethodName}}, message, result)
+	if c == nil {
+		return
+	}
+	for _, hook := range c.OnAfter{{.HookName}} {
+		hook(id, message, result)
+	}
+}
+{{- end -}}
diff --git a/server/internal/gen/main.go b/server/internal/gen/main.go
new file mode 100644
index 0000000..5bb3e81
--- /dev/null
+++ b/server/internal/gen/main.go
@@ -0,0 +1,89 @@
+package main
+
+import (
+	_ "embed"
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"text/template"
+)
+
+//go:generate go run . ../..
+
+//go:embed hooks.go.tmpl
+var hooksTemplate string
+
+//go:embed request_handler.go.tmpl
+var requestHandlerTemplate string
+
+func RenderTemplateToFile(templateContent, destPath, fileName string, data any) error {
+	// Create temp file for initial output
+	tempFile, err := os.CreateTemp("", "hooks-*.go")
+	if err != nil {
+		return err
+	}
+	tempFilePath := tempFile.Name()
+	defer os.Remove(tempFilePath) // Clean up temp file when done
+
+	// Parse and execute template to temp file
+	tmpl, err := template.New(fileName).Funcs(template.FuncMap{
+		"toLower": strings.ToLower,
+	}).Parse(templateContent)
+	if err != nil {
+		tempFile.Close()
+		return err
+	}
+
+	if err := tmpl.Execute(tempFile, data); err != nil {
+		tempFile.Close()
+		return err
+	}
+	tempFile.Close()
+
+	// Run goimports on the temp file
+	cmd := exec.Command("go", "run", "golang.org/x/tools/cmd/goimports@latest", "-w", tempFilePath)
+	if output, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("goimports failed: %v\n%s", err, output)
+	}
+
+	// Read the processed content
+	processedContent, err := os.ReadFile(tempFilePath)
+	if err != nil {
+		return err
+	}
+
+	// Write the processed content to the destination
+	var destWriter io.Writer
+	if destPath == "-" {
+		destWriter = os.Stdout
+	} else {
+		destFile, err := os.Create(filepath.Join(destPath, fileName))
+		if err != nil {
+			return err
+		}
+		defer destFile.Close()
+		destWriter = destFile
+	}
+
+	_, err = destWriter.Write(processedContent)
+	return err
+}
+
+func main() {
+	if len(os.Args) < 2 {
+		log.Fatal("usage: gen <destination-directory>")
+	}
+	destPath := os.Args[1]
+
+	if err := RenderTemplateToFile(hooksTemplate, destPath, "hooks.go", MCPRequestTypes); err != nil {
+		log.Fatal(err)
+	}
+
+	if err := RenderTemplateToFile(requestHandlerTemplate, destPath, "request_handler.go", MCPRequestTypes); err != nil {
+		log.Fatal(err)
+	}
+}
diff --git a/server/internal/gen/request_handler.go.tmpl b/server/internal/gen/request_handler.go.tmpl
new file mode 100644
index 0000000..ac9de6e
--- /dev/null
+++ b/server/internal/gen/request_handler.go.tmpl
@@ -0,0 +1,94 @@
+// Code generated by `go generate`. DO NOT EDIT.
+// source: server/internal/gen/request_handler.go.tmpl
+package server
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+
+	"github.com/mark3labs/mcp-go/mcp"
+)
+
+// HandleMessage processes an incoming JSON-RPC message and returns an appropriate response
+func (s *MCPServer) HandleMessage(
+	ctx context.Context,
+	message json.RawMessage,
+) mcp.JSONRPCMessage {
+	// Add server to context
+	ctx = context.WithValue(ctx, serverKey{}, s)
+	var err *requestError
+
+	var baseMessage struct {
+		JSONRPC string      `json:"jsonrpc"`
+		Method  mcp.MCPMethod `json:"method"`
+		ID      any           `json:"id,omitempty"`
+	}
+
+	if err := json.Unmarshal(message, &baseMessage); err != nil {
+		return createErrorResponse(
+			nil,
+			mcp.PARSE_ERROR,
+			"Failed to parse message",
+		)
+	}
+
+	// Check for valid JSONRPC version
+	if baseMessage.JSONRPC != mcp.JSONRPC_VERSION {
+		return createErrorResponse(
+			baseMessage.ID,
+			mcp.INVALID_REQUEST,
+			"Invalid JSON-RPC version",
+		)
+	}
+
+	if baseMessage.ID == nil {
+		var notification mcp.JSONRPCNotification
+		if err := json.Unmarshal(message, &notification); err != nil {
+			return createErrorResponse(
+				nil,
+				mcp.PARSE_ERROR,
+				"Failed to parse notification",
+			)
+		}
+		s.handleNotification(ctx, notification)
+		return nil // Return nil for notifications
+	}
+
+	switch baseMessage.Method {
+	{{- range .}}
+	case mcp.{{.MethodName}}:
+		var request mcp.{{.ParamType}}
+		var result *mcp.{{.ResultType}}
+		{{ if .Group }}if s.capabilities.{{.Group}} == nil {
+			err = &requestError{
+				id:   baseMessage.ID,
+				code: mcp.METHOD_NOT_FOUND,
+				err:  fmt.Errorf("{{toLower .GroupName}} %w", ErrUnsupported),
+			}
+		} else{{ end }} if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil {
+			err = &requestError{
+				id:   baseMessage.ID,
+				code: mcp.INVALID_REQUEST,
+				err:  &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method},
+			}
+		} else {
+			s.hooks.before{{.HookName}}(baseMessage.ID, &request)
+			result, err = s.{{.HandlerFunc}}(ctx, baseMessage.ID, request)
+		}
+		if err != nil {
+			s.hooks.onError(baseMessage.ID, baseMessage.Method, &request, err)
+			return err.ToJSONRPCError()
+		}
+		s.hooks.after{{.HookName}}(baseMessage.ID, &request, result)
+		return createResponse(baseMessage.ID, *result)
+	{{- end }}
+	default:
+		return createErrorResponse(
+			baseMessage.ID,
+			mcp.METHOD_NOT_FOUND,
+			fmt.Sprintf("Method %s not found", baseMessage.Method),
+		)
+	}
+}
diff --git a/server/request_handler.go b/server/request_handler.go
new file mode 100644
index 0000000..e1167b2
--- /dev/null
+++ b/server/request_handler.go
@@ -0,0 +1,279 @@
+// Code generated by `go generate`. DO NOT EDIT.
+// source: server/internal/gen/request_handler.go.tmpl
+package server
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	"github.com/mark3labs/mcp-go/mcp"
+)
+
+// HandleMessage processes an incoming JSON-RPC message and returns an appropriate response
+func (s *MCPServer) HandleMessage(
+	ctx context.Context,
+	message json.RawMessage,
+) mcp.JSONRPCMessage {
+	// Add server to context
+	ctx = context.WithValue(ctx, serverKey{}, s)
+	var err *requestError
+
+	var baseMessage struct {
+		JSONRPC string        `json:"jsonrpc"`
+		Method  mcp.MCPMethod `json:"method"`
+		ID      any           `json:"id,omitempty"`
+	}
+
+	if err := json.Unmarshal(message, &baseMessage); err != nil {
+		return createErrorResponse(
+			nil,
+			mcp.PARSE_ERROR,
+			"Failed to parse message",
+		)
+	}
+
+	// Check for valid JSONRPC version
+	if baseMessage.JSONRPC != mcp.JSONRPC_VERSION {
+		return createErrorResponse(
+			baseMessage.ID,
+			mcp.INVALID_REQUEST,
+			"Invalid JSON-RPC version",
+		)
+	}
+
+	if baseMessage.ID == nil {
+		var notification mcp.JSONRPCNotification
+		if err := json.Unmarshal(message, &notification); err != nil {
+			return createErrorResponse(
+				nil,
+				mcp.PARSE_ERROR,
+				"Failed to parse notification",
+			)
+		}
+		s.handleNotification(ctx, notification)
+		return nil // Return nil for notifications
+	}
+
+	switch baseMessage.Method {
+	case mcp.MethodInitialize:
+		var request mcp.InitializeRequest
+		var result *mcp.InitializeResult
+		if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil {
+			err = &requestError{
+				id:   baseMessage.ID,
+				code: mcp.INVALID_REQUEST,
+				err:  &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method},
+			}
+		} else {
+			s.hooks.beforeInitialize(baseMessage.ID, &request)
+			result, err = s.handleInitialize(ctx, baseMessage.ID, request)
+		}
+		if err != nil {
+			s.hooks.onError(baseMessage.ID, baseMessage.Method, &request, err)
+			return err.ToJSONRPCError()
+		}
+		s.hooks.afterInitialize(baseMessage.ID, &request, result)
+		return createResponse(baseMessage.ID, *result)
+	case mcp.MethodPing:
+		var request mcp.PingRequest
+		var result *mcp.EmptyResult
+		if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil {
+			err = &requestError{
+				id:   baseMessage.ID,
+				code: mcp.INVALID_REQUEST,
+				err:  &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method},
+			}
+		} else {
+			s.hooks.beforePing(baseMessage.ID, &request)
+			result, err = s.handlePing(ctx, baseMessage.ID, request)
+		}
+		if err != nil {
+			s.hooks.onError(baseMessage.ID, baseMessage.Method, &request, err)
+			return err.ToJSONRPCError()
+		}
+		s.hooks.afterPing(baseMessage.ID, &request, result)
+		return createResponse(baseMessage.ID, *result)
+	case mcp.MethodResourcesList:
+		var request mcp.ListResourcesRequest
+		var result *mcp.ListResourcesResult
+		if s.capabilities.resources == nil {
+			err = &requestError{
+				id:   baseMessage.ID,
+				code: mcp.METHOD_NOT_FOUND,
+				err:  fmt.Errorf("resources %w", ErrUnsupported),
+			}
+		} else if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil {
+			err = &requestError{
+				id:   baseMessage.ID,
+				code: mcp.INVALID_REQUEST,
+				err:  &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method},
+			}
+		} else {
+			s.hooks.beforeListResources(baseMessage.ID, &request)
+			result, err = s.handleListResources(ctx, baseMessage.ID, request)
+		}
+		if err != nil {
+			s.hooks.onError(baseMessage.ID, baseMessage.Method, &request, err)
+			return err.ToJSONRPCError()
+		}
+		s.hooks.afterListResources(baseMessage.ID, &request, result)
+		return createResponse(baseMessage.ID, *result)
+	case mcp.MethodResourcesTemplatesList:
+		var request mcp.ListResourceTemplatesRequest
+		var result *mcp.ListResourceTemplatesResult
+		if s.capabilities.resources == nil {
+			err = &requestError{
+				id:   baseMessage.ID,
+				code: mcp.METHOD_NOT_FOUND,
+				err:  fmt.Errorf("resources %w", ErrUnsupported),
+			}
+		} else if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil {
+			err = &requestError{
+				id:   baseMessage.ID,
+				code: mcp.INVALID_REQUEST,
+				err:  &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method},
+			}
+		} else {
+			s.hooks.beforeListResourceTemplates(baseMessage.ID, &request)
+			result, err = s.handleListResourceTemplates(ctx, baseMessage.ID, request)
+		}
+		if err != nil {
+			s.hooks.onError(baseMessage.ID, baseMessage.Method, &request, err)
+			return err.ToJSONRPCError()
+		}
+		s.hooks.afterListResourceTemplates(baseMessage.ID, &request, result)
+		return createResponse(baseMessage.ID, *result)
+	case mcp.MethodResourcesRead:
+		var request mcp.ReadResourceRequest
+		var result *mcp.ReadResourceResult
+		if s.capabilities.resources == nil {
+			err = &requestError{
+				id:   baseMessage.ID,
+				code: mcp.METHOD_NOT_FOUND,
+				err:  fmt.Errorf("resources %w", ErrUnsupported),
+			}
+		} else if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil {
+			err = &requestError{
+				id:   baseMessage.ID,
+				code: mcp.INVALID_REQUEST,
+				err:  &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method},
+			}
+		} else {
+			s.hooks.beforeReadResource(baseMessage.ID, &request)
+			result, err = s.handleReadResource(ctx, baseMessage.ID, request)
+		}
+		if err != nil {
+			s.hooks.onError(baseMessage.ID, baseMessage.Method, &request, err)
+			return err.ToJSONRPCError()
+		}
+		s.hooks.afterReadResource(baseMessage.ID, &request, result)
+		return createResponse(baseMessage.ID, *result)
+	case mcp.MethodPromptsList:
+		var request mcp.ListPromptsRequest
+		var result *mcp.ListPromptsResult
+		if s.capabilities.prompts == nil {
+			err = &requestError{
+				id:   baseMessage.ID,
+				code: mcp.METHOD_NOT_FOUND,
+				err:  fmt.Errorf("prompts %w", ErrUnsupported),
+			}
+		} else if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil {
+			err = &requestError{
+				id:   baseMessage.ID,
+				code: mcp.INVALID_REQUEST,
+				err:  &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method},
+			}
+		} else {
+			s.hooks.beforeListPrompts(baseMessage.ID, &request)
+			result, err = s.handleListPrompts(ctx, baseMessage.ID, request)
+		}
+		if err != nil {
+			s.hooks.onError(baseMessage.ID, baseMessage.Method, &request, err)
+			return err.ToJSONRPCError()
+		}
+		s.hooks.afterListPrompts(baseMessage.ID, &request, result)
+		return createResponse(baseMessage.ID, *result)
+	case mcp.MethodPromptsGet:
+		var request mcp.GetPromptRequest
+		var result *mcp.GetPromptResult
+		if s.capabilities.prompts == nil {
+			err = &requestError{
+				id:   baseMessage.ID,
+				code: mcp.METHOD_NOT_FOUND,
+				err:  fmt.Errorf("prompts %w", ErrUnsupported),
+			}
+		} else if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil {
+			err = &requestError{
+				id:   baseMessage.ID,
+				code: mcp.INVALID_REQUEST,
+				err:  &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method},
+			}
+		} else {
+			s.hooks.beforeGetPrompt(baseMessage.ID, &request)
+			result, err = s.handleGetPrompt(ctx, baseMessage.ID, request)
+		}
+		if err != nil {
+			s.hooks.onError(baseMessage.ID, baseMessage.Method, &request, err)
+			return err.ToJSONRPCError()
+		}
+		s.hooks.afterGetPrompt(baseMessage.ID, &request, result)
+		return createResponse(baseMessage.ID, *result)
+	case mcp.MethodToolsList:
+		var request mcp.ListToolsRequest
+		var result *mcp.ListToolsResult
+		if s.capabilities.tools == nil {
+			err = &requestError{
+				id:   baseMessage.ID,
+				code: mcp.METHOD_NOT_FOUND,
+				err:  fmt.Errorf("tools %w", ErrUnsupported),
+			}
+		} else if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil {
+			err = &requestError{
+				id:   baseMessage.ID,
+				code: mcp.INVALID_REQUEST,
+				err:  &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method},
+			}
+		} else {
+			s.hooks.beforeListTools(baseMessage.ID, &request)
+			result, err = s.handleListTools(ctx, baseMessage.ID, request)
+		}
+		if err != nil {
+			s.hooks.onError(baseMessage.ID, baseMessage.Method, &request, err)
+			return err.ToJSONRPCError()
+		}
+		s.hooks.afterListTools(baseMessage.ID, &request, result)
+		return createResponse(baseMessage.ID, *result)
+	case mcp.MethodToolsCall:
+		var request mcp.CallToolRequest
+		var result *mcp.CallToolResult
+		if s.capabilities.tools == nil {
+			err = &requestError{
+				id:   baseMessage.ID,
+				code: mcp.METHOD_NOT_FOUND,
+				err:  fmt.Errorf("tools %w", ErrUnsupported),
+			}
+		} else if unmarshalErr := json.Unmarshal(message, &request); unmarshalErr != nil {
+			err = &requestError{
+				id:   baseMessage.ID,
+				code: mcp.INVALID_REQUEST,
+				err:  &UnparseableMessageError{message: message, err: unmarshalErr, method: baseMessage.Method},
+			}
+		} else {
+			s.hooks.beforeCallTool(baseMessage.ID, &request)
+			result, err = s.handleToolCall(ctx, baseMessage.ID, request)
+		}
+		if err != nil {
+			s.hooks.onError(baseMessage.ID, baseMessage.Method, &request, err)
+			return err.ToJSONRPCError()
+		}
+		s.hooks.afterCallTool(baseMessage.ID, &request, result)
+		return createResponse(baseMessage.ID, *result)
+	default:
+		return createErrorResponse(
+			baseMessage.ID,
+			mcp.METHOD_NOT_FOUND,
+			fmt.Sprintf("Method %s not found", baseMessage.Method),
+		)
+	}
+}
diff --git a/server/server.go b/server/server.go
index 5e851a0..64b5162 100644
--- a/server/server.go
+++ b/server/server.go
@@ -4,6 +4,7 @@ package server
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"sort"
 	"sync"
@@ -64,6 +65,68 @@ func ClientSessionFromContext(ctx context.Context) ClientSession {
 	return nil
 }
 
+// UnparseableMessageError is attached to the RequestError when json.Unmarshal
+// fails on the request.
+type UnparseableMessageError struct {
+	message json.RawMessage
+	method  mcp.MCPMethod
+	err     error
+}
+
+func (e *UnparseableMessageError) Error() string {
+	return fmt.Sprintf("unparseable %s request: %s", e.method, e.err)
+}
+
+func (e *UnparseableMessageError) Unwrap() error {
+	return e.err
+}
+
+func (e *UnparseableMessageError) GetMessage() json.RawMessage {
+	return e.message
+}
+
+func (e *UnparseableMessageError) GetMethod() mcp.MCPMethod {
+	return e.method
+}
+
+// RequestError is an error that can be converted to a JSON-RPC error.
+// Implements Unwrap() to allow inspecting the error chain.
+type requestError struct {
+	id   any
+	code int
+	err  error
+}
+
+func (e *requestError) Error() string {
+	return fmt.Sprintf("request error: %s", e.err)
+}
+
+func (e *requestError) ToJSONRPCError() mcp.JSONRPCError {
+	return mcp.JSONRPCError{
+		JSONRPC: mcp.JSONRPC_VERSION,
+		ID:      e.id,
+		Error: struct {
+			Code    int    `json:"code"`
+			Message string `json:"message"`
+			Data    any    `json:"data,omitempty"`
+		}{
+			Code:    e.code,
+			Message: e.err.Error(),
+		},
+	}
+}
+
+func (e *requestError) Unwrap() error {
+	return e.err
+}
+
+var (
+	ErrUnsupported      = errors.New("not supported")
+	ErrResourceNotFound = errors.New("resource not found")
+	ErrPromptNotFound   = errors.New("prompt not found")
+	ErrToolNotFound     = errors.New("tool not found")
+)
+
 // NotificationHandlerFunc handles incoming notifications.
 type NotificationHandlerFunc func(ctx context.Context, notification mcp.JSONRPCNotification)
 
@@ -83,6 +146,7 @@ type MCPServer struct {
 	capabilities         serverCapabilities
 	sessions             sync.Map
 	initialized          atomic.Bool // Use atomic for the initialized flag
+	hooks                *Hooks
 }
 
 // serverKey is the context key for storing the server instance
@@ -213,6 +277,15 @@ func WithResourceCapabilities(subscribe, listChanged bool) ServerOption {
 	}
 }
 
+// WithHooks allows adding hooks that will be called before or after
+// either [all] requests or before / after specific request methods, or else
+// prior to returning an error to the client.
+func WithHooks(hooks *Hooks) ServerOption {
+	return func(s *MCPServer) {
+		s.hooks = hooks
+	}
+}
+
 // WithPromptCapabilities configures prompt-related server capabilities
 func WithPromptCapabilities(listChanged bool) ServerOption {
 	return func(s *MCPServer) {
@@ -276,199 +349,6 @@ func NewMCPServer(
 	return s
 }
 
-// HandleMessage processes an incoming JSON-RPC message and returns an appropriate response
-func (s *MCPServer) HandleMessage(
-	ctx context.Context,
-	message json.RawMessage,
-) mcp.JSONRPCMessage {
-	// Add server to context
-	ctx = context.WithValue(ctx, serverKey{}, s)
-
-	var baseMessage struct {
-		JSONRPC string      `json:"jsonrpc"`
-		Method  string      `json:"method"`
-		ID      interface{} `json:"id,omitempty"`
-	}
-
-	if err := json.Unmarshal(message, &baseMessage); err != nil {
-		return createErrorResponse(
-			nil,
-			mcp.PARSE_ERROR,
-			"Failed to parse message",
-		)
-	}
-
-	// Check for valid JSONRPC version
-	if baseMessage.JSONRPC != mcp.JSONRPC_VERSION {
-		return createErrorResponse(
-			baseMessage.ID,
-			mcp.INVALID_REQUEST,
-			"Invalid JSON-RPC version",
-		)
-	}
-
-	if baseMessage.ID == nil {
-		var notification mcp.JSONRPCNotification
-		if err := json.Unmarshal(message, &notification); err != nil {
-			return createErrorResponse(
-				nil,
-				mcp.PARSE_ERROR,
-				"Failed to parse notification",
-			)
-		}
-		s.handleNotification(ctx, notification)
-		return nil // Return nil for notifications
-	}
-
-	switch baseMessage.Method {
-	case "initialize":
-		var request mcp.InitializeRequest
-		if err := json.Unmarshal(message, &request); err != nil {
-			return createErrorResponse(
-				baseMessage.ID,
-				mcp.INVALID_REQUEST,
-				"Invalid initialize request",
-			)
-		}
-		return s.handleInitialize(ctx, baseMessage.ID, request)
-	case "ping":
-		var request mcp.PingRequest
-		if err := json.Unmarshal(message, &request); err != nil {
-			return createErrorResponse(
-				baseMessage.ID,
-				mcp.INVALID_REQUEST,
-				"Invalid ping request",
-			)
-		}
-		return s.handlePing(ctx, baseMessage.ID, request)
-	case "resources/list":
-		if s.capabilities.resources == nil {
-			return createErrorResponse(
-				baseMessage.ID,
-				mcp.METHOD_NOT_FOUND,
-				"Resources not supported",
-			)
-		}
-		var request mcp.ListResourcesRequest
-		if err := json.Unmarshal(message, &request); err != nil {
-			return createErrorResponse(
-				baseMessage.ID,
-				mcp.INVALID_REQUEST,
-				"Invalid list resources request",
-			)
-		}
-		return s.handleListResources(ctx, baseMessage.ID, request)
-	case "resources/templates/list":
-		if s.capabilities.resources == nil {
-			return createErrorResponse(
-				baseMessage.ID,
-				mcp.METHOD_NOT_FOUND,
-				"Resources not supported",
-			)
-		}
-		var request mcp.ListResourceTemplatesRequest
-		if err := json.Unmarshal(message, &request); err != nil {
-			return createErrorResponse(
-				baseMessage.ID,
-				mcp.INVALID_REQUEST,
-				"Invalid list resource templates request",
-			)
-		}
-		return s.handleListResourceTemplates(ctx, baseMessage.ID, request)
-	case "resources/read":
-		if s.capabilities.resources == nil {
-			return createErrorResponse(
-				baseMessage.ID,
-				mcp.METHOD_NOT_FOUND,
-				"Resources not supported",
-			)
-		}
-		var request mcp.ReadResourceRequest
-		if err := json.Unmarshal(message, &request); err != nil {
-			return createErrorResponse(
-				baseMessage.ID,
-				mcp.INVALID_REQUEST,
-				"Invalid read resource request",
-			)
-		}
-		return s.handleReadResource(ctx, baseMessage.ID, request)
-	case "prompts/list":
-		if s.capabilities.prompts == nil {
-			return createErrorResponse(
-				baseMessage.ID,
-				mcp.METHOD_NOT_FOUND,
-				"Prompts not supported",
-			)
-		}
-		var request mcp.ListPromptsRequest
-		if err := json.Unmarshal(message, &request); err != nil {
-			return createErrorResponse(
-				baseMessage.ID,
-				mcp.INVALID_REQUEST,
-				"Invalid list prompts request",
-			)
-		}
-		return s.handleListPrompts(ctx, baseMessage.ID, request)
-	case "prompts/get":
-		if s.capabilities.prompts == nil {
-			return createErrorResponse(
-				baseMessage.ID,
-				mcp.METHOD_NOT_FOUND,
-				"Prompts not supported",
-			)
-		}
-		var request mcp.GetPromptRequest
-		if err := json.Unmarshal(message, &request); err != nil {
-			return createErrorResponse(
-				baseMessage.ID,
-				mcp.INVALID_REQUEST,
-				"Invalid get prompt request",
-			)
-		}
-		return s.handleGetPrompt(ctx, baseMessage.ID, request)
-	case "tools/list":
-		if s.capabilities.tools == nil {
-			return createErrorResponse(
-				baseMessage.ID,
-				mcp.METHOD_NOT_FOUND,
-				"Tools not supported",
-			)
-		}
-		var request mcp.ListToolsRequest
-		if err := json.Unmarshal(message, &request); err != nil {
-			return createErrorResponse(
-				baseMessage.ID,
-				mcp.INVALID_REQUEST,
-				"Invalid list tools request",
-			)
-		}
-		return s.handleListTools(ctx, baseMessage.ID, request)
-	case "tools/call":
-		if s.capabilities.tools == nil {
-			return createErrorResponse(
-				baseMessage.ID,
-				mcp.METHOD_NOT_FOUND,
-				"Tools not supported",
-			)
-		}
-		var request mcp.CallToolRequest
-		if err := json.Unmarshal(message, &request); err != nil {
-			return createErrorResponse(
-				baseMessage.ID,
-				mcp.INVALID_REQUEST,
-				"Invalid call tool request",
-			)
-		}
-		return s.handleToolCall(ctx, baseMessage.ID, request)
-	default:
-		return createErrorResponse(
-			baseMessage.ID,
-			mcp.METHOD_NOT_FOUND,
-			fmt.Sprintf("Method %s not found", baseMessage.Method),
-		)
-	}
-}
-
 // AddResource registers a new resource and its handler
 func (s *MCPServer) AddResource(
 	resource mcp.Resource,
@@ -572,7 +452,7 @@ func (s *MCPServer) handleInitialize(
 	ctx context.Context,
 	id interface{},
 	request mcp.InitializeRequest,
-) mcp.JSONRPCMessage {
+) (*mcp.InitializeResult, *requestError) {
 	capabilities := mcp.ServerCapabilities{}
 
 	// Only add resource capabilities if they're configured
@@ -619,22 +499,22 @@ func (s *MCPServer) handleInitialize(
 	}
 
 	s.initialized.Store(true)
-	return createResponse(id, result)
+	return &result, nil
 }
 
 func (s *MCPServer) handlePing(
 	ctx context.Context,
 	id interface{},
 	request mcp.PingRequest,
-) mcp.JSONRPCMessage {
-	return createResponse(id, mcp.EmptyResult{})
+) (*mcp.EmptyResult, *requestError) {
+	return &mcp.EmptyResult{}, nil
 }
 
 func (s *MCPServer) handleListResources(
 	ctx context.Context,
 	id interface{},
 	request mcp.ListResourcesRequest,
-) mcp.JSONRPCMessage {
+) (*mcp.ListResourcesResult, *requestError) {
 	s.mu.RLock()
 	resources := make([]mcp.Resource, 0, len(s.resources))
 	for _, entry := range s.resources {
@@ -648,14 +528,14 @@ func (s *MCPServer) handleListResources(
 	if request.Params.Cursor != "" {
 		result.NextCursor = "" // Handle pagination if needed
 	}
-	return createResponse(id, result)
+	return &result, nil
 }
 
 func (s *MCPServer) handleListResourceTemplates(
 	ctx context.Context,
 	id interface{},
 	request mcp.ListResourceTemplatesRequest,
-) mcp.JSONRPCMessage {
+) (*mcp.ListResourceTemplatesResult, *requestError) {
 	s.mu.RLock()
 	templates := make([]mcp.ResourceTemplate, 0, len(s.resourceTemplates))
 	for _, entry := range s.resourceTemplates {
@@ -669,14 +549,14 @@ func (s *MCPServer) handleListResourceTemplates(
 	if request.Params.Cursor != "" {
 		result.NextCursor = "" // Handle pagination if needed
 	}
-	return createResponse(id, result)
+	return &result, nil
 }
 
 func (s *MCPServer) handleReadResource(
 	ctx context.Context,
 	id interface{},
 	request mcp.ReadResourceRequest,
-) mcp.JSONRPCMessage {
+) (*mcp.ReadResourceResult, *requestError) {
 	s.mu.RLock()
 	// First try direct resource handlers
 	if entry, ok := s.resources[request.Params.URI]; ok {
@@ -684,9 +564,13 @@ func (s *MCPServer) handleReadResource(
 		s.mu.RUnlock()
 		contents, err := handler(ctx, request)
 		if err != nil {
-			return createErrorResponse(id, mcp.INTERNAL_ERROR, err.Error())
+			return nil, &requestError{
+				id:   id,
+				code: mcp.INTERNAL_ERROR,
+				err:  err,
+			}
 		}
-		return createResponse(id, mcp.ReadResourceResult{Contents: contents})
+		return &mcp.ReadResourceResult{Contents: contents}, nil
 	}
 
 	// If no direct handler found, try matching against templates
@@ -711,22 +595,20 @@ func (s *MCPServer) handleReadResource(
 	if matched {
 		contents, err := matchedHandler(ctx, request)
 		if err != nil {
-			return createErrorResponse(id, mcp.INTERNAL_ERROR, err.Error())
+			return nil, &requestError{
+				id:   id,
+				code: mcp.INTERNAL_ERROR,
+				err:  err,
+			}
 		}
-		return createResponse(
-			id,
-			mcp.ReadResourceResult{Contents: contents},
-		)
+		return &mcp.ReadResourceResult{Contents: contents}, nil
 	}
 
-	return createErrorResponse(
-		id,
-		mcp.INVALID_PARAMS,
-		fmt.Sprintf(
-			"No handler found for resource URI: %s",
-			request.Params.URI,
-		),
-	)
+	return nil, &requestError{
+		id:   id,
+		code: mcp.INVALID_PARAMS,
+		err:  fmt.Errorf("handler not found for resource URI '%s': %w", request.Params.URI, ErrResourceNotFound),
+	}
 }
 
 // matchesTemplate checks if a URI matches a URI template pattern
@@ -738,7 +620,7 @@ func (s *MCPServer) handleListPrompts(
 	ctx context.Context,
 	id interface{},
 	request mcp.ListPromptsRequest,
-) mcp.JSONRPCMessage {
+) (*mcp.ListPromptsResult, *requestError) {
 	s.mu.RLock()
 	prompts := make([]mcp.Prompt, 0, len(s.prompts))
 	for _, prompt := range s.prompts {
@@ -752,39 +634,43 @@ func (s *MCPServer) handleListPrompts(
 	if request.Params.Cursor != "" {
 		result.NextCursor = "" // Handle pagination if needed
 	}
-	return createResponse(id, result)
+	return &result, nil
 }
 
 func (s *MCPServer) handleGetPrompt(
 	ctx context.Context,
 	id interface{},
 	request mcp.GetPromptRequest,
-) mcp.JSONRPCMessage {
+) (*mcp.GetPromptResult, *requestError) {
 	s.mu.RLock()
 	handler, ok := s.promptHandlers[request.Params.Name]
 	s.mu.RUnlock()
 
 	if !ok {
-		return createErrorResponse(
-			id,
-			mcp.INVALID_PARAMS,
-			fmt.Sprintf("Prompt not found: %s", request.Params.Name),
-		)
+		return nil, &requestError{
+			id:   id,
+			code: mcp.INVALID_PARAMS,
+			err:  fmt.Errorf("prompt '%s' not found: %w", request.Params.Name, ErrPromptNotFound),
+		}
 	}
 
 	result, err := handler(ctx, request)
 	if err != nil {
-		return createErrorResponse(id, mcp.INTERNAL_ERROR, err.Error())
+		return nil, &requestError{
+			id:   id,
+			code: mcp.INTERNAL_ERROR,
+			err:  err,
+		}
 	}
 
-	return createResponse(id, result)
+	return result, nil
 }
 
 func (s *MCPServer) handleListTools(
 	ctx context.Context,
 	id interface{},
 	request mcp.ListToolsRequest,
-) mcp.JSONRPCMessage {
+) (*mcp.ListToolsResult, *requestError) {
 	s.mu.RLock()
 	tools := make([]mcp.Tool, 0, len(s.tools))
 
@@ -809,32 +695,35 @@ func (s *MCPServer) handleListTools(
 	if request.Params.Cursor != "" {
 		result.NextCursor = "" // Handle pagination if needed
 	}
-	return createResponse(id, result)
+	return &result, nil
 }
-
 func (s *MCPServer) handleToolCall(
 	ctx context.Context,
 	id interface{},
 	request mcp.CallToolRequest,
-) mcp.JSONRPCMessage {
+) (*mcp.CallToolResult, *requestError) {
 	s.mu.RLock()
 	tool, ok := s.tools[request.Params.Name]
 	s.mu.RUnlock()
 
 	if !ok {
-		return createErrorResponse(
-			id,
-			mcp.INVALID_PARAMS,
-			fmt.Sprintf("Tool not found: %s", request.Params.Name),
-		)
+		return nil, &requestError{
+			id:   id,
+			code: mcp.INVALID_PARAMS,
+			err:  fmt.Errorf("tool '%s' not found: %w", request.Params.Name, ErrToolNotFound),
+		}
 	}
 
 	result, err := tool.Handler(ctx, request)
 	if err != nil {
-		return createErrorResponse(id, mcp.INTERNAL_ERROR, err.Error())
+		return nil, &requestError{
+			id:   id,
+			code: mcp.INTERNAL_ERROR,
+			err:  err,
+		}
 	}
 
-	return createResponse(id, result)
+	return result, nil
 }
 
 func (s *MCPServer) handleNotification(
diff --git a/server/sse.go b/server/sse.go
index 2e1dceb..c3c070e 100644
--- a/server/sse.go
+++ b/server/sse.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"net/http"
 	"net/http/httptest"
+	"net/url"
 	"strings"
 	"sync"
 
@@ -57,7 +58,23 @@ type SSEOption func(*SSEServer)
 // WithBaseURL sets the base URL for the SSE server
 func WithBaseURL(baseURL string) SSEOption {
 	return func(s *SSEServer) {
-		s.baseURL = baseURL
+		if baseURL != "" {
+			u, err := url.Parse(baseURL)
+			if err != nil {
+				return
+			}
+			if u.Scheme != "http" && u.Scheme != "https" {
+				return
+			}
+			// Check if the host is empty or only contains a port
+			if u.Host == "" || strings.HasPrefix(u.Host, ":") {
+				return
+			}
+			if len(u.Query()) > 0 {
+				return
+			}
+		}
+		s.baseURL = strings.TrimSuffix(baseURL, "/")
 	}
 }
 
@@ -69,7 +86,6 @@ func WithBasePath(basePath string) SSEOption {
 			basePath = "/" + basePath
 		}
 		s.basePath = strings.TrimSuffix(basePath, "/")
-		s.baseURL = s.baseURL + s.basePath
 	}
 }
 
@@ -108,7 +124,6 @@ func NewSSEServer(server *MCPServer, opts ...SSEOption) *SSEServer {
 		server:          server,
 		sseEndpoint:     "/sse",
 		messageEndpoint: "/message",
-		basePath:        "",
 	}
 
 	// Apply all options
@@ -219,12 +234,7 @@ func (s *SSEServer) handleSSE(w http.ResponseWriter, r *http.Request) {
 		}
 	}()
 
-	messageEndpoint := fmt.Sprintf(
-		"%s%s?sessionId=%s",
-		s.baseURL,
-		s.messageEndpoint,
-		sessionID,
-	)
+	messageEndpoint := fmt.Sprintf("%s?sessionId=%s", s.CompleteMessageEndpoint(), sessionID)
 
 	// Send the initial endpoint event
 	fmt.Fprintf(w, "event: endpoint\ndata: %s\r\n\r\n", messageEndpoint)
@@ -345,22 +355,47 @@ func (s *SSEServer) SendEventToSession(
 		return fmt.Errorf("event queue full")
 	}
 }
+func (s *SSEServer) GetUrlPath(input string) (string, error) {
+	parse, err := url.Parse(input)
+	if err != nil {
+		return "", fmt.Errorf("failed to parse URL %s: %w", input, err)
+	}
+	return parse.Path, nil
+}
+
+func (s *SSEServer) CompleteSseEndpoint() string {
+	return s.baseURL + s.basePath + s.sseEndpoint
+}
+func (s *SSEServer) CompleteSsePath() string {
+	path, err := s.GetUrlPath(s.CompleteSseEndpoint())
+	if err != nil {
+		return s.basePath + s.sseEndpoint
+	}
+	return path
+}
+
+func (s *SSEServer) CompleteMessageEndpoint() string {
+	return s.baseURL + s.basePath + s.messageEndpoint
+}
+func (s *SSEServer) CompleteMessagePath() string {
+	path, err := s.GetUrlPath(s.CompleteMessageEndpoint())
+	if err != nil {
+		return s.basePath + s.messageEndpoint
+	}
+	return path
+}
 
 // ServeHTTP implements the http.Handler interface.
 func (s *SSEServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	path := r.URL.Path
-
-	// Construct the full SSE and message paths
-	ssePath := s.basePath + s.sseEndpoint
-	messagePath := s.basePath + s.messageEndpoint
-
 	// Use exact path matching rather than Contains
-	if path == ssePath {
+	ssePath := s.CompleteSsePath()
+	if ssePath != "" && path == ssePath {
 		s.handleSSE(w, r)
 		return
 	}
-
-	if path == messagePath {
+	messagePath := s.CompleteMessagePath()
+	if messagePath != "" && path == messagePath {
 		s.handleMessage(w, r)
 		return
 	}



Test patch:
diff --git a/server/server_test.go b/server/server_test.go
index c0c2ad3..d2bbaf0 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -3,6 +3,7 @@ package server
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"testing"
 	"time"
@@ -615,7 +616,7 @@ func TestMCPServer_PromptHandling(t *testing.T) {
 				resp, ok := response.(mcp.JSONRPCResponse)
 				assert.True(t, ok)
 
-				result, ok := resp.Result.(*mcp.GetPromptResult)
+				result, ok := resp.Result.(mcp.GetPromptResult)
 				assert.True(t, ok)
 				assert.Len(t, result.Messages, 1)
 				textContent, ok := result.Messages[0].Content.(mcp.TextContent)
@@ -642,7 +643,7 @@ func TestMCPServer_PromptHandling(t *testing.T) {
 				resp, ok := response.(mcp.JSONRPCResponse)
 				assert.True(t, ok)
 
-				result, ok := resp.Result.(*mcp.GetPromptResult)
+				result, ok := resp.Result.(mcp.GetPromptResult)
 				assert.True(t, ok)
 				assert.Len(t, result.Messages, 1)
 				textContent, ok := result.Messages[0].Content.(mcp.TextContent)
@@ -664,12 +665,19 @@ func TestMCPServer_PromptHandling(t *testing.T) {
 }
 
 func TestMCPServer_HandleInvalidMessages(t *testing.T) {
-	server := NewMCPServer("test-server", "1.0.0")
+	var errs []error
+	hooks := &Hooks{}
+	hooks.AddOnError(func(id any, method mcp.MCPMethod, message any, err error) {
+		errs = append(errs, err)
+	})
+
+	server := NewMCPServer("test-server", "1.0.0", WithHooks(hooks))
 
 	tests := []struct {
 		name        string
 		message     string
 		expectedErr int
+		validateErr func(t *testing.T, err error)
 	}{
 		{
 			name:        "Invalid JSON",
@@ -685,6 +693,13 @@ func TestMCPServer_HandleInvalidMessages(t *testing.T) {
 			name:        "Invalid parameters",
 			message:     `{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": "invalid"}`,
 			expectedErr: mcp.INVALID_REQUEST,
+			validateErr: func(t *testing.T, err error) {
+				var unparseableErr = &UnparseableMessageError{}
+				var ok = errors.As(err, &unparseableErr)
+				assert.True(t, ok, "Error should be UnparseableMessageError")
+				assert.Equal(t, mcp.MethodInitialize, unparseableErr.GetMethod())
+				assert.Equal(t, json.RawMessage(`{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": "invalid"}`), unparseableErr.GetMessage())
+			},
 		},
 		{
 			name:        "Missing JSONRPC version",
@@ -695,6 +710,8 @@ func TestMCPServer_HandleInvalidMessages(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			errs = nil // Reset errors for each test case
+
 			response := server.HandleMessage(
 				context.Background(),
 				[]byte(tt.message),
@@ -704,15 +721,44 @@ func TestMCPServer_HandleInvalidMessages(t *testing.T) {
 			errorResponse, ok := response.(mcp.JSONRPCError)
 			assert.True(t, ok)
 			assert.Equal(t, tt.expectedErr, errorResponse.Error.Code)
+
+			if tt.validateErr != nil {
+				require.Len(t, errs, 1, "Expected exactly one error")
+				tt.validateErr(t, errs[0])
+			}
 		})
 	}
 }
 
 func TestMCPServer_HandleUndefinedHandlers(t *testing.T) {
+	var errs []error
+	type beforeResult struct {
+		method  mcp.MCPMethod
+		message any
+	}
+	type afterResult struct {
+		method  mcp.MCPMethod
+		message any
+		result  any
+	}
+	var beforeResults []beforeResult
+	var afterResults []afterResult
+	hooks := &Hooks{}
+	hooks.AddOnError(func(id any, method mcp.MCPMethod, message any, err error) {
+		errs = append(errs, err)
+	})
+	hooks.AddBeforeAny(func(id any, method mcp.MCPMethod, message any) {
+		beforeResults = append(beforeResults, beforeResult{method, message})
+	})
+	hooks.AddOnSuccess(func(id any, method mcp.MCPMethod, message any, result any) {
+		afterResults = append(afterResults, afterResult{method, message, result})
+	})
+
 	server := NewMCPServer("test-server", "1.0.0",
 		WithResourceCapabilities(true, true),
 		WithPromptCapabilities(true),
 		WithToolCapabilities(true),
+		WithHooks(hooks),
 	)
 
 	// Add a test tool to enable tool capabilities
@@ -728,9 +774,10 @@ func TestMCPServer_HandleUndefinedHandlers(t *testing.T) {
 	})
 
 	tests := []struct {
-		name        string
-		message     string
-		expectedErr int
+		name              string
+		message           string
+		expectedErr       int
+		validateCallbacks func(t *testing.T, err error, beforeResults beforeResult)
 	}{
 		{
 			name: "Undefined tool",
@@ -744,6 +791,10 @@ func TestMCPServer_HandleUndefinedHandlers(t *testing.T) {
                     }
                 }`,
 			expectedErr: mcp.INVALID_PARAMS,
+			validateCallbacks: func(t *testing.T, err error, beforeResults beforeResult) {
+				assert.Equal(t, mcp.MethodToolsCall, beforeResults.method)
+				assert.True(t, errors.Is(err, ErrToolNotFound))
+			},
 		},
 		{
 			name: "Undefined prompt",
@@ -757,6 +808,10 @@ func TestMCPServer_HandleUndefinedHandlers(t *testing.T) {
                     }
                 }`,
 			expectedErr: mcp.INVALID_PARAMS,
+			validateCallbacks: func(t *testing.T, err error, beforeResults beforeResult) {
+				assert.Equal(t, mcp.MethodPromptsGet, beforeResults.method)
+				assert.True(t, errors.Is(err, ErrPromptNotFound))
+			},
 		},
 		{
 			name: "Undefined resource",
@@ -769,11 +824,17 @@ func TestMCPServer_HandleUndefinedHandlers(t *testing.T) {
                     }
                 }`,
 			expectedErr: mcp.INVALID_PARAMS,
+			validateCallbacks: func(t *testing.T, err error, beforeResults beforeResult) {
+				assert.Equal(t, mcp.MethodResourcesRead, beforeResults.method)
+				assert.True(t, errors.Is(err, ErrResourceNotFound))
+			},
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			errs = nil // Reset errors for each test case
+			beforeResults = nil
 			response := server.HandleMessage(
 				context.Background(),
 				[]byte(tt.message),
@@ -783,16 +844,31 @@ func TestMCPServer_HandleUndefinedHandlers(t *testing.T) {
 			errorResponse, ok := response.(mcp.JSONRPCError)
 			assert.True(t, ok)
 			assert.Equal(t, tt.expectedErr, errorResponse.Error.Code)
+
+			if tt.validateCallbacks != nil {
+				require.Len(t, errs, 1, "Expected exactly one error")
+				require.Len(t, beforeResults, 1, "Expected exactly one before result")
+				require.Len(t, afterResults, 0, "Expected no after results because these calls generate errors")
+				tt.validateCallbacks(t, errs[0], beforeResults[0])
+			}
 		})
 	}
 }
 
 func TestMCPServer_HandleMethodsWithoutCapabilities(t *testing.T) {
+	var errs []error
+	hooks := &Hooks{}
+	hooks.AddOnError(func(id any, method mcp.MCPMethod, message any, err error) {
+		errs = append(errs, err)
+	})
+	hooksOption := WithHooks(hooks)
+
 	tests := []struct {
 		name        string
 		message     string
 		options     []ServerOption
 		expectedErr int
+		errString   string
 	}{
 		{
 			name: "Tools without capabilities",
@@ -804,8 +880,9 @@ func TestMCPServer_HandleMethodsWithoutCapabilities(t *testing.T) {
                         "name": "test-tool"
                     }
                 }`,
-			options:     []ServerOption{}, // No capabilities at all
+			options:     []ServerOption{hooksOption}, // No capabilities at all
 			expectedErr: mcp.METHOD_NOT_FOUND,
+			errString:   "tools",
 		},
 		{
 			name: "Prompts without capabilities",
@@ -817,8 +894,9 @@ func TestMCPServer_HandleMethodsWithoutCapabilities(t *testing.T) {
                         "name": "test-prompt"
                     }
                 }`,
-			options:     []ServerOption{}, // No capabilities at all
+			options:     []ServerOption{hooksOption}, // No capabilities at all
 			expectedErr: mcp.METHOD_NOT_FOUND,
+			errString:   "prompts",
 		},
 		{
 			name: "Resources without capabilities",
@@ -830,13 +908,16 @@ func TestMCPServer_HandleMethodsWithoutCapabilities(t *testing.T) {
                         "uri": "test-resource"
                     }
                 }`,
-			options:     []ServerOption{}, // No capabilities at all
+			options:     []ServerOption{hooksOption}, // No capabilities at all
 			expectedErr: mcp.METHOD_NOT_FOUND,
+			errString:   "resources",
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			errs = nil // Reset errors for each test case
+
 			server := NewMCPServer("test-server", "1.0.0", tt.options...)
 			response := server.HandleMessage(
 				context.Background(),
@@ -847,6 +928,10 @@ func TestMCPServer_HandleMethodsWithoutCapabilities(t *testing.T) {
 			errorResponse, ok := response.(mcp.JSONRPCError)
 			assert.True(t, ok)
 			assert.Equal(t, tt.expectedErr, errorResponse.Error.Code)
+
+			require.Len(t, errs, 1, "Expected exactly one error")
+			assert.True(t, errors.Is(errs[0], ErrUnsupported), "Error should be ErrUnsupported but was %v", errs[0])
+			assert.Contains(t, errs[0].Error(), tt.errString)
 		})
 	}
 }
@@ -1062,3 +1147,161 @@ func (f fakeSession) NotificationChannel() chan<- mcp.JSONRPCNotification {
 }
 
 var _ ClientSession = fakeSession{}
+
+func TestMCPServer_WithHooks(t *testing.T) {
+	// Create hook counters to verify calls
+	var (
+		beforeAnyCount   int
+		onSuccessCount   int
+		onErrorCount     int
+		beforePingCount  int
+		afterPingCount   int
+		beforeToolsCount int
+		afterToolsCount  int
+	)
+
+	// Collectors for message and result types
+	var beforeAnyMessages []any
+	var onSuccessData []struct {
+		msg any
+		res any
+	}
+	var beforePingMessages []*mcp.PingRequest
+	var afterPingData []struct {
+		msg *mcp.PingRequest
+		res *mcp.EmptyResult
+	}
+
+	// Initialize hook handlers
+	hooks := &Hooks{}
+
+	// Register "any" hooks with type verification
+	hooks.AddBeforeAny(func(id any, method mcp.MCPMethod, message any) {
+		beforeAnyCount++
+		// Only collect ping messages for our test
+		if method == mcp.MethodPing {
+			beforeAnyMessages = append(beforeAnyMessages, message)
+		}
+	})
+
+	hooks.AddOnSuccess(func(id any, method mcp.MCPMethod, message any, result any) {
+		onSuccessCount++
+		// Only collect ping responses for our test
+		if method == mcp.MethodPing {
+			onSuccessData = append(onSuccessData, struct {
+				msg any
+				res any
+			}{message, result})
+		}
+	})
+
+	hooks.AddOnError(func(id any, method mcp.MCPMethod, message any, err error) {
+		onErrorCount++
+	})
+
+	// Register method-specific hooks with type verification
+	hooks.AddBeforePing(func(id any, message *mcp.PingRequest) {
+		beforePingCount++
+		beforePingMessages = append(beforePingMessages, message)
+	})
+
+	hooks.AddAfterPing(func(id any, message *mcp.PingRequest, result *mcp.EmptyResult) {
+		afterPingCount++
+		afterPingData = append(afterPingData, struct {
+			msg *mcp.PingRequest
+			res *mcp.EmptyResult
+		}{message, result})
+	})
+
+	hooks.AddBeforeListTools(func(id any, message *mcp.ListToolsRequest) {
+		beforeToolsCount++
+	})
+
+	hooks.AddAfterListTools(func(id any, message *mcp.ListToolsRequest, result *mcp.ListToolsResult) {
+		afterToolsCount++
+	})
+
+	// Create a server with the hooks
+	server := NewMCPServer(
+		"test-server",
+		"1.0.0",
+		WithHooks(hooks),
+		WithToolCapabilities(true),
+	)
+
+	// Add a test tool
+	server.AddTool(
+		mcp.NewTool("test-tool"),
+		func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+			return &mcp.CallToolResult{}, nil
+		},
+	)
+
+	// Initialize the server
+	_ = server.HandleMessage(context.Background(), []byte(`{
+		"jsonrpc": "2.0",
+		"id": 1,
+		"method": "initialize"
+	}`))
+
+	// Test 1: Verify ping method hooks
+	pingResponse := server.HandleMessage(context.Background(), []byte(`{
+		"jsonrpc": "2.0",
+		"id": 2,
+		"method": "ping"
+	}`))
+
+	// Verify success response
+	assert.IsType(t, mcp.JSONRPCResponse{}, pingResponse)
+
+	// Test 2: Verify tools/list method hooks
+	toolsListResponse := server.HandleMessage(context.Background(), []byte(`{
+		"jsonrpc": "2.0",
+		"id": 3,
+		"method": "tools/list"
+	}`))
+
+	// Verify success response
+	assert.IsType(t, mcp.JSONRPCResponse{}, toolsListResponse)
+
+	// Test 3: Verify error hooks with invalid tool
+	errorResponse := server.HandleMessage(context.Background(), []byte(`{
+		"jsonrpc": "2.0",
+		"id": 4,
+		"method": "tools/call",
+		"params": {
+			"name": "non-existent-tool"
+		}
+	}`))
+
+	// Verify error response
+	assert.IsType(t, mcp.JSONRPCError{}, errorResponse)
+
+	// Verify hook counts
+
+	// Method-specific hooks should be called exactly once
+	assert.Equal(t, 1, beforePingCount, "beforePing should be called once")
+	assert.Equal(t, 1, afterPingCount, "afterPing should be called once")
+	assert.Equal(t, 1, beforeToolsCount, "beforeListTools should be called once")
+	assert.Equal(t, 1, afterToolsCount, "afterListTools should be called once")
+
+	// General hooks should be called for all methods
+	// beforeAny is called for all 4 methods (initialize, ping, tools/list, tools/call)
+	assert.Equal(t, 4, beforeAnyCount, "beforeAny should be called for each method")
+	// onSuccess is called for all 3 success methods (initialize, ping, tools/list)
+	assert.Equal(t, 3, onSuccessCount, "onSuccess should be called after all successful invocations")
+
+	// Error hook should be called once for the failed tools/call
+	assert.Equal(t, 1, onErrorCount, "onError should be called once")
+
+	// Verify type matching between BeforeAny and BeforePing
+	require.Len(t, beforePingMessages, 1, "Expected one BeforePing message")
+	require.Len(t, beforeAnyMessages, 1, "Expected one BeforeAny Ping message")
+	assert.IsType(t, beforePingMessages[0], beforeAnyMessages[0], "BeforeAny message should be same type as BeforePing message")
+
+	// Verify type matching between OnSuccess and AfterPing
+	require.Len(t, afterPingData, 1, "Expected one AfterPing message/result pair")
+	require.Len(t, onSuccessData, 1, "Expected one OnSuccess Ping message/result pair")
+	assert.IsType(t, afterPingData[0].msg, onSuccessData[0].msg, "OnSuccess message should be same type as AfterPing message")
+	assert.IsType(t, afterPingData[0].res, onSuccessData[0].res, "OnSuccess result should be same type as AfterPing result")
+}
diff --git a/server/sse_test.go b/server/sse_test.go
index 7ce113e..ced76e4 100644
--- a/server/sse_test.go
+++ b/server/sse_test.go
@@ -5,6 +5,7 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"math/rand"
 	"net/http"
 	"net/http/httptest"
 	"strings"
@@ -29,9 +30,9 @@ func TestSSEServer(t *testing.T) {
 		if sseServer.server == nil {
 			t.Error("MCPServer should not be nil")
 		}
-		if sseServer.baseURL != "http://localhost:8080/mcp" {
+		if sseServer.baseURL != "http://localhost:8080" {
 			t.Errorf(
-				"Expected baseURL http://localhost:8080/mcp, got %s",
+				"Expected baseURL http://localhost:8080, got %s",
 				sseServer.baseURL,
 			)
 		}
@@ -350,7 +351,7 @@ func TestSSEServer(t *testing.T) {
 		sseServer := NewSSEServer(mcpServer)
 
 		mux := http.NewServeMux()
-		mux.Handle("/mcp/", http.StripPrefix("/mcp", sseServer))
+		mux.Handle("/mcp/", sseServer)
 
 		ts := httptest.NewServer(mux)
 		defer ts.Close()
@@ -614,4 +615,48 @@ func TestSSEServer(t *testing.T) {
 		}
 	})
 
+	t.Run("SSEOption should not have negative effects when used repeatedly but should always remain consistent.", func(t *testing.T) {
+		mcpServer := NewMCPServer("test", "1.0.0")
+		basePath := "/mcp-test"
+		baseURL := "http://localhost:8080/test"
+		messageEndpoint := "/message-test"
+		sseEndpoint := "/sse-test"
+		srv := &http.Server{}
+		rands := []SSEOption{
+			WithBasePath(basePath),
+			WithBaseURL(baseURL),
+			WithMessageEndpoint(messageEndpoint),
+			WithSSEEndpoint(sseEndpoint),
+			WithHTTPServer(srv),
+		}
+		for i := 0; i < 100; i++ {
+
+			var options []SSEOption
+			for i2 := 0; i2 < 100; i2++ {
+				index := rand.Intn(len(rands))
+				options = append(options, rands[index])
+			}
+			sseServer := NewSSEServer(mcpServer, options...)
+
+			if sseServer.basePath != basePath {
+				t.Fatalf("basePath %v, got: %v", basePath, sseServer.basePath)
+			}
+
+			if sseServer.baseURL != baseURL {
+				t.Fatalf("baseURL %v, got: %v", baseURL, sseServer.baseURL)
+			}
+
+			if sseServer.sseEndpoint != sseEndpoint {
+				t.Fatalf("sseEndpoint %v, got: %v", sseEndpoint, sseServer.sseEndpoint)
+			}
+
+			if sseServer.messageEndpoint != messageEndpoint {
+				t.Fatalf("messageEndpoint  %v, got: %v", messageEndpoint, sseServer.messageEndpoint)
+			}
+
+			if sseServer.srv != srv {
+				t.Fatalf("srv  %v, got: %v", srv, sseServer.srv)
+			}
+		}
+	})
 }