Skip to content

Conversation

@jadeproheshan
Copy link

@jadeproheshan jadeproheshan commented Sep 10, 2025

本次 Pull Request 为 trpc-mcp-go 服务器引入了一套可插拔的中间件架构,其设计灵感来源于 tRPC-Go 中经过验证的 Filter 模式。该架构为以模块化、可复用的方式处理横切关注点提供了强大的支持。

除了核心引擎外,本次 PR 还交付了三个基础且关键的中间件实现:Recovery (异常恢复)、Logging (日志记录) 和 Metrics (指标监控),为未来的功能扩展奠定了坚实的基础。


1. 新增功能:核心特性与实现

a. 中间件引擎 (middleware.go, handler.go)

  • 可插拔架构: 实现了一个 MiddlewareChain (中间件链),它以“洋葱模型”将最终的请求处理器包裹起来,允许请求在触达业务逻辑前,按顺序流经一系列中间件。
  • 无缝集成: 引擎被直接集成到 mcpHandler.handleRequest 中,确保了所有到来的请求都能自动地通过中间件链进行处理。
  • 健壮性: 在 handler.go 中增加了防御性的空指针检查,以防止在 server 未初始化的情况下(例如在 SSEServer 的测试中)产生 panic,保证了向后兼容性。

b. Recovery 中间件 (examples/middlewares/recovery.go)

  • Panic 安全: 能捕获在请求生命周期内(包括其他中间件和最终处理器)任何地方发生的 panic
  • 规范的错误响应: 返回一个干净的、符合 JSON-RPC 2.0 规范的 Internal Server Error 响应,避免了向客户端暴露堆栈跟踪等内部细节。
  • 高可配置性: 支持自定义日志器、堆栈跟踪控制、以及 panic 过滤器等高级选项。
  • 使用说明:
    Recovery 中间件应作为第一个(最外层的)中间件来注册,以确保它能捕获后续所有环节的 panic。
    // 默认用法,提供基础的 panic 恢复
    s.Use(middlewares.Recovery())
    
    // 高级用法,例如自定义错误响应
    s.Use(middlewares.RecoveryWithOptions(
        middlewares.WithCustomErrorResponse(func(ctx context.Context, req *mcp.JSONRPCRequest, panicErr interface{}) mcp.JSONRPCMessage {
            // 返回一个对用户更友好的、自定义的错误信息
            return mcp.NewJSONRPCErrorResponse(
                req.ID,
                mcp.ErrCodeInternal,
                "服务暂时不可用,请稍后重试",
                nil,
            )
        }),
    ))

c. Logging 中间件 (examples/middlewares/logging.go)

日志中间件为 tRPC-MCP-Go 服务器提供请求/响应日志记录功能,支持结构化日志、自定义过滤和多种输出格式。该中间件设计用于帮助开发者监控服务器请求、调试问题以及分析性能。

功能特性

  • 多级日志支持: 支持 DEBUG, INFO, WARN, ERROR, FATAL 五个日志级别
  • 结构化字段记录: 支持键值对格式的结构化日志记录
  • 请求/响应负载记录: 可选择记录完整的请求和响应数据
  • 自定义日志过滤: 基于日志级别、执行时间和错误状态的自定义规则进行智能过滤
  • 终端颜色支持: 自动检测终端并启用彩色输出
  • 上下文字段提取: 从请求上下文中提取自定义字段
  • 可扩展性: 支持自定义 Logger 实现
    该中间件提供多个配置选项,允许进行灵活的日志记录。
    // 引入项目标准 logger
    logger := mcp.GetDefaultLogger()
    
    // 注册中间件,并启用所有请求日志和载荷记录
    s.Use(middlewares.NewLoggingMiddleware(logger,
        // 默认只记录错误,此选项会记录所有请求
        middlewares.WithShouldLog(func(level logging.Level, duration time.Duration, err error) bool {
            return true 
        }),
        // 记录请求和响应的详细内容
        middlewares.WithPayloadLogging(true),
    ))

d. Metrics 中间件 (examples/middlewares/metrics/)

  • 基于 OpenTelemetry: 构建于 OpenTelemetry 标准之上,与现代可观测性平台(如 Prometheus, Jaeger)有广泛的兼容性。
  • 核心指标: 提供开箱即用的核心指标监控:
    • mcp.server.requests.count: 请求总数
    • mcp.server.errors.count: 失败请求数
    • mcp.server.request.latency: 请求延迟直方图
    • mcp.server.requests.in_flight: 在途请求数
  • 使用说明:
    metrics 中间件依赖 OpenTelemetry CollectorPrometheus。我们已在 examples/middlewares/metrics/ 目录下提供了 docker-compose.yaml 文件,用于一键启动所有依赖。
    验证步骤:
    1. cd examples/middlewares/metrics/
    2. docker-compose up -d
    3. 运行 metrics_integration_main.go 示例并发送一些请求。
    4. 访问 http://localhost:9090,在 Prometheus UI 中查询以上核心指标。

2. 对原有代码的更改

状态: 所有改动已在 feat/middleware-final 分支上,基于最新的 main 分支 (dd0bd8) 完成变基。经测试,所有功能正常,可确保本次 PR 无冲突合入。

a. 新增 mcptest 测试工具包

  • 现象: 在开发初期,我们缺乏一种标准方式来独立测试中间件。
  • 原因: 中间件的测试需要模拟 request, sessionnext 函数,手动编写很繁琐。
  • 解决方案: 我们创建了一个新的 mcptest 公共测试包,提供了 RunMiddlewareTestCheckMiddlewareFunc 两个辅助函数,极大地简化了中间件的单元测试编写。

b. [修复] handler.go: 集成中间件引擎并增强健壮性

  • 现象: e2e 测试在集成中间件后,暴露出 nil pointer dereference 的 panic。
  • 原因: SSEServer 在创建 mcpHandler 时未提供 server 实例,导致访问 h.server.middlewares 时程序崩溃。
  • 解决方案:
    • middleware.go 中实现了 MiddlewareChain 核心引擎。
    • handler.gohandleRequest 方法中集成了该引擎,并增加了对 handler.server 的空指针检查。
    • 影响: 此改动以最小的侵入性,在不修改任何测试文件的前提下,解决了 panic 问题,保证了向后兼容性。改动范围仅限于 handleRequest 函数内部。

c. [修复] streamable_server.go: 不合规的错误响应嵌套问题

  • 现象: 当中间件返回一个 *JSONRPCError 时,HTTP 处理器会错误地将其包裹在 result 字段内,违反了 JSON-RPC 2.0 规范。
  • 原因: handlePostRequest 函数错误地假设了所有来自 mcpHandler 的返回都是成功的结果。
  • 解决方案: 我们在 handlePostRequest 中引入了类型检查。如果响应的类型已经是 *JSONRPCError,它将被直接发送给客户端。
    • 影响: 此改动范围仅限于 handlePost-Request 函数,确保了错误处理的规范性。

d. [修复] 根路径请求被静默丢弃的问题

  • 现象: 服务器在未显式配置路径时,会静默地丢弃所有发往根路径 (/) 的请求。
  • 原因: isValidPath 检查中的一个逻辑缺陷。
  • 解决方案: 虽然此修复最终应用在集成测试中(通过 mcp.WithServerPath("")),但识别出这一行为对于正确地文档化服务器的用法至关重要。

e. [修复] 全局统一错误处理

  • 问题: 用于创建错误响应的内部函数 (newJSONRPCErrorResponse) 未被导出,且在代码库中被不一致地使用。
  • 解决方案: 我们进行了一次全局重构,将其重命名为 NewJSONRPCErrorResponse,并统一了所有调用点。
    • 影响: 这是一次由 Go 的编译时检查所保证的安全重构,涉及多个 manager_*.goserver_*.go 文件,但仅限于函数调用层面的修改,未改变任何业务逻辑。

3.中间件存放约定

我们采纳了一套双层约定来组织示例代码:

  • 简单中间件: 单文件的、自包含的中间件,直接存放在 examples/middlewares/
  • 复杂中间件: 包含多个文件、文档和配置的模块(如 metrics),则拥有自己的子目录和独立的包(如 examples/middlewares/metrics/),以保持其高内聚性。

4. 如何使用

以下是一个简短的示例,展示了如何在一个服务器上同时配置这三个新的中间件:

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. 创建一个新的服务器实例
    s := mcp.NewServer(
        "my-server", "1.0.0",
        mcp.WithStatelessMode(true),
        mcp.WithServerPath(""),
    )

    // 2. 设置并注册 Metrics 中间件
    rec, shutdown, _ := metricmw.NewOtelMetricsRecorder()
    defer func() { _ = shutdown(context.Background()) }()
    s.Use(metricmw.NewMetricsMiddleware(metricmw.WithRecorder(rec)))

    // 3. 注册 Logging 和 Recovery 中间件
    // 注意: Recovery 中间件通常应该作为第一个(最外层的)中间件
    s.Use(middlewares.Recovery())
    s.Use(middlewares.NewLoggingMiddleware(mcp.GetDefaultLogger()))

    // 4. 注册你的业务工具...
    // s.RegisterTool(...)

    // 5. 启动服务器
    fmt.Println("Server starting on :8080")
    http.ListenAndServe(":8080", s.Handler())
}

Co-authored-by:
middleware-core: jadeproheshan https://github.com/jadeproheshan
middleware-metrics: no-regret666 https://github.com/no-regret666
middleware-recovery: xmkdgdz https://github.com/xmkdgdz
middleware-log: et21ff https://github.com/et21ff

@jadeproheshan jadeproheshan force-pushed the feat/middleware branch 2 times, most recently from 5dc8c33 to 87af127 Compare September 10, 2025 14:28
@jadeproheshan jadeproheshan changed the title [功能] 中间件架构与核心实现 [feat] 中间件架构与核心实现 Sep 10, 2025
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant