Commit acf22a8
feat(middleware):implement Middleware Architecture
This Pull Request introduces a pluggable middleware architecture to the `trpc-mcp-go` server, inspired by the robust Filter pattern in `tRPC-Go`. This architecture provides a powerful mechanism for handling cross-cutting concerns in a modular and reusable way.
In addition to the core engine, this PR delivers three fundamental and critical middleware implementations: `Recovery`, `Logging`, and `Metrics`, laying a solid foundation for future functional extensions.
Notably, this integration effort also drove significant improvements to the core engine. We identified and fixed several key underlying bugs and ultimately established a clear, sustainable development model for future contributors to reference.
---
- **Pluggable Architecture**: Implemented a `MiddlewareChain` that wraps the final request handler in an "onion model," allowing requests to flow sequentially through a series of middlewares before reaching the business logic.
- **Seamless Integration**: The engine is directly integrated into `mcpHandler.handleRequest`, ensuring that all incoming requests automatically pass through the middleware chain.
- **Robustness**: Added a defensive null pointer check in `handler.go` to prevent panics when the `server` is not initialized (e.g., in `SSEServer` tests), ensuring backward compatibility.
- **Panic Safety**: Catches panics occurring anywhere in the request lifecycle (including other middlewares and the final handler).
- **Standard-Compliant Error Response**: Returns a clean, JSON-RPC 2.0 compliant `Internal Server Error` response, avoiding the exposure of stack traces or other internal details to the client.
- **Highly Configurable**: Supports advanced options such as custom loggers, stack trace control, and panic filters.
- **Usage**:
The `Recovery` middleware should be registered as the first (outermost) middleware to ensure it can catch all subsequent panics.
```go
// Default usage for basic panic recovery
s.Use(middlewares.Recovery())
// Advanced usage, e.g., with a custom error response
s.Use(middlewares.RecoveryWithOptions(
middlewares.WithCustomErrorResponse(func(ctx context.Context, req *mcp.JSONRPCRequest, panicErr interface{}) mcp.JSONRPCMessage {
// Return a more user-friendly, custom error message
return mcp.NewJSONRPCErrorResponse(
req.ID,
mcp.ErrCodeInternal,
"Service is temporarily unavailable. Please try again later.",
nil,
)
}),
))
```
- **Structured Logging**: Provides structured, leveled logging for the entire request lifecycle (request start, request end, request failure).
- **Adheres to Project Standards**: Refactored to fully adapt to the project's standard `mcp.Logger` interface, making it a "good citizen" within the ecosystem.
- **Rich Context**: Supports configurable logging of request/response payloads and can extract custom fields from the `context` to be included in logs.
- **Usage**:
This middleware offers multiple configuration options for flexible logging.
```go
// Import the project's standard logger
logger := mcp.GetDefaultLogger()
// Register the middleware, enabling all request logs and payload recording
s.Use(middlewares.NewLoggingMiddleware(logger,
// The default is to only log errors; this option logs all requests
middlewares.WithShouldLog(func(level logging.Level, duration time.Duration, err error) bool {
return true
}),
// Log the detailed content of requests and responses
middlewares.WithPayloadLogging(true),
))
```
- **Based on OpenTelemetry**: Built on the OpenTelemetry standard, ensuring broad compatibility with modern observability platforms (like Prometheus, Jaeger).
- **Core Metrics**: Provides out-of-the-box monitoring of core metrics:
- `mcp.server.requests.count`: Total number of requests
- `mcp.server.errors.count`: Number of failed requests
- `mcp.server.request.latency`: Request latency histogram
- `mcp.server.requests.in_flight`: Number of in-flight requests
- **Usage**:
The `metrics` middleware depends on an `OpenTelemetry Collector` and `Prometheus`. We have provided a `docker-compose.yaml` file in the `examples/middlewares/metrics/` directory to start all dependencies with a single command.
**Verification Steps**:
1. `cd examples/middlewares/metrics/`
2. `docker-compose up -d`
3. Run the `metrics_integration_main.go` example and send some requests.
4. Access `http://localhost:9090` to query the above core metrics in the Prometheus UI.
---
**Status**: All changes have been rebased on the latest `main` branch (`dd0bd82e69c7ae947a66059264c20940f46a4eb5`) in the `feat/middleware-final` branch. All tests pass, ensuring this PR can be merged without conflicts.
We not only added middleware functionality but also made necessary improvements and fixes to the core engine during the integration process.
- **Symptom**: In the early stages of development, we lacked a standard way to independently test middlewares.
- **Reason**: Testing middlewares requires simulating `request`, `session`, and `next` functions, which is tedious to write manually.
- **Solution**: We created a new `mcptest` public testing package, providing `RunMiddlewareTest` and `CheckMiddlewareFunc` helper functions. This greatly simplified the unit testing of middlewares and enabled parallel development within the team.
- **Symptom**: E2E tests exposed a `nil pointer dereference` panic after middleware integration.
- **Reason**: `SSEServer` did not provide a `server` instance when creating `mcpHandler`, causing the program to crash when accessing `h.server.middlewares`.
- **Solution**:
- Implemented the `MiddlewareChain` core engine in `middleware.go`.
- Integrated the engine into the `handleRequest` method of `handler.go` and added a null pointer check for `handler.server`.
- **Impact**: This change elegantly resolved the panic with minimal intrusion and without modifying any test files, ensuring backward compatibility. The scope of the change is limited to the `handleRequest` function.
- **Symptom**: When a middleware returned a `*JSONRPCError`, the HTTP handler would incorrectly wrap it within the `result` field, violating the JSON-RPC 2.0 specification.
- **Reason**: The `handlePostRequest` function incorrectly assumed that all returns from `mcpHandler` were successful results.
- **Solution**: We introduced a type check in `handlePostRequest`. If the response type is already `*JSONRPCError`, it will be sent directly to the client.
- **Impact**: This change is limited to the `handlePostRequest` function and ensures the correctness of error handling.
- **Symptom**: The server would silently discard all requests to the root path (`/`) when no path was explicitly configured.
- **Reason**: A logic flaw in the `isValidPath` check.
- **Solution**: Although this fix was ultimately applied in the integration tests (via `mcp.WithServerPath("")`), identifying this behavior was crucial for correctly documenting the server's usage.
- **Impact**: No code change, but contributed important "best practice" documentation to the project.
- **Problem**: The internal function for creating error responses (`newJSONRPCErrorResponse`) was not exported and was used inconsistently throughout the codebase.
- **Solution**: We performed a global refactoring, renaming it to `NewJSONRPCErrorResponse` and unifying all call sites.
- **Impact**: This was a safe refactoring guaranteed by Go's compile-time checks. It affected multiple `manager_*.go` and `server_*.go` files but was limited to function call modifications and did not alter any business logic.
---
This large-scale feature integration allowed us to establish and document a set of best practices for future development.
We established a clear workflow:
1. **Write Tests First**: For any new middleware, start by writing a minimal, end-to-end integration test in `examples/middleware_usage/server/`.
2. **Let Tests Drive Development**: Run the test and let compiler and runtime errors guide all necessary fixes, refactoring, and adaptations.
This model proved to be invaluable in ensuring quality and accelerating development.
We adopted a two-tier convention for organizing example code:
- **Simple Middlewares**: Single-file, self-contained middlewares are placed directly in `examples/middlewares/`.
- **Complex Middlewares**: Modules with multiple files, documentation, and configurations (like `metrics`) are given their own subdirectories and separate packages (e.g., `examples/middlewares/metrics/`) to maintain high cohesion.
---
The following is a brief example demonstrating how to configure all three new middlewares on a single server:
```go
package main
import (
"context"
"fmt"
"net/http"
mcp "trpc.group/trpc-go/trpc-mcp-go"
"trpc.group.com/trpc-go/trpc-mcp-go/examples/middlewares"
metricmw "trpc.group.com/trpc-go/trpc-mcp-go/examples/middlewares/metrics"
)
func main() {
// 1. Create a new server instance
s := mcp.NewServer(
"my-server", "1.0.0",
mcp.WithStatelessMode(true),
mcp.WithServerPath(""),
)
// 2. Set up and register the Metrics middleware
rec, shutdown, _ := metricmw.NewOtelMetricsRecorder()
defer func() { _ = shutdown(context.Background()) }()
s.Use(metricmw.NewMetricsMiddleware(metricmw.WithRecorder(rec)))
// 3. Register the Logging and Recovery middlewares
// Note: The Recovery middleware should generally be the first (outermost) middleware
s.Use(middlewares.Recovery())
s.Use(middlewares.NewLoggingMiddleware(mcp.GetDefaultLogger()))
// 4. Register your business tools...
// s.RegisterTool(...)
// 5. Start the server
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", s.Handler())
}
```
Co-authored-by: Wang jia <RangelJara195@gmail.com>
Co-authored-by: Lin Yuze <jadeproheshan@gmail.com>
Co-authored-by: Chang Mingyue <xmkdgdz@foxmail.com>
Co-authored-by: Chen Lei <123213112a@gmail.com>1 parent dd0bd82 commit acf22a8
File tree
33 files changed
+2979
-90
lines changed- docs
- examples
- middleware_usage
- logging_example
- metrics_example
- recovery_example
- middlewares
- metrics
- mcptest
33 files changed
+2979
-90
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| 13 | + | |
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
| |||
80 | 81 | | |
81 | 82 | | |
82 | 83 | | |
83 | | - | |
| 84 | + | |
84 | 85 | | |
85 | 86 | | |
86 | 87 | | |
| |||
168 | 169 | | |
169 | 170 | | |
170 | 171 | | |
171 | | - | |
| 172 | + | |
172 | 173 | | |
173 | 174 | | |
174 | 175 | | |
| |||
204 | 205 | | |
205 | 206 | | |
206 | 207 | | |
207 | | - | |
| 208 | + | |
208 | 209 | | |
209 | 210 | | |
210 | 211 | | |
211 | 212 | | |
212 | 213 | | |
213 | | - | |
| 214 | + | |
| 215 | + | |
214 | 216 | | |
215 | 217 | | |
216 | 218 | | |
| |||
250 | 252 | | |
251 | 253 | | |
252 | 254 | | |
253 | | - | |
| 255 | + | |
254 | 256 | | |
255 | 257 | | |
256 | 258 | | |
| |||
353 | 355 | | |
354 | 356 | | |
355 | 357 | | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
356 | 468 | | |
357 | 469 | | |
358 | 470 | | |
| |||
735 | 847 | | |
736 | 848 | | |
737 | 849 | | |
738 | | - | |
739 | | - | |
| 850 | + | |
| 851 | + | |
740 | 852 | | |
741 | 853 | | |
742 | 854 | | |
743 | | - | |
744 | | - | |
| 855 | + | |
| 856 | + | |
745 | 857 | | |
746 | 858 | | |
747 | 859 | | |
| |||
0 commit comments