You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/08/14 00:44:56 UTC

(skywalking-mcp) branch main updated: feat: migrate to the official MCP Go SDK with 2026-07-28 protocol support (#44)

This is an automated email from the ASF dual-hosted git repository.

wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-mcp.git


The following commit(s) were added to refs/heads/main by this push:
     new ea491e1  feat: migrate to the official MCP Go SDK with 2026-07-28 protocol support (#44)
ea491e1 is described below

commit ea491e1e0510d7fef2fac360ae80f38147e39baf
Author: Zixin Zhou <zh...@apache.org>
AuthorDate: Fri Aug 14 08:44:51 2026 +0800

    feat: migrate to the official MCP Go SDK with 2026-07-28 protocol support (#44)
---
 .github/workflows/CI.yaml                          |   4 +-
 .github/workflows/publish-binaries.yaml            |   2 +-
 .golangci.yml                                      | 109 +++++---
 CHANGES.md                                         |  16 ++
 CLAUDE.md                                          |  36 +--
 Dockerfile                                         |   2 +-
 Makefile                                           |  12 +-
 README.md                                          |  26 ++
 cmd/skywalking-mcp/main.go                         |  23 +-
 dist/LICENSE                                       |  24 +-
 ...=> license-github.com-google-jsonschema-go.txt} |  16 +-
 .../license-github.com-mailru-easyjson.txt         |   7 -
 ...nse-github.com-modelcontextprotocol-go-sdk.txt} |  59 ++--
 ...er.txt => license-github.com-segmentio-asm.txt} |   2 +-
 ...t => license-github.com-segmentio-encoding.txt} |   2 +-
 ...list-go.txt => license-golang.org-x-oauth2.txt} |   4 +-
 ...ogle-uuid.txt => license-golang.org-x-sync.txt} |   4 +-
 ...c-list-go.txt => license-golang.org-x-time.txt} |   4 +-
 dist/licenses/license-gopkg.in-yaml.v3.txt         |  50 ----
 go.mod                                             |  17 +-
 go.sum                                             |  32 ++-
 internal/config/config.go                          |  13 +
 internal/prompts/analysis.go                       |  38 ++-
 internal/prompts/registry.go                       |  57 ++--
 internal/prompts/trace.go                          |  29 +-
 internal/prompts/utility.go                        |  28 +-
 internal/resources/mqe_ai_prompt.md                | 249 -----------------
 internal/resources/mqe_detailed_syntax.md          | 308 ---------------------
 internal/resources/mqe_docs.go                     | 126 ---------
 internal/resources/mqe_examples.json               | 136 ---------
 internal/swmcp/cors.go                             |  20 +-
 internal/swmcp/cors_test.go                        |  33 ++-
 internal/swmcp/server.go                           | 119 +++++---
 internal/swmcp/server_registry_test.go             | 227 +++++----------
 internal/swmcp/server_test.go                      |  82 +++---
 internal/swmcp/sse.go                              | 108 ++++----
 internal/swmcp/sse_test.go                         |  92 ++++++
 internal/swmcp/stdio.go                            |  62 +++--
 internal/swmcp/stdio_test.go                       |  90 ++++++
 internal/swmcp/streamable.go                       | 112 +++++---
 internal/swmcp/streamable_test.go                  | 115 ++++++++
 internal/swmcp/transport_test.go                   | 195 +++++++++++++
 internal/tools/alarm.go                            |  73 ++---
 internal/tools/alarm_test.go                       |  40 +++
 internal/tools/event.go                            |  78 +++---
 internal/tools/io.go                               |  96 -------
 internal/tools/log.go                              |  93 +++----
 internal/tools/metadata.go                         | 261 +++++++++--------
 internal/tools/mqe.go                              | 200 +++++++------
 internal/tools/mqe_test.go                         |  31 ++-
 internal/tools/result.go                           | 119 ++++++++
 internal/tools/result_test.go                      |  41 +++
 internal/tools/tools.go                            | 102 -------
 internal/tools/topology.go                         | 184 ++++++------
 internal/tools/trace.go                            | 151 ++++------
 internal/tools/trace_test.go                       |   9 +-
 56 files changed, 1933 insertions(+), 2235 deletions(-)

diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml
index 1e7e362..d8dff49 100644
--- a/.github/workflows/CI.yaml
+++ b/.github/workflows/CI.yaml
@@ -44,7 +44,7 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v5
         with:
-          go-version: "1.25"
+          go-version: "1.26"
 
       - name: Check Dependencies License
         uses: apache/skywalking-eyes/dependency@69f34abb75ec4e414b593ac3f34228b60e33f97b
@@ -62,7 +62,7 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v5
         with:
-          go-version: 1.25
+          go-version: 1.26
 
       - name: Lint
         run: make lint
diff --git a/.github/workflows/publish-binaries.yaml b/.github/workflows/publish-binaries.yaml
index 9402546..4a272fb 100644
--- a/.github/workflows/publish-binaries.yaml
+++ b/.github/workflows/publish-binaries.yaml
@@ -43,7 +43,7 @@ jobs:
       - name: Set up Go
         uses: actions/setup-go@v5
         with:
-          go-version: 1.25
+          go-version: 1.26
       - name: Build binary
         env:
           GOOS: ${{ matrix.goos }}
diff --git a/.golangci.yml b/.golangci.yml
index 2e10981..1f0d791 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -14,63 +14,86 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-linters-settings:
-  govet:
-    enable:
-      - shadow
-  gocyclo:
-    min-complexity: 15
-  dupl:
-    threshold: 200
-  goconst:
-    min-len: 2
-    min-occurrences: 2
-  misspell:
-    locale: US
-  lll:
-    line-length: 150
-  goimports:
-    local-prefixes: github.com/apache/skywalking-mcp
-  gocritic:
-    enabled-tags:
-      - diagnostic
-      - experimental
-      - opinionated
-      - performance
-      - style
-    disabled-checks:
-      - ifElseChain
-  funlen:
-    lines: 100
-    statements: 50
-  whitespace:
-    multi-if: false
-    multi-func: false
-
+version: "2"
 linters:
   enable:
     - bodyclose
     - dogsled
     - dupl
-    - errcheck
     - funlen
     - goconst
     - gocritic
     - gocyclo
-    - gofmt
-    - goimports
-    - revive
     - gosec
-    - gosimple
-    - govet
-    - ineffassign
     - lll
     - misspell
     - nakedret
+    - revive
     - staticcheck
-    - stylecheck
-    - typecheck
     - unconvert
     - unparam
-    - unused
     - whitespace
+  settings:
+    dupl:
+      threshold: 200
+    funlen:
+      lines: 100
+      statements: 50
+    goconst:
+      min-len: 2
+      min-occurrences: 2
+      # Tool and schema property names, which mirror the struct json tags.
+      ignore-strings: ^[a-z][a-z0-9_]*$
+    gocritic:
+      disabled-checks:
+        - ifElseChain
+        # Tool handlers take their input struct by value: the signature is
+        # fixed by the SDK's ToolHandlerFor, so it cannot take a pointer.
+        - hugeParam
+      enabled-tags:
+        - diagnostic
+        - experimental
+        - opinionated
+        - performance
+        - style
+    gocyclo:
+      min-complexity: 15
+    govet:
+      enable:
+        - shadow
+    lll:
+      line-length: 150
+    misspell:
+      locale: US
+    whitespace:
+      multi-if: false
+      multi-func: false
+  exclusions:
+    generated: lax
+    presets:
+      - comments
+      - common-false-positives
+      - legacy
+      - std-error-handling
+    rules:
+      - path: _test\.go
+        linters:
+          - goconst
+    paths:
+      - third_party$
+      - builtin$
+      - examples$
+formatters:
+  enable:
+    - gofmt
+    - goimports
+  settings:
+    goimports:
+      local-prefixes:
+        - github.com/apache/skywalking-mcp
+  exclusions:
+    generated: lax
+    paths:
+      - third_party$
+      - builtin$
+      - examples$
diff --git a/CHANGES.md b/CHANGES.md
index ccc8232..1fee857 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -2,6 +2,22 @@
 
 Release Notes.
 
+## 0.3.0
+
+### Features
+
+* Migrated to the official `modelcontextprotocol/go-sdk`, adding support for MCP protocol version `2026-07-28`. Clients on earlier revisions keep working: the server answers both the per-request `_meta` model and the legacy `initialize` handshake on the same endpoint.
+* Tool input schemas are now inferred from their Go request types instead of being declared separately, so a schema can no longer drift from the struct it describes.
+
+### Changes
+
+* Removed the MQE documentation resources. The static documents were rarely read by clients, and `mqe://metrics/available` duplicated the `list_mqe_metrics` tool.
+* `--log-command` now records the JSON-RPC message log provided by the SDK; its output format differs from previous releases. Sensitive-field redaction is preserved.
+* Required tool parameters are now enforced: a call missing a required argument (for example `list_instances` without `start`) returns a validation error instead of silently falling back to a default time window.
+* The HTTP transports now reject loopback requests carrying a non-loopback `Host`, as DNS rebinding protection; a reverse proxy that preserves the public `Host` needs the new `--disable-localhost-protection`.
+* Fixed `--allowed-origins` being ignored by the `sse` transport.
+* Requires Go 1.26 to build.
+
 ## 0.2.0
 
 ### Features
diff --git a/CLAUDE.md b/CLAUDE.md
index ed610e0..c46d31a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -4,7 +4,7 @@ This file provides guidance for AI assistants working with the Apache SkyWalking
 
 ## Project Overview
 
-Apache SkyWalking MCP — an MCP (Model Context Protocol) server that bridges AI agents with Apache SkyWalking OAP via GraphQL. It exposes SkyWalking's observability data (traces, logs, metrics, topology, alarms, events) as MCP tools, prompts, and resources. Binary name: `swmcp`.
+Apache SkyWalking MCP — an MCP (Model Context Protocol) server that bridges AI agents with Apache SkyWalking OAP via GraphQL. It exposes SkyWalking's observability data (traces, logs, metrics, topology, alarms, events) as MCP tools and prompts. Binary name: `swmcp`.
 
 ## Repository Structure
 
@@ -15,8 +15,7 @@ skywalking-mcp/
 │   ├── config/               # Config structs for each transport mode
 │   ├── swmcp/                # MCP server factory + transport adapters (stdio/sse/streamable)
 │   ├── tools/                # MCP tool implementations (16 tools, grouped by domain)
-│   ├── prompts/              # MCP prompt definitions (10 prompts, three groups)
-│   └── resources/            # MCP resources (embedded MQE docs + dynamic metrics)
+│   └── prompts/              # MCP prompt definitions (10 prompts, three groups)
 └── dist/                     # Distribution license files
 ```
 
@@ -49,7 +48,7 @@ Basic auth is configured via `--sw-username` / `--sw-password` flags. The startu
 
 TLS verification is enforced by default. Use `--sw-insecure` to skip verification (development/self-signed certs only).
 
-Each transport injects the OAP URL, insecure flag, and auth into the request context via `WithSkyWalkingURLAndInsecure()` and `WithSkyWalkingAuth()`. Tools extract them downstream using `skywalking-cli`'s `contextkey.BaseURL{}`, `contextkey.Insecure{}`, `contextkey.Username{}`, and `contextkey.Password{}`.
+`skyWalkingContextMiddleware()` injects the OAP URL, insecure flag, and auth into the request context via `WithSkyWalkingURLAndInsecure()` and `WithSkyWalkingAuth()`. It is registered with `AddReceivingMiddleware` and therefore covers every transport. Tools extract the values downstream using `skywalking-cli`'s `contextkey.BaseURL{}`, `contextkey.Insecure{}`, `contextkey.Username{}`, and `contextkey.Password{}`. The middleware deliberately reads only the configuration, never request head [...]
 
 ### CORS / CSRF (`internal/swmcp/cors.go`)
 
@@ -57,11 +56,17 @@ Each transport injects the OAP URL, insecure flag, and auth into the request con
 
 ### Server Wiring (`internal/swmcp/server.go`)
 
-`newMCPServer()` is the central registration point — it creates the MCP server and calls all `Add*Tools()`, `Add*Resources()`, and `Add*Prompts()` functions. New capabilities must be registered here.
+`newMCPServer()` is the central registration point — it creates the MCP server and calls all `Add*Tools()` and `Add*Prompts()` functions. New capabilities must be registered here.
 
-### Generic Tool Framework (`internal/tools/tools.go`)
+### Tool Schemas (`internal/tools/result.go`)
 
-`Tool[T, R]` is a typed generic wrapper over MCP's untyped interface. `ConvertTool()` bridges typed handlers into MCP by auto-binding JSON arguments to `T` and marshaling `R` back to JSON. If `R` is already `*mcp.CallToolResult`, it passes through directly. All tools are marked idempotent by default.
+Tools are registered with `mcp.AddTool`, which binds JSON arguments to the handler's input struct and infers the input schema from it. `InferSchema[T]()` derives that schema; since the `jsonschema` struct tag can only carry a description, the helpers next to it patch the rest onto the inferred schema:
+
+- `WithRequired` declares required properties explicitly, rather than leaving the contract to depend on `omitempty`
+- `WithEnum` constrains a property to a fixed set of values
+- `WithDescriptions` sets descriptions a struct tag cannot hold (backticks, newlines, or lines over the length limit)
+
+`ResultText` / `ResultError` build tool results. All tools are marked idempotent.
 
 ### Communication with SkyWalking OAP
 
@@ -83,19 +88,16 @@ All MQE tool inputs are validated before use:
 
 ### Adding a New Tool
 1. Create or edit a file in `internal/tools/` (group by domain, e.g. `event.go`)
-2. Define request struct with `json` tags, write handler using `NewTool()`, create `Add*Tools()` function
-3. Register in `newMCPServer()` in `server.go`
-4. Follow existing tools (e.g. `event.go`) as reference for the pattern
+2. Define the request struct with `json` tags (use `omitempty`) and `jsonschema` tags for descriptions
+3. Write the handler with the `mcp.ToolHandlerFor` signature, plus a `xxxTool()` function returning the `*mcp.Tool`
+4. Register it with `mcp.AddTool` in the domain's `Add*Tools()` function
+5. Register that function in `newMCPServer()` in `server.go`
+6. Follow existing tools (e.g. `event.go`) as reference for the pattern
 
 ### Adding a New Prompt
 1. Add handler in `internal/prompts/` (analysis, trace, or utility group)
 2. Register via `s.AddPrompt()` in the corresponding group function in `registry.go`
 
-### Adding a New Resource
-1. For static content: embed files with `//go:embed` in `internal/resources/`
-2. For dynamic content: call internal tool functions in the resource handler
-3. Register via `s.AddResource()` in `AddMQEResources()` or a new registration function
-
 ## Code Conventions
 
 ### License Header
@@ -110,14 +112,14 @@ All `.go` files must have the Apache 2.0 license header (17-line block). Run `ma
 - 22 linters enabled including gosec, errcheck, dupl, gocritic
 
 ### Error Handling in Tools
-Tool handlers should return `(mcp.NewToolResultError(...), nil)` for expected query failures (bad input, OAP errors), not `(nil, err)`. Reserve Go errors for truly unexpected failures. Use the `ErrMarshalFailed` constant for JSON marshal errors.
+Tool handlers should return `(ResultError(...), nil, nil)` for expected query failures (bad input, OAP errors), not a Go error. Reserve Go errors for truly unexpected failures. Use the `ErrMarshalFailed` constant for JSON marshal errors.
 
 ### Comments
 Add a comment only when the code is not clear on its own — explain the non-obvious *why* (a workaround, a compatibility constraint, a surprising side effect), not the *what*. Do not write doc comments that merely restate a function/const name. Example worth keeping: `queryTraceV1GQL` omits the `duration` arg because it does not exist on OAP < 10.3.0.
 
 ## CI & Merge Policy
 
-Squash-merge only. PRs to `main` require 1 approval and passing `Required` status check (license + lint + docker build). Go 1.25.
+Squash-merge only. PRs to `main` require 1 approval and passing `Required` status check (license + lint + docker build). Go 1.26.
 
 ### GitHub Actions & the ASF allowed-actions list
 This repo is under `apache/*` and is governed by the ASF org-wide allowed-actions policy, enforced on every workflow run:
diff --git a/Dockerfile b/Dockerfile
index 2ff2a34..dac6db3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -14,7 +14,7 @@
 # limitations under the License.
 
 # Build stage
-FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS builder
+FROM --platform=$BUILDPLATFORM golang:1.26-bookworm AS builder
 
 # Default version
 ARG VERSION="dev"
diff --git a/Makefile b/Makefile
index 61de0c0..5b42be7 100644
--- a/Makefile
+++ b/Makefile
@@ -22,7 +22,8 @@ GIT_COMMIT ?= $(shell git rev-parse HEAD 2>/dev/null || echo unknown)
 BUILD_DATE=$(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
 MKDIR_P = mkdir -p
 
-GO_LINT = golangci-lint
+GO_BIN = $(shell go env GOPATH)/bin
+GO_LINT = $(GO_BIN)/golangci-lint
 LICENSE_EYE = license-eye
 
 HUB ?= ghcr.io/apache
@@ -59,15 +60,16 @@ test: ## Run unit tests.
 test-cover: ## Run unit tests with coverage output in coverage.txt.
 	go test $(GO_TEST_FLAGS) -coverprofile=coverage.txt $(GO_TEST_PKGS)
 
-$(GO_LINT):
-	@$(GO_LINT) version > /dev/null 2>&1 || go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.0
+.PHONY: install-lint
+install-lint:
+	@$(GO_LINT) version 2>/dev/null | grep -qE 'version v?2\.' || GOBIN=$(GO_BIN) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
 $(LICENSE_EYE):
 	@$(LICENSE_EYE) --version > /dev/null 2>&1 || go install github.com/apache/skywalking-eyes/cmd/license-eye@latest
 
-lint: $(GO_LINT) ## Run linter.
+lint: install-lint ## Run linter.
 	$(GO_LINT) run -v --timeout 5m ./...
 
-fix-lint: $(GO_LINT) ## Auto-fix lint issues.
+fix-lint: install-lint ## Auto-fix lint issues.
 	$(GO_LINT) run -v --fix ./...
 
 .PHONY: license-header
diff --git a/README.md b/README.md
index d176830..b219f08 100644
--- a/README.md
+++ b/README.md
@@ -51,11 +51,13 @@ SSE-specific Flags:
       --sse-address string     Host and port for the SSE server (default "localhost:8000")
       --base-path string       Base path for the SSE server
       --allowed-origins string Comma-separated list of allowed CORS origins. Empty reflects any origin (open CORS). Use * to send the wildcard header.
+      --disable-localhost-protection  Disable DNS rebinding protection (see Reverse proxies below)
 
 Streamable-specific Flags:
       --address string         Host and port for the Streamable HTTP server (default "localhost:8000")
       --endpoint-path string   Endpoint path for the Streamable HTTP server (default "/mcp")
       --allowed-origins string Comma-separated list of allowed CORS origins. Empty reflects any origin (open CORS). Use * to send the wildcard header.
+      --disable-localhost-protection  Disable DNS rebinding protection (see Reverse proxies below)
 
 Use "swmcp [command] --help" for more information about a command.
 ```
@@ -87,6 +89,30 @@ Transport URL behavior:
 - `stdio`, `sse`, and `streamable` all use the configured `--sw-url` value (or the default `http://localhost:12800/graphql`).
 - `sse` and `streamable` ignore request-level URL override headers.
 
+### Reverse proxies
+
+The HTTP transports reject a request that arrives over a loopback address while
+carrying a non-loopback `Host` header, answering `403 Forbidden: invalid Host
+header`. This is DNS rebinding protection: a malicious page can point its own
+hostname at `127.0.0.1` to reach a server running on the visitor's machine, and
+such a request is indistinguishable from a legitimate one except for that `Host`.
+
+A same-host reverse proxy that forwards to `127.0.0.1` while preserving the
+public `Host` produces the same shape and is rejected too. Prefer having the
+proxy rewrite `Host` to localhost, or point it at a non-loopback address.
+
+When you do opt out, pair it with `--allowed-origins`. A browser on the proxy
+host reaches `127.0.0.1` just as the proxy does, so the origin allowlist is the
+only defense left:
+
+```bash
+bin/swmcp streamable --sw-url http://localhost:12800 \
+  --disable-localhost-protection --allowed-origins https://mcp.example.com
+```
+
+Disabling the protection while leaving CORS open (the default) lets any web page
+drive the server through the browser of anyone on that host.
+
 ### Usage with Cursor, Copilot, Claude Code
 
 ```json
diff --git a/cmd/skywalking-mcp/main.go b/cmd/skywalking-mcp/main.go
index 1a80d8f..6c51a93 100644
--- a/cmd/skywalking-mcp/main.go
+++ b/cmd/skywalking-mcp/main.go
@@ -18,13 +18,9 @@
 package main
 
 import (
-	"context"
 	"fmt"
 	"log/slog"
 	"os"
-	"os/signal"
-	"strings"
-	"syscall"
 
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
@@ -42,16 +38,16 @@ var (
 		Short:   "Apache SkyWalking MCP Server.",
 		Long:    `This is a server that implements the MCP protocol for Apache SkyWalking.`,
 		Version: fmt.Sprintf("Version: %s\nCommit: %s\nBuild Date: %s", version, commit, date),
+		PersistentPreRun: func(*cobra.Command, []string) {
+			slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
+				Level: parseLogLevel(viper.GetString("log-level")),
+			})))
+		},
 	}
 )
 
 func init() {
-	// Set the environment variable prefix
-	viper.SetEnvPrefix("SW")
-	// Enable environment variable reading
-	viper.AutomaticEnv()
-	// All fields with . or - will be replaced with _ for ENV vars
-	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
+	swmcp.ConfigureEnv(viper.GetViper())
 
 	rootCmd.SetVersionTemplate("{{.Short}}\n{{.Version}}\n")
 
@@ -75,13 +71,6 @@ func init() {
 	_ = viper.BindPFlag("log-command", rootCmd.PersistentFlags().Lookup("log-command"))
 	_ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file"))
 
-	slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
-		Level: parseLogLevel(viper.GetString("log-level")),
-	})))
-
-	_, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
-	defer stop()
-
 	// Add subcommands
 	rootCmd.AddCommand(swmcp.NewStdioServer())
 	rootCmd.AddCommand(swmcp.NewSSEServer())
diff --git a/dist/LICENSE b/dist/LICENSE
index 8425d24..283e16d 100644
--- a/dist/LICENSE
+++ b/dist/LICENSE
@@ -186,9 +186,18 @@ The text of each license is also included at licenses/license-[project].txt.
     github.com/machinebox/graphql v0.2.2 Apache-2.0
     github.com/spf13/afero v1.15.0 Apache-2.0
     github.com/spf13/cobra v1.10.2 Apache-2.0
-    github.com/wk8/go-ordered-map/v2 v2.1.8 Apache-2.0
     skywalking.apache.org/repo/goapi v0.0.0-20251021000546-17778a1a5d70 Apache-2.0
 
+========================================================================
+Apache-2.0 and MIT and CC-BY-4.0 licenses
+========================================================================
+
+The following components are provided under the Apache-2.0 and MIT and CC-BY-4.0 License. See project link for details.
+The text of each license is also included at licenses/license-[project].txt.
+
+
+    github.com/modelcontextprotocol/go-sdk v1.7.0 Apache-2.0 and MIT and CC-BY-4.0
+
 ========================================================================
 BSD-2-Clause licenses
 ========================================================================
@@ -208,13 +217,14 @@ The following components are provided under the BSD-3-Clause License. See projec
 The text of each license is also included at licenses/license-[project].txt.
 
 
-    github.com/bahlo/generic-list-go v0.2.0 BSD-3-Clause
     github.com/fsnotify/fsnotify v1.9.0 BSD-3-Clause
-    github.com/google/uuid v1.6.0 BSD-3-Clause
     github.com/spf13/pflag v1.0.10 BSD-3-Clause
     github.com/yosida95/uritemplate/v3 v3.0.2 BSD-3-Clause
+    golang.org/x/oauth2 v0.35.0 BSD-3-Clause
+    golang.org/x/sync v0.20.0 BSD-3-Clause
     golang.org/x/sys v0.42.0 BSD-3-Clause
     golang.org/x/text v0.34.0 BSD-3-Clause
+    golang.org/x/time v0.15.0 BSD-3-Clause
 
 ========================================================================
 MIT licenses
@@ -224,13 +234,12 @@ The following components are provided under the MIT License. See project link fo
 The text of each license is also included at licenses/license-[project].txt.
 
 
-    github.com/buger/jsonparser v1.1.2 MIT
     github.com/go-viper/mapstructure/v2 v2.5.0 MIT
-    github.com/invopop/jsonschema v0.13.0 MIT
-    github.com/mailru/easyjson v0.9.1 MIT
-    github.com/mark3labs/mcp-go v0.45.0 MIT
+    github.com/google/jsonschema-go v0.4.3 MIT
     github.com/pelletier/go-toml/v2 v2.2.4 MIT
     github.com/sagikazarmark/locafero v0.12.0 MIT
+    github.com/segmentio/asm v1.1.3 MIT
+    github.com/segmentio/encoding v0.5.4 MIT
     github.com/sirupsen/logrus v1.9.4 MIT
     github.com/spf13/cast v1.10.0 MIT
     github.com/spf13/viper v1.21.0 MIT
@@ -245,5 +254,4 @@ The text of each license is also included at licenses/license-[project].txt.
 
 
     go.yaml.in/yaml/v3 v3.0.4 MIT and Apache-2.0
-    gopkg.in/yaml.v3 v3.0.1 MIT and Apache-2.0
 
diff --git a/dist/licenses/license-github.com-invopop-jsonschema.txt b/dist/licenses/license-github.com-google-jsonschema-go.txt
similarity index 60%
rename from dist/licenses/license-github.com-invopop-jsonschema.txt
rename to dist/licenses/license-github.com-google-jsonschema-go.txt
index 2993ec0..1cb53e9 100644
--- a/dist/licenses/license-github.com-invopop-jsonschema.txt
+++ b/dist/licenses/license-github.com-google-jsonschema-go.txt
@@ -1,11 +1,13 @@
-Copyright (C) 2014 Alec Thomas
+MIT License
 
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions:
+Copyright (c) 2025 JSON Schema Go Project Authors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
 
 The above copyright notice and this permission notice shall be included in all
 copies or substantial portions of the Software.
diff --git a/dist/licenses/license-github.com-mailru-easyjson.txt b/dist/licenses/license-github.com-mailru-easyjson.txt
deleted file mode 100644
index fbff658..0000000
--- a/dist/licenses/license-github.com-mailru-easyjson.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-Copyright (c) 2016 Mail.Ru Group
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/dist/licenses/license-github.com-wk8-go-ordered-map-v2.txt b/dist/licenses/license-github.com-modelcontextprotocol-go-sdk.txt
similarity index 81%
rename from dist/licenses/license-github.com-wk8-go-ordered-map-v2.txt
rename to dist/licenses/license-github.com-modelcontextprotocol-go-sdk.txt
index 8dada3e..5791499 100644
--- a/dist/licenses/license-github.com-wk8-go-ordered-map-v2.txt
+++ b/dist/licenses/license-github.com-modelcontextprotocol-go-sdk.txt
@@ -1,3 +1,11 @@
+The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
+
+Contributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License.
+
+No rights beyond those granted by the applicable original license are conveyed for such contributions.
+
+---
+
                                  Apache License
                            Version 2.0, January 2004
                         http://www.apache.org/licenses/
@@ -48,9 +56,9 @@
       "Contribution" shall mean any work of authorship, including
       the original version of the Work and any modifications or additions
       to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
+      submitted to Licensor for inclusion in the Work by the copyright
+      owner or by an individual or Legal Entity authorized to submit on behalf
+      of the copyright owner. For the purposes of this definition, "submitted"
       means any form of electronic, verbal, or written communication sent
       to the Licensor or its representatives, including but not limited to
       communication on electronic mailing lists, source code control systems,
@@ -175,27 +183,34 @@
 
    END OF TERMS AND CONDITIONS
 
-   APPENDIX: How to apply the Apache License to your work.
+---
+
+MIT License
+
+Copyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
 
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "{}"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
 
-   Copyright {yyyy} {name of copyright owner}
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
 
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
+---
 
-       http://www.apache.org/licenses/LICENSE-2.0
+Creative Commons Attribution 4.0 International (CC-BY-4.0)
 
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
+Documentation in this project (excluding specifications) is licensed under
+CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for
+the full license text.
diff --git a/dist/licenses/license-github.com-buger-jsonparser.txt b/dist/licenses/license-github.com-segmentio-asm.txt
similarity index 96%
rename from dist/licenses/license-github.com-buger-jsonparser.txt
rename to dist/licenses/license-github.com-segmentio-asm.txt
index ac25aeb..29e1ab6 100644
--- a/dist/licenses/license-github.com-buger-jsonparser.txt
+++ b/dist/licenses/license-github.com-segmentio-asm.txt
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2016 Leonid Bugaev
+Copyright (c) 2021 Segment
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/dist/licenses/license-github.com-mark3labs-mcp-go.txt b/dist/licenses/license-github.com-segmentio-encoding.txt
similarity index 96%
rename from dist/licenses/license-github.com-mark3labs-mcp-go.txt
rename to dist/licenses/license-github.com-segmentio-encoding.txt
index 3d48435..1fbffdf 100644
--- a/dist/licenses/license-github.com-mark3labs-mcp-go.txt
+++ b/dist/licenses/license-github.com-segmentio-encoding.txt
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2024 Anthropic, PBC
+Copyright (c) 2019 Segment.io, Inc.
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/dist/licenses/license-github.com-bahlo-generic-list-go.txt b/dist/licenses/license-golang.org-x-oauth2.txt
similarity index 92%
copy from dist/licenses/license-github.com-bahlo-generic-list-go.txt
copy to dist/licenses/license-golang.org-x-oauth2.txt
index 6a66aea..2a7cf70 100644
--- a/dist/licenses/license-github.com-bahlo-generic-list-go.txt
+++ b/dist/licenses/license-golang.org-x-oauth2.txt
@@ -1,4 +1,4 @@
-Copyright (c) 2009 The Go Authors. All rights reserved.
+Copyright 2009 The Go Authors.
 
 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are
@@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer.
 copyright notice, this list of conditions and the following disclaimer
 in the documentation and/or other materials provided with the
 distribution.
-   * Neither the name of Google Inc. nor the names of its
+   * Neither the name of Google LLC nor the names of its
 contributors may be used to endorse or promote products derived from
 this software without specific prior written permission.
 
diff --git a/dist/licenses/license-github.com-google-uuid.txt b/dist/licenses/license-golang.org-x-sync.txt
similarity index 92%
rename from dist/licenses/license-github.com-google-uuid.txt
rename to dist/licenses/license-golang.org-x-sync.txt
index 5dc6826..2a7cf70 100644
--- a/dist/licenses/license-github.com-google-uuid.txt
+++ b/dist/licenses/license-golang.org-x-sync.txt
@@ -1,4 +1,4 @@
-Copyright (c) 2009,2014 Google Inc. All rights reserved.
+Copyright 2009 The Go Authors.
 
 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are
@@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer.
 copyright notice, this list of conditions and the following disclaimer
 in the documentation and/or other materials provided with the
 distribution.
-   * Neither the name of Google Inc. nor the names of its
+   * Neither the name of Google LLC nor the names of its
 contributors may be used to endorse or promote products derived from
 this software without specific prior written permission.
 
diff --git a/dist/licenses/license-github.com-bahlo-generic-list-go.txt b/dist/licenses/license-golang.org-x-time.txt
similarity index 92%
rename from dist/licenses/license-github.com-bahlo-generic-list-go.txt
rename to dist/licenses/license-golang.org-x-time.txt
index 6a66aea..2a7cf70 100644
--- a/dist/licenses/license-github.com-bahlo-generic-list-go.txt
+++ b/dist/licenses/license-golang.org-x-time.txt
@@ -1,4 +1,4 @@
-Copyright (c) 2009 The Go Authors. All rights reserved.
+Copyright 2009 The Go Authors.
 
 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are
@@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer.
 copyright notice, this list of conditions and the following disclaimer
 in the documentation and/or other materials provided with the
 distribution.
-   * Neither the name of Google Inc. nor the names of its
+   * Neither the name of Google LLC nor the names of its
 contributors may be used to endorse or promote products derived from
 this software without specific prior written permission.
 
diff --git a/dist/licenses/license-gopkg.in-yaml.v3.txt b/dist/licenses/license-gopkg.in-yaml.v3.txt
deleted file mode 100644
index 2683e4b..0000000
--- a/dist/licenses/license-gopkg.in-yaml.v3.txt
+++ /dev/null
@@ -1,50 +0,0 @@
-
-This project is covered by two different licenses: MIT and Apache.
-
-#### MIT License ####
-
-The following files were ported to Go from C files of libyaml, and thus
-are still covered by their original MIT license, with the additional
-copyright staring in 2011 when the project was ported over:
-
-    apic.go emitterc.go parserc.go readerc.go scannerc.go
-    writerc.go yamlh.go yamlprivateh.go
-
-Copyright (c) 2006-2010 Kirill Simonov
-Copyright (c) 2006-2011 Kirill Simonov
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files (the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-### Apache License ###
-
-All the remaining project files are covered by the Apache license:
-
-Copyright (c) 2011-2019 Canonical Ltd
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
diff --git a/go.mod b/go.mod
index e25959d..ffb4c9d 100644
--- a/go.mod
+++ b/go.mod
@@ -1,11 +1,12 @@
 module github.com/apache/skywalking-mcp
 
-go 1.25.0
+go 1.26.0
 
 require (
 	github.com/apache/skywalking-cli v0.0.0-20251013114703-9a1beab08413
+	github.com/google/jsonschema-go v0.4.3
 	github.com/machinebox/graphql v0.2.2
-	github.com/mark3labs/mcp-go v0.45.0
+	github.com/modelcontextprotocol/go-sdk v1.7.0
 	github.com/sirupsen/logrus v1.9.4
 	github.com/spf13/cobra v1.10.2
 	github.com/spf13/viper v1.21.0
@@ -13,26 +14,24 @@ require (
 )
 
 require (
-	github.com/bahlo/generic-list-go v0.2.0 // indirect
-	github.com/buger/jsonparser v1.1.2 // indirect
 	github.com/fsnotify/fsnotify v1.9.0 // indirect
 	github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
-	github.com/google/uuid v1.6.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
-	github.com/invopop/jsonschema v0.13.0 // indirect
-	github.com/mailru/easyjson v0.9.1 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/sagikazarmark/locafero v0.12.0 // indirect
+	github.com/segmentio/asm v1.1.3 // indirect
+	github.com/segmentio/encoding v0.5.4 // indirect
 	github.com/spf13/afero v1.15.0 // indirect
 	github.com/spf13/cast v1.10.0 // indirect
 	github.com/spf13/pflag v1.0.10 // indirect
 	github.com/subosito/gotenv v1.6.0 // indirect
-	github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
 	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
 	go.yaml.in/yaml/v3 v3.0.4 // indirect
+	golang.org/x/oauth2 v0.35.0 // indirect
+	golang.org/x/sync v0.20.0 // indirect
 	golang.org/x/sys v0.42.0 // indirect
 	golang.org/x/text v0.34.0 // indirect
+	golang.org/x/time v0.15.0 // indirect
 	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
-	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index e8e8da5..79d336d 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,5 @@
 github.com/apache/skywalking-cli v0.0.0-20251013114703-9a1beab08413 h1:x5zJpQCh+BHC/S/s3VXk461BFD9ks4trOduwDiuMiG8=
 github.com/apache/skywalking-cli v0.0.0-20251013114703-9a1beab08413/go.mod h1:PuQJmkxOGXkv8ZHU+lGmfkQ9lX8eCqcYa1p/A9/oouA=
-github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
-github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
-github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
-github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -13,26 +9,24 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
 github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
 github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
+github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
-github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
-github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
+github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
-github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/machinebox/graphql v0.2.2 h1:dWKpJligYKhYKO5A2gvNhkJdQMNZeChZYyBbrZkBZfo=
 github.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA=
-github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
-github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
-github.com/mark3labs/mcp-go v0.45.0 h1:s0S8qR/9fWaQ3pHxz7pm1uQ0DrswoSnRIxKIjbiQtkc=
-github.com/mark3labs/mcp-go v0.45.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
 github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
 github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/modelcontextprotocol/go-sdk v1.7.0 h1:yqjY2dsbKAC0LSuWZVBMrHgiG8ukXv6NRo0JiALay44=
+github.com/modelcontextprotocol/go-sdk v1.7.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts=
 github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -44,6 +38,10 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
 github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
+github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
+github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
+github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
+github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
 github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
 github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
 github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
@@ -61,16 +59,22 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
-github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
-github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
 github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
 github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
 go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
 go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
+golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
 golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
 golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
 golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
 golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
+golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
+golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
+golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
+golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/internal/config/config.go b/internal/config/config.go
index 239b893..2fcbd44 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -67,6 +67,8 @@ type SSEServerConfig struct {
 
 	// Base path for the sse server
 	BasePath string
+
+	HTTPTransportConfig
 }
 
 type StreamableServerConfig struct {
@@ -88,4 +90,15 @@ type StreamableServerConfig struct {
 
 	// Base path for the Streamable HTTP transport server
 	EndpointPath string
+
+	HTTPTransportConfig
+}
+
+// HTTPTransportConfig holds the settings shared by the HTTP transports.
+type HTTPTransportConfig struct {
+	// Allowed CORS origins, empty reflects any origin
+	AllowedOrigins []string
+
+	// DisableLocalhostProtection turns off DNS rebinding protection
+	DisableLocalhostProtection bool
 }
diff --git a/internal/prompts/analysis.go b/internal/prompts/analysis.go
index 4c045fd..944cedf 100644
--- a/internal/prompts/analysis.go
+++ b/internal/prompts/analysis.go
@@ -21,10 +21,10 @@ import (
 	"context"
 	"fmt"
 
-	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 )
 
-func performanceAnalysisHandler(_ context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
+func performanceAnalysisHandler(_ context.Context, request *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
 	args := request.Params.Arguments
 	serviceName := args["service_name"]
 	start := args["start"]
@@ -74,11 +74,10 @@ Please provide actionable insights and specific recommendations based on the dat
 
 	return &mcp.GetPromptResult{
 		Description: "Performance analysis using SkyWalking tools",
-		Messages: []mcp.PromptMessage{
+		Messages: []*mcp.PromptMessage{
 			{
-				Role: mcp.RoleUser,
-				Content: mcp.TextContent{
-					Type: "text",
+				Role: "user",
+				Content: &mcp.TextContent{
 					Text: prompt,
 				},
 			},
@@ -86,7 +85,7 @@ Please provide actionable insights and specific recommendations based on the dat
 	}, nil
 }
 
-func mqeQueryBuilderHandler(_ context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
+func mqeQueryBuilderHandler(_ context.Context, request *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
 	args := request.Params.Arguments
 	queryType := args["query_type"]
 	metrics := args["metrics"]
@@ -117,11 +116,10 @@ If there are multiple ways to achieve this, please show alternatives with pros a
 
 	return &mcp.GetPromptResult{
 		Description: "MQE query building assistance",
-		Messages: []mcp.PromptMessage{
+		Messages: []*mcp.PromptMessage{
 			{
-				Role: mcp.RoleUser,
-				Content: mcp.TextContent{
-					Type: "text",
+				Role: "user",
+				Content: &mcp.TextContent{
 					Text: prompt,
 				},
 			},
@@ -129,7 +127,7 @@ If there are multiple ways to achieve this, please show alternatives with pros a
 	}, nil
 }
 
-func compareServicesHandler(_ context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
+func compareServicesHandler(_ context.Context, request *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
 	args := request.Params.Arguments
 	services := args["services"]
 	metrics := args["metrics"]
@@ -182,11 +180,10 @@ Please present the comparison in a clear, tabular format where possible, and hig
 
 	return &mcp.GetPromptResult{
 		Description: "Service comparison analysis",
-		Messages: []mcp.PromptMessage{
+		Messages: []*mcp.PromptMessage{
 			{
-				Role: mcp.RoleUser,
-				Content: mcp.TextContent{
-					Type: "text",
+				Role: "user",
+				Content: &mcp.TextContent{
 					Text: prompt,
 				},
 			},
@@ -194,7 +191,7 @@ Please present the comparison in a clear, tabular format where possible, and hig
 	}, nil
 }
 
-func topServicesHandler(_ context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
+func topServicesHandler(_ context.Context, request *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
 	args := request.Params.Arguments
 	metricName := args["metric_name"]
 	topN := args["top_n"]
@@ -237,11 +234,10 @@ Provide ranked results with specific recommendations.`, metricName, topN, order,
 
 	return &mcp.GetPromptResult{
 		Description: "Top services analysis",
-		Messages: []mcp.PromptMessage{
+		Messages: []*mcp.PromptMessage{
 			{
-				Role: mcp.RoleUser,
-				Content: mcp.TextContent{
-					Type: "text",
+				Role: "user",
+				Content: &mcp.TextContent{
 					Text: prompt,
 				},
 			},
diff --git a/internal/prompts/registry.go b/internal/prompts/registry.go
index 8c9cf50..5ba0583 100644
--- a/internal/prompts/registry.go
+++ b/internal/prompts/registry.go
@@ -18,34 +18,35 @@
 package prompts
 
 import (
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 )
 
+const descAnalysisWindowEnd = `End of the analysis window. Examples: "now", "2024-01-01 13:00:00". Default: now`
+
 // AddSkyWalkingPrompts registers all SkyWalking-related prompts
-func AddSkyWalkingPrompts(s *server.MCPServer) {
+func AddSkyWalkingPrompts(s *mcp.Server) {
 	addCoreAnalysisPrompts(s)
 	addTraceAnalysisPrompts(s)
 	addUtilityPrompts(s)
 }
 
-func addCoreAnalysisPrompts(s *server.MCPServer) {
+func addCoreAnalysisPrompts(s *mcp.Server) {
 	// Performance Analysis Prompt
-	s.AddPrompt(mcp.Prompt{
+	s.AddPrompt(&mcp.Prompt{
 		Name:        "analyze-performance",
 		Description: "Analyze service performance using metrics tools",
-		Arguments: []mcp.PromptArgument{
+		Arguments: []*mcp.PromptArgument{
 			{Name: "service_name", Description: "The name of the service to analyze", Required: true},
 			{Name: "start", Description: `Start of the analysis window. Examples: "-1h", "-30m", "2024-01-01 12:00:00". Default: -1h`, Required: false},
-			{Name: "end", Description: `End of the analysis window. Examples: "now", "2024-01-01 13:00:00". Default: now`, Required: false},
+			{Name: "end", Description: descAnalysisWindowEnd, Required: false},
 		},
 	}, performanceAnalysisHandler)
 
 	// Service Comparison Prompt
-	s.AddPrompt(mcp.Prompt{
+	s.AddPrompt(&mcp.Prompt{
 		Name:        "compare-services",
 		Description: "Compare performance metrics between multiple services",
-		Arguments: []mcp.PromptArgument{
+		Arguments: []*mcp.PromptArgument{
 			{Name: "services", Description: "Comma-separated list of service names to compare", Required: true},
 			{Name: "metrics", Description: "Metrics to compare (response_time, sla, cpm, all)", Required: false},
 			{Name: "start", Description: `Start of the comparison window. Examples: "-1h", "-2h", "-1d". Default: -1h`, Required: false},
@@ -54,10 +55,10 @@ func addCoreAnalysisPrompts(s *server.MCPServer) {
 	}, compareServicesHandler)
 
 	// Top N Metrics Analysis
-	s.AddPrompt(mcp.Prompt{
+	s.AddPrompt(&mcp.Prompt{
 		Name:        "top-services",
 		Description: "Find top N services by various metrics",
-		Arguments: []mcp.PromptArgument{
+		Arguments: []*mcp.PromptArgument{
 			{Name: "metric_name", Description: "Metric to rank by (service_cpm, service_resp_time, service_sla)", Required: true},
 			{Name: "top_n", Description: "Number of top services to return (default: 10)", Required: false},
 			{Name: "order", Description: "Order direction (ASC, DES)", Required: false},
@@ -65,12 +66,12 @@ func addCoreAnalysisPrompts(s *server.MCPServer) {
 	}, topServicesHandler)
 }
 
-func addTraceAnalysisPrompts(s *server.MCPServer) {
+func addTraceAnalysisPrompts(s *mcp.Server) {
 	// Trace Investigation Prompt
-	s.AddPrompt(mcp.Prompt{
+	s.AddPrompt(&mcp.Prompt{
 		Name:        "investigate-traces",
 		Description: "Investigate traces for errors and performance issues",
-		Arguments: []mcp.PromptArgument{
+		Arguments: []*mcp.PromptArgument{
 			{Name: "service_id", Description: "The service to investigate", Required: false},
 			{Name: "trace_state", Description: "Filter by trace state (success, error, all)", Required: false},
 			{Name: "start", Description: `Start of the search window. Examples: "-1h" (last hour), "-30m" (last 30 minutes). Default: -1h`, Required: false},
@@ -79,34 +80,34 @@ func addTraceAnalysisPrompts(s *server.MCPServer) {
 	}, traceInvestigationHandler)
 
 	// Trace Deep Dive
-	s.AddPrompt(mcp.Prompt{
+	s.AddPrompt(&mcp.Prompt{
 		Name:        "trace-deep-dive",
 		Description: "Deep dive analysis of a specific trace",
-		Arguments: []mcp.PromptArgument{
+		Arguments: []*mcp.PromptArgument{
 			{Name: "trace_id", Description: "The trace ID to analyze", Required: true},
 			{Name: "view", Description: "Analysis view (full, summary, errors_only)", Required: false},
 		},
 	}, traceDeepDiveHandler)
 
 	// Log Analysis Prompt
-	s.AddPrompt(mcp.Prompt{
+	s.AddPrompt(&mcp.Prompt{
 		Name:        "analyze-logs",
 		Description: "Analyze service logs for errors and patterns",
-		Arguments: []mcp.PromptArgument{
+		Arguments: []*mcp.PromptArgument{
 			{Name: "service_id", Description: "Service to analyze logs", Required: false},
 			{Name: "log_level", Description: "Log level to filter (ERROR, WARN, INFO)", Required: false},
 			{Name: "start", Description: `Start of the analysis window. Examples: "-1h" (last hour), "-6h" (last 6 hours). Default: -1h`, Required: false},
-			{Name: "end", Description: `End of the analysis window. Examples: "now", "2024-01-01 13:00:00". Default: now`, Required: false},
+			{Name: "end", Description: descAnalysisWindowEnd, Required: false},
 		},
 	}, logAnalysisHandler)
 }
 
-func addUtilityPrompts(s *server.MCPServer) {
+func addUtilityPrompts(s *mcp.Server) {
 	// Service Topology Explorer
-	s.AddPrompt(mcp.Prompt{
+	s.AddPrompt(&mcp.Prompt{
 		Name:        "explore-service-topology",
 		Description: "Explore the service topology of a layer: list services, instances, endpoints, and processes within a time range",
-		Arguments: []mcp.PromptArgument{
+		Arguments: []*mcp.PromptArgument{
 			{Name: "layer", Description: "The layer to explore (e.g. GENERAL, MESH, K8S). Use list_layers if unknown.", Required: true},
 			{Name: "start", Description: `Start time for the query. Examples: "2024-01-01 12:00:00", "-1h" (1 hour ago).`, Required: true},
 			{Name: "end", Description: `End time for the query. Examples: "2024-01-01 13:00:00", "now".` +
@@ -115,11 +116,11 @@ func addUtilityPrompts(s *server.MCPServer) {
 	}, exploreServiceTopologyHandler)
 
 	// Generate Duration Prompt
-	s.AddPrompt(mcp.Prompt{
+	s.AddPrompt(&mcp.Prompt{
 		Name: "generate_duration",
 		Description: "Convert a natural-language time range into a {start, end} duration object" +
 			" for use with list_instances, list_endpoints, list_processes, and similar tools",
-		Arguments: []mcp.PromptArgument{
+		Arguments: []*mcp.PromptArgument{
 			{Name: "time_range", Description: `Natural-language description of the desired time range.` +
 				` Examples: "last hour", "past 30 minutes", "yesterday 9am to 5pm", "2024-01-01 12:00 to 13:00"`,
 				Required: true},
@@ -127,10 +128,10 @@ func addUtilityPrompts(s *server.MCPServer) {
 	}, generateDurationHandler)
 
 	// MQE Query Builder Prompt
-	s.AddPrompt(mcp.Prompt{
+	s.AddPrompt(&mcp.Prompt{
 		Name:        "build-mqe-query",
 		Description: "Help build MQE (Metrics Query Expression) for complex queries",
-		Arguments: []mcp.PromptArgument{
+		Arguments: []*mcp.PromptArgument{
 			{Name: "query_type", Description: "Type of query (performance, comparison, trend, alert)", Required: true},
 			{Name: "metrics", Description: "Comma-separated list of metrics to query", Required: true},
 			{Name: "conditions", Description: "Additional conditions or filters", Required: false},
@@ -138,10 +139,10 @@ func addUtilityPrompts(s *server.MCPServer) {
 	}, mqeQueryBuilderHandler)
 
 	// MQE Metrics Explorer
-	s.AddPrompt(mcp.Prompt{
+	s.AddPrompt(&mcp.Prompt{
 		Name:        "explore-metrics",
 		Description: "Explore available metrics and their types",
-		Arguments: []mcp.PromptArgument{
+		Arguments: []*mcp.PromptArgument{
 			{Name: "pattern", Description: "Regex pattern to filter metrics", Required: false},
 			{Name: "show_examples", Description: "Show usage examples for each metric (true/false)", Required: false},
 		},
diff --git a/internal/prompts/trace.go b/internal/prompts/trace.go
index 90bbf32..b0b3127 100644
--- a/internal/prompts/trace.go
+++ b/internal/prompts/trace.go
@@ -21,10 +21,10 @@ import (
 	"context"
 	"fmt"
 
-	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 )
 
-func traceInvestigationHandler(_ context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
+func traceInvestigationHandler(_ context.Context, request *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
 	args := request.Params.Arguments
 	serviceID := args["service_id"]
 	traceState := args["trace_state"]
@@ -75,11 +75,10 @@ Provide specific findings and actionable recommendations.`, serviceID, traceStat
 
 	return &mcp.GetPromptResult{
 		Description: "Trace investigation using query tools",
-		Messages: []mcp.PromptMessage{
+		Messages: []*mcp.PromptMessage{
 			{
-				Role: mcp.RoleUser,
-				Content: mcp.TextContent{
-					Type: "text",
+				Role: "user",
+				Content: &mcp.TextContent{
 					Text: prompt,
 				},
 			},
@@ -87,7 +86,7 @@ Provide specific findings and actionable recommendations.`, serviceID, traceStat
 	}, nil
 }
 
-func logAnalysisHandler(_ context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
+func logAnalysisHandler(_ context.Context, request *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
 	args := request.Params.Arguments
 	serviceID := args["service_id"]
 	logLevel := args["log_level"]
@@ -141,11 +140,10 @@ Provide specific log analysis findings and recommendations.`, serviceID, logLeve
 
 	return &mcp.GetPromptResult{
 		Description: "Log analysis using query_logs tool",
-		Messages: []mcp.PromptMessage{
+		Messages: []*mcp.PromptMessage{
 			{
-				Role: mcp.RoleUser,
-				Content: mcp.TextContent{
-					Type: "text",
+				Role: "user",
+				Content: &mcp.TextContent{
 					Text: prompt,
 				},
 			},
@@ -153,7 +151,7 @@ Provide specific log analysis findings and recommendations.`, serviceID, logLeve
 	}, nil
 }
 
-func traceDeepDiveHandler(_ context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
+func traceDeepDiveHandler(_ context.Context, request *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
 	args := request.Params.Arguments
 	traceID := args["trace_id"]
 	view := args["view"]
@@ -197,11 +195,10 @@ Provide detailed trace analysis with specific optimization recommendations.`, tr
 
 	return &mcp.GetPromptResult{
 		Description: "Deep dive trace analysis",
-		Messages: []mcp.PromptMessage{
+		Messages: []*mcp.PromptMessage{
 			{
-				Role: mcp.RoleUser,
-				Content: mcp.TextContent{
-					Type: "text",
+				Role: "user",
+				Content: &mcp.TextContent{
 					Text: prompt,
 				},
 			},
diff --git a/internal/prompts/utility.go b/internal/prompts/utility.go
index a9e66a8..7d4cb8a 100644
--- a/internal/prompts/utility.go
+++ b/internal/prompts/utility.go
@@ -21,10 +21,10 @@ import (
 	"context"
 	"fmt"
 
-	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 )
 
-func generateDurationHandler(_ context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
+func generateDurationHandler(_ context.Context, request *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
 	timeRange := request.Params.Arguments["time_range"]
 
 	prompt := fmt.Sprintf(`Convert the following time range description into a duration object with "start" and "end" fields.
@@ -45,16 +45,16 @@ This duration can be passed directly to tools such as list_instances, list_endpo
 
 	return &mcp.GetPromptResult{
 		Description: "Generate a {start, end} duration object from a natural-language time range",
-		Messages: []mcp.PromptMessage{
+		Messages: []*mcp.PromptMessage{
 			{
-				Role:    mcp.RoleUser,
-				Content: mcp.TextContent{Type: "text", Text: prompt},
+				Role:    "user",
+				Content: &mcp.TextContent{Text: prompt},
 			},
 		},
 	}, nil
 }
 
-func exploreServiceTopologyHandler(_ context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
+func exploreServiceTopologyHandler(_ context.Context, request *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
 	args := request.Params.Arguments
 	layer := args["layer"]
 	start := args["start"]
@@ -105,11 +105,10 @@ func exploreServiceTopologyHandler(_ context.Context, request mcp.GetPromptReque
 
 	return &mcp.GetPromptResult{
 		Description: "Service topology exploration",
-		Messages: []mcp.PromptMessage{
+		Messages: []*mcp.PromptMessage{
 			{
-				Role: mcp.RoleUser,
-				Content: mcp.TextContent{
-					Type: "text",
+				Role: "user",
+				Content: &mcp.TextContent{
 					Text: prompt,
 				},
 			},
@@ -117,7 +116,7 @@ func exploreServiceTopologyHandler(_ context.Context, request mcp.GetPromptReque
 	}, nil
 }
 
-func exploreMetricsHandler(_ context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
+func exploreMetricsHandler(_ context.Context, request *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
 	args := request.Params.Arguments
 	pattern := args["pattern"]
 	showExamples := args["show_examples"]
@@ -168,11 +167,10 @@ Provide a comprehensive guide to available metrics and their usage.`, pattern, t
 
 	return &mcp.GetPromptResult{
 		Description: "Metrics exploration guide",
-		Messages: []mcp.PromptMessage{
+		Messages: []*mcp.PromptMessage{
 			{
-				Role: mcp.RoleUser,
-				Content: mcp.TextContent{
-					Type: "text",
+				Role: "user",
+				Content: &mcp.TextContent{
 					Text: prompt,
 				},
 			},
diff --git a/internal/resources/mqe_ai_prompt.md b/internal/resources/mqe_ai_prompt.md
deleted file mode 100644
index 6c6bd1e..0000000
--- a/internal/resources/mqe_ai_prompt.md
+++ /dev/null
@@ -1,249 +0,0 @@
-# MQE AI Model Understanding Guide
-
-## Core Understanding Principles
-
-When users describe monitoring requirements in natural language, you need to:
-
-1. **Understand user intent**, not just mechanically match keywords
-2. **Infer implicit information**, such as time ranges, aggregation methods, etc.
-3. **Handle errors gracefully**, understand spelling mistakes or non-standard expressions
-4. **Provide explanations**, explain the meaning of generated MQE expressions
-
-## Natural Language Understanding Strategies
-
-### 1. Intent Recognition Patterns
-
-#### 1.1 Query Type Intents
-- "view/show/get/display..." → Simple query
-- "monitor/observe/track/watch..." → Continuous query
-- "analyze/compare/contrast..." → Complex query
-- "alert/alarm/notify..." → Conditional judgment
-
-#### 1.2 Metric Recognition
-Users may express the same metric in various ways:
-
-**Response Time**:
-- response time, latency, delay, time spent, RT, latency
-- → `service_resp_time` or `endpoint_resp_time`
-
-**Success Rate**:
-- success rate, availability, SLA, uptime, health
-- → `service_sla`
-
-**Call Volume**:
-- call volume, request count, traffic, QPS, TPS, throughput
-- → `service_cpm` (needs context-based conversion)
-
-**Error Rate**:
-- error rate, failure rate, exception rate
-- → `100 - (service_sla * 100)` or use status_code metrics
-
-### 2. Time Range Understanding
-
-#### 2.1 Relative Time
-**Past Time (negative duration)**:
-- "recently/past/just now" + number + unit
-  - "recent 5 minutes" → `duration: "-5m"`
-  - "past one hour" → `duration: "-1h"`
-  - "recent one day" → `duration: "-24h"` or `duration: "-1d"`
-
-**Future Time (positive duration)**:
-- "next/future/upcoming/coming" + number + unit
-  - "next 5 minutes" → `duration: "5m"`
-  - "upcoming one hour" → `duration: "1h"`
-  - "next day" → `duration: "24h"` or `duration: "1d"`
-
-#### 2.2 Fuzzy Time
-**Past-oriented (negative)**:
-- "just now" → `duration: "-5m"` (defaults to 5 minutes)
-- "recently" → `duration: "-1h"` (defaults to 1 hour)
-
-**Future-oriented (positive)**:
-- "soon" → `duration: "5m"` (defaults to next 5 minutes)
-- "later" → `duration: "1h"` (defaults to next 1 hour)
-
-#### 2.3 Absolute Time
-- "from...to..." → use start and end
-- "January 1, 2025, 10 am to 11 am" → convert to standard format
-
-### 3. Inference of Aggregation Methods
-
-#### 3.1 Default Aggregation
-- Query response time → usually use `avg()`
-- Query error count → usually use `sum()`
-- Query peak value → use `max()`
-- Query minimum value → use `min()`
-
-#### 3.2 Explicit Aggregation
-- "average response time" → `avg(service_resp_time)`
-- "maximum latency" → `max(service_resp_time)`
-- "total call volume" → `sum(service_cpm)`
-- "median response time" → `service_percentile{p='50'}`
-
-### 4. Condition Understanding
-
-#### 4.1 Threshold Judgment
-- "exceeds/greater than/higher than" → `>`
-- "less than/lower than" → `<`
-- "reaches/equal to" → `>=` or `==`
-- "between...and..." → combine `>` and `<`
-
-#### 4.2 Unit Conversion
-- "exceeds 1 second" → `> 1000` (convert to milliseconds)
-- "less than 90%" → `< 90` (after converting SLA: `service_sla * 100 < 90`)
-- "100 times per second" → convert to CPM: `> 6000`
-
-### 5. Complex Query Patterns
-
-#### 5.1 Multi-Condition Combination
-User: "response time exceeds 3 seconds and low call volume"
-- Identify two conditions
-- "low" requires inferred threshold, such as `< 100`
-- Generate: `service_resp_time > 3000 && service_cpm < 100`
-
-#### 5.2 Percentile Query
-User: "check P95 and P99 response time"
-- Identify percentile requirement
-- Generate: `service_percentile{p='95,99'}`
-
-#### 5.3 Sorting Requirement
-User: "slowest 10 services"
-- Identify sorting and quantity
-- Generate: `top_n(service_resp_time, 10, des)`
-
-### 6. Error Tolerance Handling
-
-#### 6.1 Spelling Errors
-- "sevice" → "service"
-- "responce" → "response"
-- "avrage" → "average"
-
-#### 6.2 Abbreviation Understanding
-- "RT" → Response Time
-- "QPS" → Queries Per Second (convert to CPM)
-- "P95" → 95th percentile
-
-#### 6.3 Colloquial Expressions
-- "this service is slow" → query response time
-- "traffic suddenly increased" → use increase() function
-- "higher than usual" → use baseline() function
-
-### 7. Context Understanding
-
-#### 7.1 Service Recognition
-- If the user mentions a specific service name, extract and use it
-- If not specified, may need to:
-  - Query aggregate data for all services
-  - Use top_n to find related services
-
-#### 7.2 Level Understanding
-- "database" → `layer: "DATABASE"`
-- "gateway" → possibly a specific service name
-- "microservice" → `layer: "GENERAL"`
-
-### 8. Response Generation Strategy
-
-#### 8.1 Explaining Generated Expressions
-```
-User: "check the recent average response time for service A"
-Response: "I will query the average response time for service A in the last hour:"
-Expression: avg(service_resp_time)
-Parameters: service_name="A", duration="-1h"
-```
-
-#### 8.2 Providing Alternatives
-```
-User: "service is slow"
-Response: "I understand you want to check service performance issues. Here are several query options:
-1. Check average response time: avg(service_resp_time)
-2. Check response time percentiles: service_percentile{p='50,75,90,95,99'}
-3. Check slowest services: top_n(service_resp_time, 10, des)"
-```
-
-#### 8.3 Suggesting Optimizations
-```
-User: "requests per second"
-Response: "service_cpm returns calls per minute, I'll help you convert to per second:"
-Expression: service_cpm / 60
-```
-
-### 9. Common Scenario Templates
-
-#### 9.1 Performance Analysis
-- "service performance analysis" → combine multiple metrics
-  ```
-  Response time: avg(service_resp_time)
-  Call volume: service_cpm
-  Success rate: service_sla * 100
-  ```
-
-#### 9.2 Alert Rules
-- "response time alert" → generate comparison expression
-  ```
-  avg(service_resp_time) > 1000
-  ```
-
-#### 9.3 Trend Analysis
-- "traffic trend" → use trend functions
-  ```
-  rate(service_cpm, 5)  # rate of change over 5 minutes
-  ```
-
-### 10. Error Handling Tips
-
-When unable to understand user intent:
-1. Ask for specifics: "Do you want to check response time or call volume?"
-2. Provide examples: "For example: check average response time for service A"
-3. List available options: "Available metrics include: response time, call volume, success rate, etc."
-
-## Practical Examples
-
-### Example 1: Fuzzy Query
-User: "service seems to have issues"
-Analysis:
-- Intent: diagnose problems
-- Suggest querying multiple key metrics
-
-Response:
-```
-Let me check the key service metrics:
-1. Success rate: service_sla * 100
-2. Response time: avg(service_resp_time)
-3. Call volume: service_cpm
-These metrics can help identify the issue.
-```
-
-### Example 2: Complex Conditions
-User: "find services with slow response but high traffic"
-Analysis:
-- Needs to satisfy two conditions
-- "slow" and "high" need threshold definitions
-
-Response:
-```
-expression: "service_resp_time > 1000 && service_cpm > 1000"
-Explanation: Find services with response time over 1 second and over 1000 calls per minute
-You can also use top_n to find such services.
-```
-
-### Example 3: Time Comparison
-User: "compare today's traffic with yesterday"
-Analysis:
-- Needs two time period queries
-- May need to calculate difference or ratio
-
-Response:
-```
-Need to execute two queries:
-1. Today: start="today 00:00", end="now", expression="avg(service_cpm)"
-2. Yesterday: start="yesterday 00:00", end="yesterday 24:00", expression="avg(service_cpm)"
-Then compare the results.
-```
-
-## Best Practices
-
-1. **Proactive Inference**: Users may not know specific metric names, infer from descriptions
-2. **Provide Context**: Explain why choosing a certain expression
-3. **Flexible Handling**: Same requirement may have multiple implementations
-4. **Educate Users**: Include MQE knowledge in responses to help users learn
-5. **Safe Defaults**: When uncertain, choose more conservative query approaches
diff --git a/internal/resources/mqe_detailed_syntax.md b/internal/resources/mqe_detailed_syntax.md
deleted file mode 100644
index ccb38df..0000000
--- a/internal/resources/mqe_detailed_syntax.md
+++ /dev/null
@@ -1,308 +0,0 @@
-# MQE (Metrics Query Expression) Detailed Syntax Rules
-
-## 1. Syntax Structure Overview
-
-The basic structure of MQE expressions follows these BNF grammar rules:
-
-```bnf
-expression ::= metric_expression | calculation_expression | comparison_expression | function_expression
-
-metric_expression ::= metric_name | metric_name '{' label_selector '}'
-
-label_selector ::= label '=' '"' value_list '"' (',' label '=' '"' value_list '"')*
-
-value_list ::= value (',' value)*
-
-calculation_expression ::= expression operator expression | '(' expression ')'
-
-operator ::= '+' | '-' | '*' | '/' | '%'
-
-comparison_expression ::= expression comparison_operator expression
-
-comparison_operator ::= '>' | '>=' | '<' | '<=' | '==' | '!='
-
-logical_expression ::= expression logical_operator expression
-
-logical_operator ::= '&&' | '||'
-
-function_expression ::= function_name '(' argument_list ')'
-
-argument_list ::= expression (',' expression)*
-```
-
-## 2. Core Components Details
-
-### 2.1 Metric Names
-- **Rules**: Must be valid metric names, typically lowercase letters separated by underscores
-- **Pattern**: `[a-z][a-z0-9_]*`
-- **Examples**: `service_sla`, `service_cpm`, `endpoint_resp_time`
-
-### 2.2 Label Selectors
-- **Syntax**: `{label='value1,value2,...'}`
-- **Multiple labels**: `{label1='value1', label2='value2'}`
-- **Special case**: Percentile labels `{p='50,75,90,95,99'}`
-
-### 2.3 Operator Precedence (highest to lowest)
-1. Parentheses `()`
-2. Function calls
-3. Multiplication, division, modulo `*`, `/`, `%`
-4. Addition, subtraction `+`, `-`
-5. Comparison operators `>`, `>=`, `<`, `<=`, `==`, `!=`
-6. Logical AND `&&`
-7. Logical OR `||`
-
-## 3. Function Categories and Syntax
-
-### 3.1 Aggregation Functions
-```
-avg(expression) -> single_value
-sum(expression) -> single_value
-max(expression) -> single_value
-min(expression) -> single_value
-count(expression) -> single_value
-latest(expression) -> single_value
-```
-
-### 3.2 Mathematical Functions
-```
-abs(expression) -> same_type_as_input
-ceil(expression) -> same_type_as_input
-floor(expression) -> same_type_as_input
-round(expression, decimal_places) -> same_type_as_input
-```
-
-### 3.3 Sorting and Selection Functions
-```
-top_n(metric_name, top_number, order, attrs) -> sorted_list/record_list
-  - top_number: positive integer
-  - order: 'asc' | 'des'
-  - attrs: optional, e.g., attr0='value', attr1='value'
-
-top_n_of(top_n_expr1, top_n_expr2, ..., top_number, order) -> merged_top_n
-  - top_n_expr: top_n expressions
-  - top_number: positive integer
-  - order: 'asc' | 'des'
-
-sort_values(expression, limit, order) -> follows_input_type
-  - limit: optional positive integer
-  - order: 'asc' | 'des'
-
-sort_label_values(expression, order, label_names...) -> follows_input_type
-  - order: 'asc' | 'des'
-  - label_names: at least one label name
-```
-
-### 3.4 Trend Analysis Functions
-```
-increase(expression, time_range) -> time_series_values
-rate(expression, time_range) -> time_series_values
-  - time_range: positive integer, unit aligns with query Step
-```
-
-### 3.5 Label Operations
-```
-relabel(expression, target_label='origin_values', new_label='new_values') -> follows_input
-aggregate_labels(expression, aggregation_method(label_names...)) -> time_series_values
-  - aggregation_method: sum | avg | max | min
-  - label_names: optional, if not specified, all labels will be aggregated
-```
-
-### 3.6 Logical Functions
-```
-view_as_seq([expression1, expression2, ...]) -> follows_selected_expression
-is_present([expression1, expression2, ...]) -> single_value (0 or 1)
-```
-
-### 3.7 Baseline Functions
-```
-baseline(expression, baseline_type) -> time_series_values
-  - baseline_type: 'value' | 'upper' | 'lower'
-```
-
-## 4. Expression Types and Return Values
-
-### 4.1 Return Value Types
-- **SINGLE_VALUE**: Single value, like `avg(service_cpm)`
-- **TIME_SERIES_VALUES**: Time series data with timestamps
-- **SORTED_LIST**: Sorted list of values, like `top_n()`
-- **RECORD_LIST**: List of records
-- **LABELED_VALUE**: Values with labels, such as percentiles
-
-### 4.2 Type Conversion Rules
-- Numeric values can be used directly in arithmetic operations
-- Boolean values (comparison results) convert to 0 or 1
-- Label values must be accessed through label selectors
-
-## 5. Entity Filtering Rules
-
-### 5.1 Service-Level Filtering
-```
-expression + entity{serviceName='name', layer='GENERAL', normal=true}
-```
-
-### 5.2 Instance-Level Filtering
-```
-expression + entity{serviceName='name', serviceInstanceName='instance'}
-```
-
-### 5.3 Endpoint-Level Filtering
-```
-expression + entity{serviceName='name', endpointName='endpoint'}
-```
-
-### 5.4 Relation Query Filtering
-```
-expression + entity{
-  serviceName='source',
-  destServiceName='destination',
-  layer='GENERAL',
-  destLayer='DATABASE'
-}
-```
-
-## 6. Common Patterns and Best Practices
-
-### 6.1 Percentage Conversion
-```
-# Convert SLA from decimal to percentage
-service_sla * 100
-
-# Convert CPM to RPS
-service_cpm / 60
-```
-
-### 6.2 Condition Combinations
-```
-# High latency with low traffic
-service_resp_time > 3000 && service_cpm < 100
-
-# Multiple percentiles exceed threshold
-sum(service_percentile{p='50,75,90,95,99'} > 1000) >= 3
-```
-
-### 6.3 Trend Monitoring
-```
-# Response time growth rate
-rate(service_resp_time, 5)
-
-# Increase over the past 2 minutes
-increase(service_cpm, 2)
-```
-
-### 6.4 Aggregation Calculations
-```
-# Average response time (convert milliseconds to seconds)
-avg(service_resp_time) / 1000
-
-# Error rate statistics
-sum(aggregate_labels(meter_status_code{status='4xx,5xx'}, sum))
-```
-
-## 7. Syntax Validation Rules
-
-### 7.1 Required Conditions
-1. Metric names must exist in the system
-2. Label names must match the metric type
-3. Function parameter count must be correct
-4. Types on both sides of operators must be compatible
-
-### 7.2 Common Error Patterns
-```
-# Error: Missing label selector
-service_percentile  # Should be service_percentile{p='50'}
-
-# Error: Invalid aggregation
-avg(service_percentile{p='50,75,90'})  # Cannot average multiple values directly
-
-# Error: Type mismatch
-"string" + 123  # Cannot add string and number
-```
-
-## 8. Parsing Precedence Example
-
-```
-# Expression: avg(service_cpm) * 60 > 1000 && service_sla < 0.95
-# Parsing order:
-1. avg(service_cpm)  # Function call
-2. ... * 60          # Multiplication
-3. ... > 1000        # Comparison
-4. service_sla < 0.95 # Comparison
-5. ... && ...        # Logical AND
-```
-
-## 9. Advanced Usage
-
-### 9.1 Nested Functions
-```
-round(avg(service_resp_time) / 1000, 2)  # Average response time in seconds, rounded to 2 decimal places
-```
-
-### 9.2 Conditional Aggregation
-```
-sum((service_sla * 100) < 95)  # Count instances where SLA is below 95%
-```
-
-### 9.3 Dynamic Labels
-```
-relabel(
-  service_percentile{p='50,75,90,95,99'}, 
-  p='50,75,90,95,99', 
-  percentile='P50,P75,P90,P95,P99'
-)
-```
-
-## 10. Natural Language to MQE Mapping Rules
-
-### 10.1 Keyword Mappings
-- "average" → `avg()`
-- "maximum" → `max()`
-- "minimum" → `min()`
-- "total", "sum" → `sum()`
-- "count" → `count()`
-- "latest" → `latest()`
-- "top N" → `top_n(..., N, des)`
-- "bottom N" → `top_n(..., N, asc)`
-- "percentage" → `* 100`
-- "per second" → `/ 60` (for CPM)
-- "increase" → `increase()`
-- "rate" → `rate()`
-
-### 10.2 Condition Mappings
-- "greater than", "more than" → `>`
-- "less than", "below" → `<`
-- "equals", "is" → `==`
-- "not equal", "is not" → `!=`
-- "and" → `&&`
-- "or" → `||`
-
-### 10.3 Time Range Mappings
-- "last hour", "past hour" (past) → `duration: "-1h"`
-- "last 30 minutes" (past) → `duration: "-30m"`
-- "next 30 minutes" (future) → `duration: "30m"`
-
-## 11. Model Understanding Guidelines
-
-When processing natural language queries from users, the model should:
-
-1. **Identify Intent**: Is it querying a single value, time series, sorted list, or comparison?
-2. **Extract Entities**: Service names, instance names, endpoint names, etc.
-3. **Identify Metrics**: Response time, success rate, call volume, etc.
-4. **Determine Operations**: Aggregation, calculation, comparison, sorting, etc.
-5. **Build Expression**: Combine components according to syntax rules
-
-### Example Conversion Process
-User input: "Show the average response time for service A in the last hour, alert if it exceeds 1 second"
-
-Analysis steps:
-1. Entity: Service A → `service_name: "A"`
-2. Metric: Response time → `service_resp_time`
-3. Operation: Average → `avg()`
-4. Time: Last hour → `duration: "1h"`
-5. Condition: Exceeds 1 second → `> 1000` (convert to milliseconds)
-
-Final expression:
-```
-expression: "avg(service_resp_time) > 1000"
-service_name: "A"
-duration: "1h"
-```
diff --git a/internal/resources/mqe_docs.go b/internal/resources/mqe_docs.go
deleted file mode 100644
index 3d9d45b..0000000
--- a/internal/resources/mqe_docs.go
+++ /dev/null
@@ -1,126 +0,0 @@
-// Licensed to Apache Software Foundation (ASF) under one or more contributor
-// license agreements. See the NOTICE file distributed with
-// this work for additional information regarding copyright
-// ownership. Apache Software Foundation (ASF) licenses this file to you under
-// the Apache License, Version 2.0 (the "License"); you may
-// not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-package resources
-
-import (
-	"context"
-	_ "embed"
-	"encoding/json"
-	"fmt"
-
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
-
-	"github.com/apache/skywalking-mcp/internal/tools"
-)
-
-// Embed MQE documentation and examples
-
-//go:embed mqe_examples.json
-var mqeExamples string
-
-//go:embed mqe_detailed_syntax.md
-var mqeDetailedSyntaxDoc string
-
-//go:embed mqe_ai_prompt.md
-var mqeAIPromptDoc string
-
-// AddMQEResources registers MQE-related resources with the MCP server
-func AddMQEResources(s *server.MCPServer) {
-	// Add detailed MQE syntax documentation as a resource
-	s.AddResource(mcp.Resource{
-		URI:         "mqe://docs/syntax",
-		Name:        "MQE Detailed Syntax Rules",
-		Description: "Comprehensive syntax rules and grammar for MQE expressions",
-		MIMEType:    "text/markdown",
-	}, func(_ context.Context, _ mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
-		return []mcp.ResourceContents{
-			mcp.TextResourceContents{
-				URI:      "mqe://docs/syntax",
-				MIMEType: "text/markdown",
-				Text:     mqeDetailedSyntaxDoc,
-			},
-		}, nil
-	})
-
-	// Add MQE examples as a resource
-	s.AddResource(mcp.Resource{
-		URI:         "mqe://docs/examples",
-		Name:        "MQE Examples",
-		Description: "Common MQE expression examples with natural language descriptions",
-		MIMEType:    "application/json",
-	}, func(_ context.Context, _ mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
-		return []mcp.ResourceContents{
-			mcp.TextResourceContents{
-				URI:      "mqe://docs/examples",
-				MIMEType: "application/json",
-				Text:     mqeExamples,
-			},
-		}, nil
-	})
-
-	// Add a dynamic resource that lists available metrics
-	s.AddResource(mcp.Resource{
-		URI:         "mqe://metrics/available",
-		Name:        "Available Metrics",
-		Description: "List of all available metrics in the current SkyWalking instance",
-		MIMEType:    "application/json",
-	}, func(ctx context.Context, _ mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
-		// Call list_mqe_metrics to get real-time data
-		resp, err := tools.ListMQEMetricsInternal(ctx, nil)
-		if err != nil {
-			return nil, fmt.Errorf("failed to list metrics: %w", err)
-		}
-
-		// Format the response
-		var formattedData interface{}
-		if unmarshalErr := json.Unmarshal([]byte(resp), &formattedData); unmarshalErr != nil {
-			return nil, fmt.Errorf("failed to parse metrics data: %w", unmarshalErr)
-		}
-
-		// Convert back to JSON with proper formatting
-		formattedJSON, err := json.MarshalIndent(formattedData, "", "  ")
-		if err != nil {
-			return nil, fmt.Errorf("failed to format metrics data: %w", err)
-		}
-
-		return []mcp.ResourceContents{
-			mcp.TextResourceContents{
-				URI:      "mqe://metrics/available",
-				MIMEType: "application/json",
-				Text:     string(formattedJSON),
-			},
-		}, nil
-	})
-
-	// Add AI understanding guide as a resource
-	s.AddResource(mcp.Resource{
-		URI:         "mqe://docs/ai_prompt",
-		Name:        "MQE AI Understanding Guide",
-		Description: "Guide for AI models to understand natural language queries and convert to MQE",
-		MIMEType:    "text/markdown",
-	}, func(_ context.Context, _ mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
-		return []mcp.ResourceContents{
-			mcp.TextResourceContents{
-				URI:      "mqe://docs/ai_prompt",
-				MIMEType: "text/markdown",
-				Text:     mqeAIPromptDoc,
-			},
-		}, nil
-	})
-}
diff --git a/internal/resources/mqe_examples.json b/internal/resources/mqe_examples.json
deleted file mode 100644
index 80e4018..0000000
--- a/internal/resources/mqe_examples.json
+++ /dev/null
@@ -1,136 +0,0 @@
-{
-  "examples": [
-    {
-      "description": "Query service response time",
-      "natural_language": "What is the average response time for service 'frontend'?",
-      "mqe_expression": "avg(service_resp_time)",
-      "notes": "Returns average response time in milliseconds"
-    },
-    {
-      "description": "Service SLA percentage",
-      "natural_language": "Show me the success rate for service 'backend' as a percentage",
-      "mqe_expression": "service_sla * 100",
-      "notes": "Converts SLA from decimal (0-1) to percentage (0-100)"
-    },
-    {
-      "description": "Response time percentiles",
-      "natural_language": "Get the P50, P75, P90, P95, and P99 response times",
-      "mqe_expression": "service_percentile{p='50,75,90,95,99'}",
-      "notes": "Returns multiple percentile values with labels"
-    },
-    {
-      "description": "Rename percentile labels",
-      "natural_language": "Get response time percentiles with custom labels",
-      "mqe_expression": "relabel(service_percentile{p='50,75,90,95,99'}, p='50,75,90,95,99', percentile='P50,P75,P90,P95,P99')",
-      "notes": "Relabels the percentile values for better readability"
-    },
-    {
-      "description": "Calls per minute above threshold",
-      "natural_language": "Is the service receiving more than 1000 calls per minute?",
-      "mqe_expression": "service_cpm > 1000",
-      "notes": "Returns 1 if true, 0 if false"
-    },
-    {
-      "description": "Service response time alert condition",
-      "natural_language": "Alert when average response time is over 1 second",
-      "mqe_expression": "avg(service_resp_time) > 1000",
-      "notes": "Used in alarm rules, checks if average response time exceeds 1000ms"
-    },
-    {
-      "description": "Service SLA below threshold alert",
-      "natural_language": "Alert when service success rate drops below 85%",
-      "mqe_expression": "sum((service_sla * 100) < 85) >= 4",
-      "notes": "Triggers when SLA is below 85% for 4 or more time buckets"
-    },
-    {
-      "description": "Multiple percentiles above threshold",
-      "natural_language": "Alert when multiple percentiles exceed 1 second",
-      "mqe_expression": "sum(service_percentile{p='50,75,90,95,99'} > 1000) >= 3",
-      "notes": "Triggers when 3 or more percentiles exceed 1000ms"
-    },
-    {
-      "description": "Combined SLA and response time condition",
-      "natural_language": "Alert when both SLA is low AND response time is high",
-      "mqe_expression": "(avg(service_sla * 100) < 80) && (avg(service_percentile{p='50'}) > 1000)",
-      "notes": "Complex condition using boolean AND operator"
-    },
-    {
-      "description": "Top 10 services by CPM",
-      "natural_language": "Which are the top 10 busiest services by calls per minute?",
-      "mqe_expression": "top_n(service_cpm, 10, des)",
-      "notes": "Returns sorted list of top 10 services"
-    },
-    {
-      "description": "Top 10 instances of a service",
-      "natural_language": "Show the top 10 instances by CPM for a specific service",
-      "mqe_expression": "top_n(service_instance_cpm, 10, des)",
-      "notes": "Use with entity.serviceName to get instances of that service"
-    },
-    {
-      "description": "Filter top services by layer",
-      "natural_language": "Get top 10 general services by CPM",
-      "mqe_expression": "top_n(service_cpm, 10, des, attr0='GENERAL')",
-      "notes": "Filters results by service layer attribute"
-    },
-    {
-      "description": "HTTP error rate monitoring",
-      "natural_language": "Monitor 4xx and 5xx error rates",
-      "mqe_expression": "sum(aggregate_labels(meter_status_code{status='4xx,5xx'},sum) > 10) > 3",
-      "notes": "Aggregates error status codes and alerts on high error count"
-    },
-    {
-      "description": "Instance response time with regex filter",
-      "natural_language": "Monitor instances matching a pattern",
-      "mqe_expression": "avg(service_instance_resp_time)",
-      "notes": "Use with include-names-regex in alarm rules to filter instances"
-    },
-    {
-      "description": "Endpoint success rate monitoring",
-      "natural_language": "Alert when endpoint success rate is below 75%",
-      "mqe_expression": "sum((endpoint_sla * 100) < 75) >= 3",
-      "notes": "Monitors specific endpoint performance"
-    },
-    {
-      "description": "Service CPM in requests per second",
-      "natural_language": "Convert calls per minute to requests per second",
-      "mqe_expression": "round(service_cpm / 60, 2)",
-      "notes": "Converts CPM to RPS with 2 decimal places"
-    },
-    {
-      "description": "Service response time trend",
-      "natural_language": "Show the rate of change in response time",
-      "mqe_expression": "rate(service_resp_time, 5)",
-      "notes": "Calculates per-second rate of change over 5-minute window"
-    },
-    {
-      "description": "Response time increase over period",
-      "natural_language": "How much has response time increased in the last 2 minutes?",
-      "mqe_expression": "increase(service_resp_time, 2)",
-      "notes": "Shows absolute increase over time range"
-    },
-    {
-      "description": "Sort service response times",
-      "natural_language": "Get top 10 slowest response times",
-      "mqe_expression": "sort_values(service_resp_time, 10, des)",
-      "notes": "Sorts values in descending order, returns top 10"
-    },
-    {
-      "description": "First available metric fallback",
-      "natural_language": "Use service CPM or fallback to instance CPM",
-      "mqe_expression": "view_as_seq([service_cpm, service_instance_cpm])",
-      "notes": "Returns first non-null metric in sequence"
-    },
-    {
-      "description": "Check if metrics exist",
-      "natural_language": "Check if any monitoring data is available",
-      "mqe_expression": "is_present([service_cpm, service_resp_time, service_sla])",
-      "notes": "Returns 1 if any metric has values, 0 otherwise"
-    },
-    {
-      "description": "Baseline comparison",
-      "natural_language": "Compare response time to predicted baseline",
-      "mqe_expression": "sum(service_resp_time > baseline(service_resp_time, upper)) > 3",
-      "notes": "Alert when response time exceeds upper baseline prediction"
-    }
-  ]
-}
diff --git a/internal/swmcp/cors.go b/internal/swmcp/cors.go
index 8f1b0f9..22459b3 100644
--- a/internal/swmcp/cors.go
+++ b/internal/swmcp/cors.go
@@ -28,6 +28,13 @@ import (
 // When allowedOrigins is non-empty, only listed origins receive CORS headers;
 // requests from any other origin receive 403 Forbidden. Use "*" as an entry
 // to explicitly allow all origins via the wildcard header.
+// defaultAllowedHeaders covers the fixed headers the MCP transports read:
+// Mcp-Protocol-Version, Mcp-Method, and Mcp-Name are required by protocol
+// 2026-07-28; Mcp-Session-Id and Last-Event-ID are sent by clients on
+// earlier protocol revisions.
+const defaultAllowedHeaders = "Content-Type, Authorization, Accept, " +
+	"Mcp-Protocol-Version, Mcp-Method, Mcp-Name, Mcp-Session-Id, Last-Event-ID"
+
 func corsMiddleware(allowedOrigins []string, next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		origin := r.Header.Get("Origin")
@@ -35,8 +42,17 @@ func corsMiddleware(allowedOrigins []string, next http.Handler) http.Handler {
 			if len(allowedOrigins) == 0 || isOriginAllowed(origin, allowedOrigins) {
 				w.Header().Set("Access-Control-Allow-Origin", origin)
 				w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
-				w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept")
-				w.Header().Set("Vary", "Origin")
+				// Protocol 2026-07-28 mirrors tool parameters annotated with
+				// x-mcp-header into dynamic Mcp-Param-* request headers, so the
+				// allowed set cannot be enumerated statically; reflect whatever
+				// the preflight declares. Origin validation above remains the
+				// security gate.
+				if requested := r.Header.Get("Access-Control-Request-Headers"); requested != "" {
+					w.Header().Set("Access-Control-Allow-Headers", requested)
+				} else {
+					w.Header().Set("Access-Control-Allow-Headers", defaultAllowedHeaders)
+				}
+				w.Header().Set("Vary", "Origin, Access-Control-Request-Headers")
 			} else {
 				http.Error(w, "forbidden: origin not allowed", http.StatusForbidden)
 				return
diff --git a/internal/swmcp/cors_test.go b/internal/swmcp/cors_test.go
index fdaa1db..e4fe19c 100644
--- a/internal/swmcp/cors_test.go
+++ b/internal/swmcp/cors_test.go
@@ -20,6 +20,7 @@ package swmcp
 import (
 	"net/http"
 	"net/http/httptest"
+	"strings"
 	"testing"
 )
 
@@ -75,8 +76,8 @@ func TestCORSEmptyAllowlistReflectsAnyOrigin(t *testing.T) {
 	if got := rr.Header().Get("Access-Control-Allow-Origin"); got != openOrigin {
 		t.Fatalf("ACAO = %q, want reflected origin", got)
 	}
-	if got := rr.Header().Get("Vary"); got != "Origin" {
-		t.Fatalf("Vary = %q, want Origin", got)
+	if got := rr.Header().Get("Vary"); got != "Origin, Access-Control-Request-Headers" {
+		t.Fatalf("Vary = %q, want Origin and Access-Control-Request-Headers", got)
 	}
 }
 
@@ -180,6 +181,34 @@ func TestCORSPreflightEmptyAllowlist(t *testing.T) {
 	}
 }
 
+// Protocol 2026-07-28 clients send Mcp-Param-* headers derived from tool
+// parameters, so a preflight declaring them must be allowed as-is.
+func TestCORSPreflightReflectsRequestedHeaders(t *testing.T) {
+	sentinel := &sentinelHandler{}
+	handler := corsMiddleware(nil, sentinel)
+
+	req := httptest.NewRequest(http.MethodOptions, "/mcp", http.NoBody)
+	req.Header.Set("Origin", openOrigin)
+	req.Header.Set("Access-Control-Request-Headers", "mcp-method, mcp-name, mcp-param-region")
+	rr := httptest.NewRecorder()
+	handler.ServeHTTP(rr, req)
+
+	if got := rr.Header().Get("Access-Control-Allow-Headers"); got != "mcp-method, mcp-name, mcp-param-region" {
+		t.Fatalf("Allow-Headers = %q, want the requested headers reflected", got)
+	}
+}
+
+func TestCORSPreflightDefaultAllowsMCPHeaders(t *testing.T) {
+	rr, _ := corsRequest(http.MethodOptions, openOrigin, nil)
+
+	got := rr.Header().Get("Access-Control-Allow-Headers")
+	for _, header := range []string{"Mcp-Protocol-Version", "Mcp-Method", "Mcp-Name", "Mcp-Session-Id"} {
+		if !strings.Contains(got, header) {
+			t.Fatalf("Allow-Headers = %q, missing %q", got, header)
+		}
+	}
+}
+
 // --- parseAllowedOrigins ---
 
 func TestParseAllowedOrigins(t *testing.T) {
diff --git a/internal/swmcp/server.go b/internal/swmcp/server.go
index c24e888..48f1e87 100644
--- a/internal/swmcp/server.go
+++ b/internal/swmcp/server.go
@@ -20,30 +20,26 @@ package swmcp
 import (
 	"context"
 	"fmt"
-	"net/http"
 	"os"
 	"strings"
 
-	"github.com/mark3labs/mcp-go/server"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 	"github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
 
 	"github.com/apache/skywalking-cli/pkg/contextkey"
 
 	"github.com/apache/skywalking-mcp/internal/config"
 	"github.com/apache/skywalking-mcp/internal/prompts"
-	"github.com/apache/skywalking-mcp/internal/resources"
 	"github.com/apache/skywalking-mcp/internal/tools"
 )
 
-// newMCPServer creates a new MCP server with all tools, resources, and prompts registered.
-func newMCPServer() *server.MCPServer {
-	s := server.NewMCPServer(
-		"skywalking-mcp", "0.1.0",
-		server.WithResourceCapabilities(true, true),
-		server.WithPromptCapabilities(true),
-		server.WithLogging(),
-	)
+// newMCPServer creates a new MCP server with all tools and prompts registered.
+func newMCPServer() *mcp.Server {
+	s := mcp.NewServer(&mcp.Implementation{Name: "skywalking-mcp", Version: "0.1.0"}, nil)
+	s.AddReceivingMiddleware(skyWalkingContextMiddleware())
+
 	tools.AddTraceTools(s)
 	tools.AddLogTools(s)
 	tools.AddMQETools(s)
@@ -51,11 +47,24 @@ func newMCPServer() *server.MCPServer {
 	tools.AddEventTools(s)
 	tools.AddAlarmTools(s)
 	tools.AddTopologyTools(s)
-	resources.AddMQEResources(s)
 	prompts.AddSkyWalkingPrompts(s)
 	return s
 }
 
+// skyWalkingContextMiddleware injects the configured SkyWalking endpoint and
+// credentials into every incoming request. It replaces the per-transport
+// context funcs, which all resolved the same global configuration and ignored
+// their *http.Request argument.
+func skyWalkingContextMiddleware() mcp.Middleware {
+	return func(next mcp.MethodHandler) mcp.MethodHandler {
+		return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
+			ctx = WithSkyWalkingURLAndInsecure(ctx, configuredSkyWalkingURL(), viper.GetBool("insecure"))
+			ctx = withConfiguredAuth(ctx)
+			return next(ctx, method, req)
+		}
+	}
+}
+
 func initLogger(logFilePath string) (*logrus.Logger, error) {
 	if logFilePath == "" {
 		return logrus.New(), nil
@@ -114,6 +123,62 @@ func validateConfiguredSkyWalkingURL() error {
 	return err
 }
 
+// disableLocalhostProtectionUsage documents the flag shared by the HTTP
+// transports. The SDK rejects a loopback request whose Host header is not a
+// loopback name, which a same-host reverse proxy preserving the public Host
+// looks exactly like.
+const disableLocalhostProtectionUsage = "Disable DNS rebinding protection, which rejects loopback requests " +
+	"carrying a non-localhost Host header. Enable only behind a trusted reverse proxy that preserves the public Host."
+
+const allowedOriginsUsage = "Comma-separated allowed CORS origins. " +
+	"Empty = open (any origin reflected). Use * for wildcard header."
+
+// ConfigureEnv applies the environment variable conventions shared by the root
+// command's viper and the per-transport ones.
+func ConfigureEnv(v *viper.Viper) {
+	v.SetEnvPrefix("SW")
+	v.AutomaticEnv()
+	// All fields with . or - will be replaced with _ for ENV vars
+	v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
+}
+
+// bindHTTPTransportFlags registers the flags every HTTP transport accepts and
+// binds them to a viper private to that subcommand. The transports share flag
+// names, and viper keeps a single pflag binding per key, so binding them on the
+// global viper lets the subcommand registered last own every transport's
+// values.
+func bindHTTPTransportFlags(cmd *cobra.Command) *viper.Viper {
+	v := viper.New()
+	ConfigureEnv(v)
+
+	cmd.Flags().String("allowed-origins", "", allowedOriginsUsage)
+	cmd.Flags().Bool("disable-localhost-protection", false, disableLocalhostProtectionUsage)
+	_ = v.BindPFlag("allowed-origins", cmd.Flags().Lookup("allowed-origins"))
+	_ = v.BindPFlag("disable-localhost-protection", cmd.Flags().Lookup("disable-localhost-protection"))
+
+	return v
+}
+
+// httpTransportConfigFrom reads the shared HTTP transport settings.
+func httpTransportConfigFrom(v *viper.Viper) config.HTTPTransportConfig {
+	return config.HTTPTransportConfig{
+		AllowedOrigins:             parseAllowedOrigins(v.GetString("allowed-origins")),
+		DisableLocalhostProtection: v.GetBool("disable-localhost-protection"),
+	}
+}
+
+// normalizeHTTPPath applies the same normalization the previous SDK applied to
+// transport path flags: "mcp" and "/mcp/" both become "/mcp"; "" and "/"
+// become "". Without it, a value like "mcp" is a host pattern to ServeMux and
+// "" makes ServeMux panic.
+func normalizeHTTPPath(path string) string {
+	trimmed := strings.Trim(path, "/")
+	if trimmed == "" {
+		return ""
+	}
+	return "/" + trimmed
+}
+
 // resolveEnvVar resolves a value that may contain an environment variable reference
 // in the form ${VAR_NAME}. If the value matches this pattern, it returns the
 // environment variable's value. Otherwise, it returns the raw value as-is.
@@ -144,33 +209,3 @@ func withConfiguredAuth(ctx context.Context) context.Context {
 	}
 	return ctx
 }
-
-// EnhanceStdioContextFunc returns a StdioContextFunc that enriches the context
-// with SkyWalking settings from the global configuration.
-func EnhanceStdioContextFunc() server.StdioContextFunc {
-	return func(ctx context.Context) context.Context {
-		ctx = WithSkyWalkingURLAndInsecure(ctx, configuredSkyWalkingURL(), viper.GetBool("insecure"))
-		ctx = withConfiguredAuth(ctx)
-		return ctx
-	}
-}
-
-// EnhanceSSEContextFunc returns a SSEContextFunc that enriches the context
-// with SkyWalking settings from the CLI configuration and configured auth.
-func EnhanceSSEContextFunc() server.SSEContextFunc {
-	return func(ctx context.Context, _ *http.Request) context.Context {
-		ctx = WithSkyWalkingURLAndInsecure(ctx, configuredSkyWalkingURL(), viper.GetBool("insecure"))
-		ctx = withConfiguredAuth(ctx)
-		return ctx
-	}
-}
-
-// EnhanceHTTPContextFunc returns a HTTPContextFunc that enriches the context
-// with SkyWalking settings from the CLI configuration and configured auth.
-func EnhanceHTTPContextFunc() server.HTTPContextFunc {
-	return func(ctx context.Context, _ *http.Request) context.Context {
-		ctx = WithSkyWalkingURLAndInsecure(ctx, configuredSkyWalkingURL(), viper.GetBool("insecure"))
-		ctx = withConfiguredAuth(ctx)
-		return ctx
-	}
-}
diff --git a/internal/swmcp/server_registry_test.go b/internal/swmcp/server_registry_test.go
index ebc7073..10fc2f7 100644
--- a/internal/swmcp/server_registry_test.go
+++ b/internal/swmcp/server_registry_test.go
@@ -6,7 +6,7 @@
 // not use this file except in compliance with the License.
 // You may obtain a copy of the License at
 //
-//	http://www.apache.org/licenses/LICENSE-2.0
+//     http://www.apache.org/licenses/LICENSE-2.0
 //
 // Unless required by applicable law or agreed to in writing,
 // software distributed under the License is distributed on an
@@ -14,29 +14,57 @@
 // KIND, either express or implied.  See the License for the
 // specific language governing permissions and limitations
 // under the License.
+
 package swmcp
 
 import (
+	"context"
 	"reflect"
 	"sort"
 	"testing"
-	"unsafe"
 
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 )
 
-// These registry tests verify that newMCPServer wires up the expected tools,
-// prompts, and resources. Prefer MCPServer public APIs where available. As of
-// mcp-go v0.45.0 only tools have a public inventory API, so prompt/resource
-// assertions go through the helper layer below. If a future mcp-go release
-// exposes prompt/resource listing, replace the reflection helper there rather
-// than spreading reflect/unsafe access across tests.
+// These registry tests verify that newMCPServer wires up the expected tools and
+// prompts. They list them over a real in-memory session, so the assertions run
+// through the same path a client would.
+
+// newTestSession connects a client to newMCPServer over an in-memory transport.
+func newTestSession(t *testing.T) *mcp.ClientSession {
+	t.Helper()
+
+	ctx := context.Background()
+	serverTransport, clientTransport := mcp.NewInMemoryTransports()
+
+	serverSession, err := newMCPServer().Connect(ctx, serverTransport, nil)
+	if err != nil {
+		t.Fatalf("connect server: %v", err)
+	}
+	t.Cleanup(func() { _ = serverSession.Close() })
+
+	clientSession, err := mcp.NewClient(&mcp.Implementation{Name: "test", Version: "0.0.1"}, nil).
+		Connect(ctx, clientTransport, nil)
+	if err != nil {
+		t.Fatalf("connect client: %v", err)
+	}
+	t.Cleanup(func() { _ = clientSession.Close() })
+
+	return clientSession
+}
 
 func TestNewMCPServerRegistersExpectedTools(t *testing.T) {
-	srv := newMCPServer()
+	res, err := newTestSession(t).ListTools(context.Background(), nil)
+	if err != nil {
+		t.Fatalf("list tools: %v", err)
+	}
+
+	got := make([]string, 0, len(res.Tools))
+	for _, tool := range res.Tools {
+		got = append(got, tool.Name)
+	}
+	sort.Strings(got)
 
-	got := sortedToolNames(srv)
 	want := []string{
 		"execute_mqe_expression",
 		"get_mqe_metric_type",
@@ -60,10 +88,7 @@ func TestNewMCPServerRegistersExpectedTools(t *testing.T) {
 }
 
 func TestNewMCPServerRegistersExpectedPrompts(t *testing.T) {
-	srv := newMCPServer()
-	inventory := inspectServerInventory(srv)
-
-	got := sortedPromptNames(inventory.prompts)
+	got := sortedPromptNames(t)
 	want := []string{
 		"analyze-logs",
 		"analyze-performance",
@@ -80,30 +105,8 @@ func TestNewMCPServerRegistersExpectedPrompts(t *testing.T) {
 	assertStringSlicesEqual(t, got, want)
 }
 
-func TestNewMCPServerRegistersExpectedResources(t *testing.T) {
-	srv := newMCPServer()
-	inventory := inspectServerInventory(srv)
-
-	resources := inventory.resources
-	got := make([]string, 0, len(resources))
-	for uri := range resources {
-		got = append(got, uri)
-	}
-	sort.Strings(got)
-
-	want := []string{
-		"mqe://docs/ai_prompt",
-		"mqe://docs/examples",
-		"mqe://docs/syntax",
-		"mqe://metrics/available",
-	}
-
-	assertStringSlicesEqual(t, got, want)
-}
-
 func TestPromptMetadataIncludesExpectedArguments(t *testing.T) {
-	srv := newMCPServer()
-	prompts := inspectServerInventory(srv).prompts
+	prompts := promptMap(t)
 
 	prompt, ok := prompts["generate_duration"]
 	if !ok {
@@ -131,48 +134,23 @@ func TestPromptMetadataIncludesExpectedArguments(t *testing.T) {
 	}
 }
 
-func TestResourceMetadataIncludesExpectedMIMETypes(t *testing.T) {
-	srv := newMCPServer()
-	resources := inspectServerInventory(srv).resources
-
-	tests := []struct {
-		uri      string
-		name     string
-		mimeType string
-	}{
-		{uri: "mqe://docs/syntax", name: "MQE Detailed Syntax Rules", mimeType: "text/markdown"},
-		{uri: "mqe://docs/examples", name: "MQE Examples", mimeType: "application/json"},
-		{uri: "mqe://metrics/available", name: "Available Metrics", mimeType: "application/json"},
-		{uri: "mqe://docs/ai_prompt", name: "MQE AI Understanding Guide", mimeType: "text/markdown"},
+func TestToolMetadataIncludesExpectedDescriptionsAndSchemas(t *testing.T) {
+	res, err := newTestSession(t).ListTools(context.Background(), nil)
+	if err != nil {
+		t.Fatalf("list tools: %v", err)
 	}
 
-	for _, tc := range tests {
-		t.Run(tc.uri, func(t *testing.T) {
-			resource, ok := resources[tc.uri]
-			if !ok {
-				t.Fatalf("resource %q not registered", tc.uri)
-			}
-			if resource.Name != tc.name {
-				t.Fatalf("resource name = %q, want %q", resource.Name, tc.name)
-			}
-			if resource.MIMEType != tc.mimeType {
-				t.Fatalf("resource MIME type = %q, want %q", resource.MIMEType, tc.mimeType)
-			}
-		})
+	tools := make(map[string]*mcp.Tool, len(res.Tools))
+	for _, tool := range res.Tools {
+		tools[tool.Name] = tool
 	}
-}
-
-func TestToolMetadataIncludesExpectedDescriptionsAndSchemas(t *testing.T) {
-	srv := newMCPServer()
-	tools := toolMap(srv)
 
 	tests := []struct {
 		name             string
-		expectDesc       bool
 		expectProperties []string
 	}{
-		{name: "query_traces", expectDesc: true, expectProperties: []string{"service_id", "trace_id", "view"}},
-		{name: "execute_mqe_expression", expectDesc: true, expectProperties: []string{"expression", "service_name", "debug"}},
+		{name: "query_traces", expectProperties: []string{"service_id", "trace_id", "view"}},
+		{name: "execute_mqe_expression", expectProperties: []string{"expression", "service_name", "debug"}},
 	}
 
 	for _, tc := range tests {
@@ -181,10 +159,19 @@ func TestToolMetadataIncludesExpectedDescriptionsAndSchemas(t *testing.T) {
 			if !ok {
 				t.Fatalf("tool %q not registered", tc.name)
 			}
-			if tc.expectDesc && tool.Description == "" {
+			if tool.Description == "" {
 				t.Fatalf("tool %q description is empty", tc.name)
 			}
-			properties := tool.InputSchema.Properties
+
+			// Over the wire the schema is plain JSON, not a *jsonschema.Schema.
+			schema, ok := tool.InputSchema.(map[string]any)
+			if !ok {
+				t.Fatalf("tool %q input schema type = %T", tc.name, tool.InputSchema)
+			}
+			properties, ok := schema["properties"].(map[string]any)
+			if !ok {
+				t.Fatalf("tool %q input schema has no properties", tc.name)
+			}
 			for _, property := range tc.expectProperties {
 				if _, ok := properties[property]; !ok {
 					t.Fatalf("tool %q missing input schema property %q", tc.name, property)
@@ -194,27 +181,25 @@ func TestToolMetadataIncludesExpectedDescriptionsAndSchemas(t *testing.T) {
 	}
 }
 
-func toolMap(srv *server.MCPServer) map[string]mcp.Tool {
-	serverTools := srv.ListTools()
-	result := make(map[string]mcp.Tool, len(serverTools))
-	for name, tool := range serverTools {
-		result[name] = tool.Tool
-	}
+func promptMap(t *testing.T) map[string]*mcp.Prompt {
+	t.Helper()
 
-	return result
-}
+	res, err := newTestSession(t).ListPrompts(context.Background(), nil)
+	if err != nil {
+		t.Fatalf("list prompts: %v", err)
+	}
 
-func sortedToolNames(srv *server.MCPServer) []string {
-	tools := toolMap(srv)
-	names := make([]string, 0, len(tools))
-	for name := range tools {
-		names = append(names, name)
+	prompts := make(map[string]*mcp.Prompt, len(res.Prompts))
+	for _, prompt := range res.Prompts {
+		prompts[prompt.Name] = prompt
 	}
-	sort.Strings(names)
-	return names
+	return prompts
 }
 
-func sortedPromptNames(prompts map[string]mcp.Prompt) []string {
+func sortedPromptNames(t *testing.T) []string {
+	t.Helper()
+
+	prompts := promptMap(t)
 	names := make([]string, 0, len(prompts))
 	for name := range prompts {
 		names = append(names, name)
@@ -229,63 +214,3 @@ func assertStringSlicesEqual(t *testing.T, got, want []string) {
 		t.Fatalf("values mismatch:\n got: %v\nwant: %v", got, want)
 	}
 }
-
-func readPrivateField(v reflect.Value) reflect.Value {
-	return reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Elem()
-}
-
-func testedServerValue(srv *server.MCPServer) reflect.Value {
-	return reflect.ValueOf(srv).Elem()
-}
-
-func mustReadServerField(srv reflect.Value, fieldName string) reflect.Value {
-	field := srv.FieldByName(fieldName)
-	if !field.IsValid() {
-		panic("mcp-go MCPServer no longer has field " + fieldName)
-	}
-	return readPrivateField(field)
-}
-
-type serverInventory struct {
-	prompts   map[string]mcp.Prompt
-	resources map[string]mcp.Resource
-}
-
-func inspectServerInventory(srv *server.MCPServer) serverInventory {
-	serverValue := testedServerValue(srv)
-	return serverInventory{
-		prompts:   readPromptMap(serverValue),
-		resources: readResourceMap(serverValue),
-	}
-}
-
-func readPromptMap(serverValue reflect.Value) map[string]mcp.Prompt {
-	serverPrompts := mustReadServerField(serverValue, "prompts")
-	result := make(map[string]mcp.Prompt, serverPrompts.Len())
-
-	iter := serverPrompts.MapRange()
-	for iter.Next() {
-		result[iter.Key().String()] = copyReflectValue(iter.Value()).Interface().(mcp.Prompt)
-	}
-
-	return result
-}
-
-func readResourceMap(serverValue reflect.Value) map[string]mcp.Resource {
-	serverResources := mustReadServerField(serverValue, "resources")
-	result := make(map[string]mcp.Resource, serverResources.Len())
-
-	iter := serverResources.MapRange()
-	for iter.Next() {
-		resourceField := copyReflectValue(iter.Value()).FieldByName("resource")
-		result[iter.Key().String()] = readPrivateField(resourceField).Interface().(mcp.Resource)
-	}
-
-	return result
-}
-
-func copyReflectValue(v reflect.Value) reflect.Value {
-	cloned := reflect.New(v.Type()).Elem()
-	cloned.Set(v)
-	return cloned
-}
diff --git a/internal/swmcp/server_test.go b/internal/swmcp/server_test.go
index 8ec433e..bb39461 100644
--- a/internal/swmcp/server_test.go
+++ b/internal/swmcp/server_test.go
@@ -22,6 +22,7 @@ import (
 	"testing"
 
 	"github.com/apache/skywalking-cli/pkg/contextkey"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
 
@@ -147,14 +148,31 @@ func TestWithConfiguredAuthSkipsEmptyUsername(t *testing.T) {
 	}
 }
 
-func TestEnhanceStdioContextFuncUsesConfiguredURLAndAuth(t *testing.T) {
+// middlewareContext runs the context middleware over req and returns the
+// context it handed downstream. One middleware now serves every transport, so
+// the injection behavior only needs asserting once.
+func middlewareContext(t *testing.T, req mcp.Request) context.Context {
+	t.Helper()
+
+	var got context.Context
+	next := func(ctx context.Context, _ string, _ mcp.Request) (mcp.Result, error) {
+		got = ctx
+		return nil, nil
+	}
+	if _, err := skyWalkingContextMiddleware()(next)(context.Background(), "tools/call", req); err != nil {
+		t.Fatalf("middleware: %v", err)
+	}
+	return got
+}
+
+func TestSkyWalkingContextMiddlewareInjectsURLAndAuth(t *testing.T) {
 	t.Cleanup(viper.Reset)
 	t.Setenv("SW_STDIO_PASS", "stdio-pass")
 	viper.Set("url", "https://configured-oap.example.com")
 	viper.Set("username", "stdio-user")
 	viper.Set("password", "${SW_STDIO_PASS}")
 
-	ctx := EnhanceStdioContextFunc()(context.Background())
+	ctx := middlewareContext(t, nil)
 
 	gotURL, _ := ctx.Value(contextkey.BaseURL{}).(string)
 	if gotURL != configuredHTTPSOAPURL {
@@ -172,56 +190,28 @@ func TestEnhanceStdioContextFuncUsesConfiguredURLAndAuth(t *testing.T) {
 	}
 }
 
-func TestEnhanceHTTPContextFuncDoesNotUseSWURLHeader(t *testing.T) {
+// A client must not be able to redirect queries to another OAP through request
+// headers.
+func TestSkyWalkingContextMiddlewareIgnoresSWURLHeader(t *testing.T) {
 	t.Cleanup(viper.Reset)
 	viper.Set("url", "http://configured-oap:12800")
 
-	req, err := http.NewRequest(http.MethodPost, "http://client/request", http.NoBody)
-	if err != nil {
-		t.Fatalf("create request: %v", err)
+	req := &mcp.CallToolRequest{
+		Extra: &mcp.RequestExtra{Header: http.Header{"SW-URL": {"http://attacker.invalid:8080"}}},
 	}
-	req.Header.Set("SW-URL", "http://attacker.invalid:8080")
-
-	ctx := EnhanceHTTPContextFunc()(context.Background(), req)
 
-	gotURL, _ := ctx.Value(contextkey.BaseURL{}).(string)
+	gotURL, _ := middlewareContext(t, req).Value(contextkey.BaseURL{}).(string)
 	if gotURL != configuredHTTPOAPURL {
 		t.Fatalf("base URL = %q", gotURL)
 	}
 }
 
-func TestEnhanceSSEContextFuncDoesNotUseSWURLHeader(t *testing.T) {
-	t.Cleanup(viper.Reset)
-	viper.Set("url", "https://configured-oap.example.com")
-
-	req, err := http.NewRequest(http.MethodGet, "http://client/events", http.NoBody)
-	if err != nil {
-		t.Fatalf("create request: %v", err)
-	}
-	req.Header.Set("SW-URL", "https://attacker.invalid")
-
-	ctx := EnhanceSSEContextFunc()(context.Background(), req)
-
-	gotURL, _ := ctx.Value(contextkey.BaseURL{}).(string)
-	if gotURL != configuredHTTPSOAPURL {
-		t.Fatalf("base URL = %q", gotURL)
-	}
-}
-
 func TestInsecureFlagDefaultsToFalse(t *testing.T) {
 	t.Cleanup(viper.Reset)
 
-	req, _ := http.NewRequest(http.MethodGet, "http://client/events", http.NoBody)
-
-	for name, ctx := range map[string]context.Context{
-		"stdio":      EnhanceStdioContextFunc()(context.Background()),
-		"sse":        EnhanceSSEContextFunc()(context.Background(), req),
-		"streamable": EnhanceHTTPContextFunc()(context.Background(), req),
-	} {
-		insecure, _ := ctx.Value(contextkey.Insecure{}).(bool)
-		if insecure {
-			t.Errorf("%s: contextkey.Insecure{} should default to false", name)
-		}
+	insecure, _ := middlewareContext(t, nil).Value(contextkey.Insecure{}).(bool)
+	if insecure {
+		t.Error("contextkey.Insecure{} should default to false")
 	}
 }
 
@@ -229,16 +219,8 @@ func TestInsecureFlagPropagatedToContext(t *testing.T) {
 	t.Cleanup(viper.Reset)
 	viper.Set("insecure", true)
 
-	req, _ := http.NewRequest(http.MethodGet, "http://client/events", http.NoBody)
-
-	for name, ctx := range map[string]context.Context{
-		"stdio":      EnhanceStdioContextFunc()(context.Background()),
-		"sse":        EnhanceSSEContextFunc()(context.Background(), req),
-		"streamable": EnhanceHTTPContextFunc()(context.Background(), req),
-	} {
-		insecure, ok := ctx.Value(contextkey.Insecure{}).(bool)
-		if !ok || !insecure {
-			t.Errorf("%s: contextkey.Insecure{} should be true when viper insecure=true", name)
-		}
+	insecure, ok := middlewareContext(t, nil).Value(contextkey.Insecure{}).(bool)
+	if !ok || !insecure {
+		t.Error("contextkey.Insecure{} should be true when viper insecure=true")
 	}
 }
diff --git a/internal/swmcp/sse.go b/internal/swmcp/sse.go
index 0dab635..d2d4e3d 100644
--- a/internal/swmcp/sse.go
+++ b/internal/swmcp/sse.go
@@ -28,44 +28,74 @@ import (
 	"syscall"
 	"time"
 
-	"github.com/mark3labs/mcp-go/server"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
 
 	"github.com/apache/skywalking-mcp/internal/config"
 )
 
 func NewSSEServer() *cobra.Command {
+	cmd, _ := newSSECommand()
+	return cmd
+}
+
+// newSSECommand also returns the accessor for the configuration the command
+// runs with, so callers observe exactly what RunE consumes.
+func newSSECommand() (cmd *cobra.Command, cfg func() config.SSEServerConfig) {
 	sseCmd := &cobra.Command{
 		Use:   "sse",
 		Short: "Start SSE server",
 		Long:  `Start a server that listens for Server-Sent Events (SSE) on the specified address.`,
-		RunE: func(_ *cobra.Command, _ []string) error {
-			if err := validateConfiguredSkyWalkingURL(); err != nil {
-				return err
-			}
-
-			sseServerConfig := config.SSEServerConfig{
-				Address:  viper.GetString("sse-address"),
-				BasePath: viper.GetString("base-path"),
-			}
-
-			return runSSEServer(context.Background(), &sseServerConfig)
-		},
 	}
 
 	// Add SSE server specific flags
+	v := bindHTTPTransportFlags(sseCmd)
 	sseCmd.Flags().String("sse-address", "localhost:8000",
 		"The host and port to start the sse server on")
 	sseCmd.Flags().String("base-path", "",
 		"Base path for the sse server")
-	sseCmd.Flags().String("allowed-origins", "",
-		"Comma-separated allowed CORS origins. Empty = open (any origin reflected). Use * for wildcard header.")
-	_ = viper.BindPFlag("sse-address", sseCmd.Flags().Lookup("sse-address"))
-	_ = viper.BindPFlag("base-path", sseCmd.Flags().Lookup("base-path"))
-	_ = viper.BindPFlag("allowed-origins", sseCmd.Flags().Lookup("allowed-origins"))
+	_ = v.BindPFlag("sse-address", sseCmd.Flags().Lookup("sse-address"))
+	_ = v.BindPFlag("base-path", sseCmd.Flags().Lookup("base-path"))
+
+	sseConfig := func() config.SSEServerConfig {
+		return config.SSEServerConfig{
+			Address:             v.GetString("sse-address"),
+			BasePath:            v.GetString("base-path"),
+			HTTPTransportConfig: httpTransportConfigFrom(v),
+		}
+	}
+
+	sseCmd.RunE = func(_ *cobra.Command, _ []string) error {
+		if err := validateConfiguredSkyWalkingURL(); err != nil {
+			return err
+		}
+
+		sseServerConfig := sseConfig()
 
-	return sseCmd
+		return runSSEServer(context.Background(), &sseServerConfig)
+	}
+
+	return sseCmd, sseConfig
+}
+
+// newSSEHandler builds the SSE handler for cfg. The HTTP+SSE transport
+// predates streamable HTTP and is kept for clients that still speak it.
+func newSSEHandler(cfg *config.SSEServerConfig) http.Handler {
+	srv := newMCPServer()
+	return mcp.NewSSEHandler(
+		func(*http.Request) *mcp.Server { return srv },
+		&mcp.SSEOptions{DisableLocalhostProtection: cfg.DisableLocalhostProtection},
+	)
+}
+
+// newSSEMux wires the served handler: the SSE handler, the CORS and origin
+// check around it, and the base path route. It also reports the SSE endpoint.
+func newSSEMux(cfg *config.SSEServerConfig) (handler http.Handler, ssePath string) {
+	basePath := normalizeHTTPPath(cfg.BasePath)
+	mux := http.NewServeMux()
+	mux.Handle(basePath+"/", corsMiddleware(cfg.AllowedOrigins, newSSEHandler(cfg)))
+
+	return mux, basePath + "/sse"
 }
 
 // runSSEServer starts a server that listens for Server-Sent Events (SSE) on the specified address.
@@ -78,31 +108,19 @@ func runSSEServer(ctx context.Context, cfg *config.SSEServerConfig) error {
 		return fmt.Errorf("failed to initialize logger: %w", err)
 	}
 
-	allowedOrigins := parseAllowedOrigins(viper.GetString("allowed-origins"))
+	mux, ssePath := newSSEMux(cfg)
 
-	// sseServer is assigned after NewSSEServer so the CORS handler closure
-	// can forward to it via the captured pointer variable.
-	var sseServerRef *server.SSEServer
-	customSrv := &http.Server{
-		Handler: corsMiddleware(allowedOrigins, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			sseServerRef.ServeHTTP(w, r)
-		})),
+	sseServer := &http.Server{
+		Addr:              cfg.Address,
+		Handler:           mux,
 		ReadHeaderTimeout: 10 * time.Second,
 	}
 
-	sseServer := server.NewSSEServer(
-		newMCPServer(),
-		server.WithStaticBasePath(cfg.BasePath),
-		server.WithSSEContextFunc(EnhanceSSEContextFunc()),
-		server.WithHTTPServer(customSrv),
-	)
-	sseServerRef = sseServer
-	ssePath := sseServer.CompleteSsePath()
 	log.Printf("Starting SkyWalking MCP server using SSE transport listening on http://%s%s\n ", cfg.Address, ssePath)
 
 	errCh := make(chan error, 1)
 	go func() {
-		if err := sseServer.Start(cfg.Address); err != nil && !errors.Is(err, http.ErrServerClosed) {
+		if err := sseServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
 			errCh <- err // bubble up real crashes
 		}
 	}()
@@ -120,25 +138,19 @@ func runSSEServer(ctx context.Context, cfg *config.SSEServerConfig) error {
 		return fmt.Errorf("sse server error: %w", err)
 	}
 
-	// Graceful shutdown
+	// Graceful shutdown. SSE streams never become idle, so Shutdown alone
+	// would wait out its full deadline whenever a client is still connected;
+	// force-close whatever outlives it.
 	shCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
 	defer cancel()
 
-	// First try to shut down the SSE server
 	if err := sseServer.Shutdown(shCtx); err != nil {
 		if !errors.Is(err, http.ErrServerClosed) {
-			logger.Errorf("Error shutting down SSE server: %v", err)
+			logger.Infof("closing remaining SSE connections: %v", err)
+			_ = sseServer.Close()
 		}
 	}
 
-	// Wait for any remaining operations to complete
-	select {
-	case <-shCtx.Done():
-		return fmt.Errorf("shutdown timed out")
-	case <-time.After(100 * time.Millisecond):
-		// Give a small grace period for cleanup
-	}
-
 	_, _ = fmt.Fprintln(os.Stderr, "SSE server stopped gracefully")
 	return nil
 }
diff --git a/internal/swmcp/sse_test.go b/internal/swmcp/sse_test.go
new file mode 100644
index 0000000..d54debc
--- /dev/null
+++ b/internal/swmcp/sse_test.go
@@ -0,0 +1,92 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package swmcp
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/apache/skywalking-mcp/internal/config"
+)
+
+// serveSSE serves the mux runSSEServer builds, over the command's own defaults,
+// for the same reason serveStreamable does.
+func serveSSE(t *testing.T, httpConfig config.HTTPTransportConfig) *transportServer {
+	t.Helper()
+
+	_, defaults := newSSECommand()
+	cfg := defaults()
+	cfg.HTTPTransportConfig = httpConfig
+
+	mux, ssePath := newSSEMux(&cfg)
+	return serveLoopback(t, mux, ssePath)
+}
+
+// The base path has to reach the routes, so the SSE endpoint sits under it and
+// nothing answers outside it.
+func TestSSEServesConfiguredBasePath(t *testing.T) {
+	cfg := config.SSEServerConfig{BasePath: "/custom"}
+	mux, ssePath := newSSEMux(&cfg)
+	if ssePath != "/custom/sse" {
+		t.Fatalf("sse path = %q, want /custom/sse", ssePath)
+	}
+
+	testServer := serveLoopback(t, mux, ssePath)
+	host := testServer.Listener.Addr().String()
+
+	if got := postStatus(t, testServer, host, ""); got == http.StatusNotFound {
+		t.Fatal("status = 404 on the configured base path, want the handler to answer")
+	}
+	if got := postStatusTo(t, testServer, testServer.URL+"/", host, ""); got != http.StatusNotFound {
+		t.Fatalf("status = %d on the listener root, want 404", got)
+	}
+}
+
+func TestSSERejectsProxiedHostByDefault(t *testing.T) {
+	testServer := serveSSE(t, config.HTTPTransportConfig{})
+
+	if got := postStatus(t, testServer, proxiedHost, ""); got != http.StatusForbidden {
+		t.Fatalf("status = %d, want 403 for a non-localhost Host on a loopback listener", got)
+	}
+}
+
+// Without a session the SSE handler rejects the POST, but a 403 would mean the
+// rebinding check fired instead — that is the property under test.
+func TestSSEAcceptsProxiedHostWhenProtectionDisabled(t *testing.T) {
+	testServer := serveSSE(t, config.HTTPTransportConfig{DisableLocalhostProtection: true})
+
+	if got := postStatus(t, testServer, proxiedHost, ""); got == http.StatusForbidden {
+		t.Fatal("status = 403, want the request to get past localhost protection")
+	}
+}
+
+// The sse transport must apply its origin allowlist too; it silently did not
+// before the per-transport config landed. The allowed origin must also get
+// through: without a session the SSE handler still rejects the POST, but a 403
+// would mean the allowlist over-blocked it.
+func TestSSEHonorsOriginAllowlist(t *testing.T) {
+	testServer := serveSSE(t, config.HTTPTransportConfig{AllowedOrigins: []string{allowedOrigin}})
+	host := testServer.Listener.Addr().String()
+
+	if got := postStatus(t, testServer, host, allowedOrigin); got == http.StatusForbidden {
+		t.Fatal("allowed origin status = 403, want the request past the origin allowlist")
+	}
+	if got := postStatus(t, testServer, host, "https://evil.example"); got != http.StatusForbidden {
+		t.Fatalf("disallowed origin status = %d, want 403", got)
+	}
+}
diff --git a/internal/swmcp/stdio.go b/internal/swmcp/stdio.go
index 12fe05b..e6fe3e2 100644
--- a/internal/swmcp/stdio.go
+++ b/internal/swmcp/stdio.go
@@ -18,23 +18,61 @@
 package swmcp
 
 import (
+	"bytes"
 	"context"
 	"fmt"
 	"io"
-	"log"
 	"log/slog"
 	"os"
 	"os/signal"
+	"regexp"
+	"sync"
 	"syscall"
 
-	"github.com/mark3labs/mcp-go/server"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
 
 	"github.com/apache/skywalking-mcp/internal/config"
-	"github.com/apache/skywalking-mcp/internal/tools"
 )
 
+// sensitiveFieldPattern matches JSON fields whose values should be redacted in logs.
+var sensitiveFieldPattern = regexp.MustCompile(`(?i)("(?:authorization|password|token|secret)"\s*:\s*")((?:[^"\\]|\\.)*)(")`) //nolint:lll // regex must be on one line
+
+// redactingWriter masks values of sensitive JSON fields before forwarding to
+// the log writer. Data is buffered per line so the redaction regex always sees
+// a full message and secrets split across writes are never partially logged.
+// It is safe for concurrent use.
+type redactingWriter struct {
+	w   io.Writer
+	mu  sync.Mutex
+	buf bytes.Buffer
+}
+
+func (r *redactingWriter) Write(p []byte) (int, error) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+
+	r.buf.Write(p)
+	data := r.buf.Bytes()
+	var werr error
+	for werr == nil {
+		idx := bytes.IndexByte(data, '\n')
+		if idx < 0 {
+			break
+		}
+		line := sensitiveFieldPattern.ReplaceAll(data[:idx+1], []byte(`${1}[REDACTED]${3}`))
+		// A failed line is dropped rather than retried: keeping it buffered
+		// would rewrite the lines already logged on the next call.
+		_, werr = r.w.Write(line)
+		data = data[idx+1:]
+	}
+	rest := append([]byte(nil), data...)
+	r.buf.Reset()
+	r.buf.Write(rest)
+	return len(p), werr
+}
+
 func NewStdioServer() *cobra.Command {
 	return &cobra.Command{
 		Use:   "stdio",
@@ -64,28 +102,20 @@ func runStdioServer(ctx context.Context, cfg *config.StdioServerConfig) error {
 	ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
 	defer stop()
 
-	stdioServer := server.NewStdioServer(newMCPServer())
-
 	logger, err := initLogger(cfg.LogFilePath)
 	if err != nil {
 		return fmt.Errorf("failed to initialize logger: %w", err)
 	}
 
-	stdLogger := log.New(logger.Writer(), "swmcp-stdio-server", 0)
-	stdioServer.SetErrorLogger(stdLogger)
-	stdioServer.SetContextFunc(EnhanceStdioContextFunc())
+	var transport mcp.Transport = &mcp.StdioTransport{}
+	if cfg.LogCommands {
+		transport = &mcp.LoggingTransport{Transport: transport, Writer: &redactingWriter{w: logger.Writer()}}
+	}
 
 	// Start listening for messages
 	errC := make(chan error, 1)
 	go func() {
-		in, out := io.Reader(os.Stdin), io.Writer(os.Stdout)
-
-		if cfg.LogCommands {
-			loggedIO := tools.NewIOLogger(in, out, logger)
-			in, out = loggedIO, loggedIO
-		}
-
-		errC <- stdioServer.Listen(ctx, in, out)
+		errC <- newMCPServer().Run(ctx, transport)
 	}()
 
 	_, _ = fmt.Fprintf(os.Stderr, "SkyWalking MCP Server running on stdio\n")
diff --git a/internal/swmcp/stdio_test.go b/internal/swmcp/stdio_test.go
new file mode 100644
index 0000000..f6b431a
--- /dev/null
+++ b/internal/swmcp/stdio_test.go
@@ -0,0 +1,90 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package swmcp
+
+import (
+	"bytes"
+	"errors"
+	"strings"
+	"testing"
+)
+
+type failingWriter struct {
+	fail bool
+	out  bytes.Buffer
+}
+
+func (f *failingWriter) Write(p []byte) (int, error) {
+	if f.fail {
+		return 0, errors.New("writer unavailable")
+	}
+	return f.out.Write(p)
+}
+
+// A transient log-writer failure must not replay already-logged lines once
+// writes succeed again.
+func TestRedactingWriterDoesNotReplayLinesAfterWriteError(t *testing.T) {
+	out := &failingWriter{}
+	w := &redactingWriter{w: out}
+
+	if _, err := w.Write([]byte("first\n")); err != nil {
+		t.Fatalf("write: %v", err)
+	}
+	out.fail = true
+	if _, err := w.Write([]byte("second\n")); err == nil {
+		t.Fatal("expected error from failing writer")
+	}
+	out.fail = false
+	if _, err := w.Write([]byte("third\n")); err != nil {
+		t.Fatalf("write: %v", err)
+	}
+
+	if got := out.out.String(); got != "first\nthird\n" {
+		t.Fatalf("logged lines = %q, want %q", got, "first\nthird\n")
+	}
+}
+
+func TestRedactingWriterMasksSensitiveFields(t *testing.T) {
+	var out bytes.Buffer
+	w := &redactingWriter{w: &out}
+
+	// The secret is split across two writes: nothing may reach the underlying
+	// writer until the line is complete and redacted.
+	if _, err := w.Write([]byte(`read: {"password":"hun`)); err != nil {
+		t.Fatalf("write: %v", err)
+	}
+	if out.Len() != 0 {
+		t.Fatalf("partial line leaked to the log: %q", out.String())
+	}
+	if _, err := w.Write([]byte(`ter2","Authorization":"Bearer s3cr3t","user":"u"}` + "\n")); err != nil {
+		t.Fatalf("write: %v", err)
+	}
+
+	got := out.String()
+	for _, leaked := range []string{"hunter2", "s3cr3t"} {
+		if strings.Contains(got, leaked) {
+			t.Fatalf("secret %q leaked to the log: %q", leaked, got)
+		}
+	}
+	if !strings.Contains(got, `"password":"[REDACTED]"`) || !strings.Contains(got, `"Authorization":"[REDACTED]"`) {
+		t.Fatalf("sensitive fields not redacted: %q", got)
+	}
+	if !strings.Contains(got, `"user":"u"`) {
+		t.Fatalf("non-sensitive field mangled: %q", got)
+	}
+}
diff --git a/internal/swmcp/streamable.go b/internal/swmcp/streamable.go
index 5d692ad..11eea6b 100644
--- a/internal/swmcp/streamable.go
+++ b/internal/swmcp/streamable.go
@@ -22,74 +22,104 @@ import (
 	"net/http"
 	"time"
 
-	"github.com/mark3labs/mcp-go/server"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
-	"github.com/spf13/viper"
 
 	"github.com/apache/skywalking-mcp/internal/config"
 )
 
 func NewStreamable() *cobra.Command {
+	cmd, _ := newStreamableCommand()
+	return cmd
+}
+
+// newStreamableCommand also returns the accessor for the configuration the
+// command runs with, so callers observe exactly what RunE consumes.
+func newStreamableCommand() (cmd *cobra.Command, cfg func() config.StreamableServerConfig) {
 	streamableCmd := &cobra.Command{
 		Use:   "streamable",
 		Short: "Start Streamable server",
 		Long:  `Starting SkyWalking MCP server with Streamable HTTP transport.`,
-		RunE: func(_ *cobra.Command, _ []string) error {
-			if err := validateConfiguredSkyWalkingURL(); err != nil {
-				return err
-			}
-
-			streamableConfig := config.StreamableServerConfig{
-				Address:      viper.GetString("address"),
-				EndpointPath: viper.GetString("endpoint-path"),
-			}
-
-			return runStreamableServer(&streamableConfig)
-		},
 	}
 
 	// Add Streamable server specific flags
+	v := bindHTTPTransportFlags(streamableCmd)
 	streamableCmd.Flags().String("address", "localhost:8000",
 		"The host and port to start the Streamable server on")
 	streamableCmd.Flags().String("endpoint-path", "/mcp",
 		"The path for the streamable-http server")
-	streamableCmd.Flags().String("allowed-origins", "",
-		"Comma-separated allowed CORS origins. Empty = open (any origin reflected). Use * for wildcard header.")
-	_ = viper.BindPFlag("address", streamableCmd.Flags().Lookup("address"))
-	_ = viper.BindPFlag("endpoint-path", streamableCmd.Flags().Lookup("endpoint-path"))
-	_ = viper.BindPFlag("allowed-origins", streamableCmd.Flags().Lookup("allowed-origins"))
+	_ = v.BindPFlag("address", streamableCmd.Flags().Lookup("address"))
+	_ = v.BindPFlag("endpoint-path", streamableCmd.Flags().Lookup("endpoint-path"))
+
+	streamableConfig := func() config.StreamableServerConfig {
+		return config.StreamableServerConfig{
+			Address:             v.GetString("address"),
+			EndpointPath:        v.GetString("endpoint-path"),
+			HTTPTransportConfig: httpTransportConfigFrom(v),
+		}
+	}
+
+	streamableCmd.RunE = func(_ *cobra.Command, _ []string) error {
+		if err := validateConfiguredSkyWalkingURL(); err != nil {
+			return err
+		}
+
+		cfg := streamableConfig()
+
+		return runStreamableServer(&cfg)
+	}
+
+	return streamableCmd, streamableConfig
+}
+
+// newStreamableHandler builds the streamable HTTP handler for cfg.
+func newStreamableHandler(cfg *config.StreamableServerConfig) http.Handler {
+	// Stateless is required for protocol version 2026-07-28, which carries
+	// identity and capabilities per request instead of in a session.
+	srv := newMCPServer()
+	return mcp.NewStreamableHTTPHandler(
+		func(*http.Request) *mcp.Server { return srv },
+		&mcp.StreamableHTTPOptions{
+			Stateless:                  true,
+			DisableLocalhostProtection: cfg.DisableLocalhostProtection,
+		},
+	)
+}
+
+// newStreamableMux wires the served handler: the MCP handler, the CORS and
+// origin check around it, and the endpoint routes.
+func newStreamableMux(cfg *config.StreamableServerConfig) (handler http.Handler, endpoint string) {
+	endpointPath := normalizeHTTPPath(cfg.EndpointPath)
+	if endpointPath == "" {
+		endpointPath = "/"
+	}
+
+	wrapped := corsMiddleware(cfg.AllowedOrigins, newStreamableHandler(cfg))
+	mux := http.NewServeMux()
+	mux.Handle(endpointPath, wrapped)
+	if endpointPath != "/" {
+		// Accept the trailing-slash form too; the previous server did not
+		// route on the path at all.
+		mux.Handle(endpointPath+"/", wrapped)
+	}
 
-	return streamableCmd
+	return mux, endpointPath
 }
 
 // runStreamableServer starts the Streamable server with the provided configuration.
 func runStreamableServer(cfg *config.StreamableServerConfig) error {
-	allowedOrigins := parseAllowedOrigins(viper.GetString("allowed-origins"))
-
-	// httpServer is assigned after NewStreamableHTTPServer so the CORS handler
-	// closure can forward to it via the captured pointer variable.
-	var httpServerRef *server.StreamableHTTPServer
-	customSrv := &http.Server{
-		Handler: corsMiddleware(allowedOrigins, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			httpServerRef.ServeHTTP(w, r)
-		})),
+	mux, endpointPath := newStreamableMux(cfg)
+
+	httpServer := &http.Server{
+		Addr:              cfg.Address,
+		Handler:           mux,
 		ReadHeaderTimeout: 10 * time.Second,
 	}
 
-	httpServer := server.NewStreamableHTTPServer(
-		newMCPServer(),
-		server.WithStateLess(true),
-		server.WithLogger(log.StandardLogger()),
-		server.WithHTTPContextFunc(EnhanceHTTPContextFunc()),
-		server.WithEndpointPath(viper.GetString("endpoint-path")),
-		server.WithStreamableHTTPServer(customSrv),
-	)
-	httpServerRef = httpServer
-
-	log.Infof("streamable HTTP server listening on %s%s\n", cfg.Address, cfg.EndpointPath)
+	log.Infof("streamable HTTP server listening on %s%s\n", cfg.Address, endpointPath)
 
-	if err := httpServer.Start(cfg.Address); err != nil {
+	if err := httpServer.ListenAndServe(); err != nil {
 		return fmt.Errorf("streamable HTTP server error: %v", err)
 	}
 
diff --git a/internal/swmcp/streamable_test.go b/internal/swmcp/streamable_test.go
new file mode 100644
index 0000000..54d935f
--- /dev/null
+++ b/internal/swmcp/streamable_test.go
@@ -0,0 +1,115 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package swmcp
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/apache/skywalking-mcp/internal/config"
+)
+
+// serveStreamable serves the mux runStreamableServer builds, over the command's
+// own defaults, so the tests reach the endpoint production serves rather than
+// the listener root.
+func serveStreamable(t *testing.T, httpConfig config.HTTPTransportConfig) *transportServer {
+	t.Helper()
+
+	_, defaults := newStreamableCommand()
+	cfg := defaults()
+	cfg.HTTPTransportConfig = httpConfig
+
+	mux, endpoint := newStreamableMux(&cfg)
+	return serveLoopback(t, mux, endpoint)
+}
+
+func TestStreamableRejectsProxiedHostByDefault(t *testing.T) {
+	testServer := serveStreamable(t, config.HTTPTransportConfig{})
+
+	if got := postStatus(t, testServer, proxiedHost, ""); got != http.StatusForbidden {
+		t.Fatalf("status = %d, want 403 for a non-localhost Host on a loopback listener", got)
+	}
+}
+
+func TestStreamableAcceptsProxiedHostWhenProtectionDisabled(t *testing.T) {
+	testServer := serveStreamable(t, config.HTTPTransportConfig{DisableLocalhostProtection: true})
+
+	if got := postStatus(t, testServer, proxiedHost, ""); got != http.StatusOK {
+		t.Fatalf("status = %d, want 200 once localhost protection is disabled", got)
+	}
+}
+
+// A direct local client is unaffected either way, so the protection must not
+// be the reason a plain loopback request fails.
+func TestStreamableAcceptsLoopbackHost(t *testing.T) {
+	testServer := serveStreamable(t, config.HTTPTransportConfig{})
+
+	if got := postStatus(t, testServer, testServer.Listener.Addr().String(), ""); got != http.StatusOK {
+		t.Fatalf("status = %d, want 200 for a loopback Host", got)
+	}
+}
+
+// The mux has to serve the configured endpoint and only that, so a request to
+// another path is a 404 rather than a catch-all hit.
+func TestStreamableServesConfiguredEndpoint(t *testing.T) {
+	cfg := config.StreamableServerConfig{EndpointPath: "/custom-mcp"}
+	mux, endpoint := newStreamableMux(&cfg)
+	if endpoint != "/custom-mcp" {
+		t.Fatalf("endpoint = %q, want /custom-mcp", endpoint)
+	}
+
+	testServer := serveLoopback(t, mux, endpoint)
+	host := testServer.Listener.Addr().String()
+
+	if got := postStatus(t, testServer, host, ""); got != http.StatusOK {
+		t.Fatalf("status = %d on the configured endpoint, want 200", got)
+	}
+	if got := postStatusTo(t, testServer, testServer.URL+"/", host, ""); got != http.StatusNotFound {
+		t.Fatalf("status = %d on the listener root, want 404", got)
+	}
+}
+
+// The endpoint is mounted in both forms, so a client that appends a slash to
+// the configured path still reaches the server.
+func TestStreamableServesTrailingSlashEndpoint(t *testing.T) {
+	testServer := serveStreamable(t, config.HTTPTransportConfig{})
+
+	url := testServer.url() + "/"
+	if got := postStatusTo(t, testServer, url, testServer.Listener.Addr().String(), ""); got != http.StatusOK {
+		t.Fatalf("status = %d for %s, want 200", got, url)
+	}
+}
+
+// The README tells operators to pair the two flags, because disabling the
+// protection removes the Host check and leaves the origin allowlist as the only
+// barrier for a browser on the proxy host. Both halves have to carry weight:
+// the allowed origin only gets through with the protection off, and the
+// disallowed one only gets rejected while the allowlist is enforced.
+func TestProxiedHostStillHonorsOriginAllowlist(t *testing.T) {
+	testServer := serveStreamable(t, config.HTTPTransportConfig{
+		AllowedOrigins:             []string{allowedOrigin},
+		DisableLocalhostProtection: true,
+	})
+
+	if got := postStatus(t, testServer, proxiedHost, allowedOrigin); got != http.StatusOK {
+		t.Fatalf("allowed origin status = %d, want 200", got)
+	}
+	if got := postStatus(t, testServer, proxiedHost, "https://evil.example"); got != http.StatusForbidden {
+		t.Fatalf("disallowed origin status = %d, want 403", got)
+	}
+}
diff --git a/internal/swmcp/transport_test.go b/internal/swmcp/transport_test.go
new file mode 100644
index 0000000..4492f93
--- /dev/null
+++ b/internal/swmcp/transport_test.go
@@ -0,0 +1,195 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package swmcp
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+
+	"github.com/apache/skywalking-mcp/internal/config"
+)
+
+// proxiedHost stands in for the public Host a same-host reverse proxy
+// forwards unchanged to a loopback backend.
+const proxiedHost = "mcp.example.com"
+
+// transportServer is a transport served over loopback, which is what arms the
+// SDK's DNS rebinding protection, paired with the endpoint it answers on.
+type transportServer struct {
+	*httptest.Server
+	endpoint string
+}
+
+// url addresses the endpoint the transport serves rather than the listener
+// root, so requests take the route production routes them through.
+func (s *transportServer) url() string {
+	return s.URL + s.endpoint
+}
+
+func serveLoopback(t *testing.T, handler http.Handler, endpoint string) *transportServer {
+	t.Helper()
+
+	testServer := httptest.NewServer(handler)
+	t.Cleanup(testServer.Close)
+	return &transportServer{Server: testServer, endpoint: endpoint}
+}
+
+// postStatus posts a tools/list call with the given Host and Origin headers,
+// omitting Origin when empty, and reports the response status.
+func postStatus(t *testing.T, testServer *transportServer, host, origin string) int {
+	t.Helper()
+
+	return postStatusTo(t, testServer, testServer.url(), host, origin)
+}
+
+func postStatusTo(t *testing.T, testServer *transportServer, url, host, origin string) int {
+	t.Helper()
+
+	body := `{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}`
+	req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(body))
+	if err != nil {
+		t.Fatalf("create request: %v", err)
+	}
+	req.Host = host
+	if origin != "" {
+		req.Header.Set("Origin", origin)
+	}
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Accept", "application/json, text/event-stream")
+
+	res, err := testServer.Client().Do(req)
+	if err != nil {
+		t.Fatalf("do request: %v", err)
+	}
+	defer func() { _ = res.Body.Close() }()
+	return res.StatusCode
+}
+
+type transportUnderTest struct {
+	cmd  *cobra.Command
+	http func() config.HTTPTransportConfig
+}
+
+// registeredTransports builds every subcommand the way main.go does, so the
+// tests see the state production runs in: the transports share flag names, and
+// a shared registry would let the subcommand registered last own them all.
+func registeredTransports(t *testing.T) map[string]transportUnderTest {
+	t.Helper()
+
+	viper.Reset()
+	t.Cleanup(viper.Reset)
+
+	sseCmd, sseConfig := newSSECommand()
+	streamableCmd, streamableConfig := newStreamableCommand()
+
+	return map[string]transportUnderTest{
+		"sse": {
+			cmd:  sseCmd,
+			http: func() config.HTTPTransportConfig { return sseConfig().HTTPTransportConfig },
+		},
+		"streamable": {
+			cmd:  streamableCmd,
+			http: func() config.HTTPTransportConfig { return streamableConfig().HTTPTransportConfig },
+		},
+	}
+}
+
+// The protection must stay on unless an operator opts out, so every transport
+// has to register the flag, default it to false, and carry its own value into
+// the config its handler is built from.
+func TestLocalhostProtectionFlagDefaultsToEnabled(t *testing.T) {
+	for name, transport := range registeredTransports(t) {
+		t.Run(name, func(t *testing.T) {
+			flag := transport.cmd.Flags().Lookup("disable-localhost-protection")
+			if flag == nil {
+				t.Fatal("disable-localhost-protection flag not registered")
+			}
+			if flag.DefValue != "false" {
+				t.Fatalf("flag default = %q, want %q", flag.DefValue, "false")
+			}
+			if transport.http().DisableLocalhostProtection {
+				t.Fatal("config reports the protection disabled before the flag is set")
+			}
+
+			if err := transport.cmd.Flags().Set("disable-localhost-protection", "true"); err != nil {
+				t.Fatalf("set flag: %v", err)
+			}
+			if !transport.http().DisableLocalhostProtection {
+				t.Fatal("setting the flag did not reach the transport's config")
+			}
+		})
+	}
+}
+
+// Every transport must read its own --allowed-origins; an empty list means the
+// CORS allowlist silently degrades to open.
+func TestAllowedOriginsReachesEachTransport(t *testing.T) {
+	for name, transport := range registeredTransports(t) {
+		t.Run(name, func(t *testing.T) {
+			if err := transport.cmd.Flags().Set("allowed-origins", allowedOrigin); err != nil {
+				t.Fatalf("set flag: %v", err)
+			}
+
+			got := transport.http().AllowedOrigins
+			if len(got) != 1 || got[0] != allowedOrigin {
+				t.Fatalf("allowed origins = %v, want [%s]", got, allowedOrigin)
+			}
+		})
+	}
+}
+
+// The shared flags are also configurable through SW_* environment variables,
+// which only works while each per-command viper applies the env conventions;
+// dropping ConfigureEnv from bindHTTPTransportFlags would silently break it.
+func TestEnvVarsReachEachTransport(t *testing.T) {
+	t.Setenv("SW_ALLOWED_ORIGINS", allowedOrigin)
+	t.Setenv("SW_DISABLE_LOCALHOST_PROTECTION", "true")
+
+	for name, transport := range registeredTransports(t) {
+		t.Run(name, func(t *testing.T) {
+			got := transport.http()
+			if len(got.AllowedOrigins) != 1 || got.AllowedOrigins[0] != allowedOrigin {
+				t.Fatalf("allowed origins = %v, want [%s]", got.AllowedOrigins, allowedOrigin)
+			}
+			if !got.DisableLocalhostProtection {
+				t.Fatal("SW_DISABLE_LOCALHOST_PROTECTION did not reach the transport's config")
+			}
+		})
+	}
+}
+
+// Setting a flag on one transport must not leak into another.
+func TestTransportFlagsAreIndependent(t *testing.T) {
+	transports := registeredTransports(t)
+
+	if err := transports["streamable"].cmd.Flags().Set("disable-localhost-protection", "true"); err != nil {
+		t.Fatalf("set flag: %v", err)
+	}
+
+	if !transports["streamable"].http().DisableLocalhostProtection {
+		t.Fatal("streamable did not pick up its own flag")
+	}
+	if transports["sse"].http().DisableLocalhostProtection {
+		t.Fatal("streamable's flag leaked into sse")
+	}
+}
diff --git a/internal/tools/alarm.go b/internal/tools/alarm.go
index 25417dd..544e36b 100644
--- a/internal/tools/alarm.go
+++ b/internal/tools/alarm.go
@@ -22,16 +22,15 @@ import (
 	"encoding/json"
 	"fmt"
 
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 	api "skywalking.apache.org/repo/goapi/query"
 
 	swalarm "github.com/apache/skywalking-cli/pkg/graphql/alarm"
 )
 
 // AddAlarmTools registers alarm-related tools with the MCP server
-func AddAlarmTools(s *server.MCPServer) {
-	AlarmQueryTool.Register(s)
+func AddAlarmTools(s *mcp.Server) {
+	mcp.AddTool(s, alarmQueryTool(), queryAlarms)
 }
 
 type AlarmTag struct {
@@ -40,14 +39,14 @@ type AlarmTag struct {
 }
 
 type AlarmQueryRequest struct {
-	Scope    string     `json:"scope,omitempty"`
-	Keyword  string     `json:"keyword,omitempty"`
-	Tags     []AlarmTag `json:"tags,omitempty"`
-	Start    string     `json:"start,omitempty"`
-	End      string     `json:"end,omitempty"`
-	Step     string     `json:"step,omitempty"`
-	PageNum  int        `json:"page_num,omitempty"`
-	PageSize int        `json:"page_size,omitempty"`
+	Scope    string     `json:"scope,omitempty" jsonschema:"Scope to filter alarms."`
+	Keyword  string     `json:"keyword,omitempty" jsonschema:"Keyword to filter alarm messages."`
+	Tags     []AlarmTag `json:"tags,omitempty" jsonschema:"Array of alarm tags to filter by, each with key and value."`
+	Start    string     `json:"start,omitempty" jsonschema:"Start time for the query."`
+	End      string     `json:"end,omitempty" jsonschema:"End time for the query. Default is now."`
+	Step     string     `json:"step,omitempty" jsonschema:"Time step granularity. If not specified, uses adaptive step sizing."`
+	PageNum  int        `json:"page_num,omitempty" jsonschema:"Page number, default 1."`
+	PageSize int        `json:"page_size,omitempty" jsonschema:"Page size, default 15."`
 }
 
 func buildAlarmQueryCondition(req *AlarmQueryRequest, timeCtx TimeContext) *swalarm.ListAlarmCondition {
@@ -73,52 +72,38 @@ func buildAlarmQueryCondition(req *AlarmQueryRequest, timeCtx TimeContext) *swal
 	return cond
 }
 
-func queryAlarms(ctx context.Context, req *AlarmQueryRequest) (*mcp.CallToolResult, error) {
+func queryAlarms(ctx context.Context, _ *mcp.CallToolRequest, req AlarmQueryRequest) (*mcp.CallToolResult, any, error) {
 	timeCtx := GetTimeContext(ctx)
-	cond := buildAlarmQueryCondition(req, timeCtx)
+	cond := buildAlarmQueryCondition(&req, timeCtx)
 
 	alarms, err := swalarm.Alarms(ctx, cond)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("failed to query alarms: %v", err)), nil
+		return ResultError(fmt.Sprintf("failed to query alarms: %v", err)), nil, nil
 	}
 
 	jsonBytes, err := json.Marshal(alarms)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil
+		return ResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil, nil
 	}
-	return mcp.NewToolResultText(string(jsonBytes)), nil
+	return ResultText(string(jsonBytes)), nil, nil
 }
 
-var AlarmQueryTool = NewTool(
-	"query_alarms",
-	`Query alarms from SkyWalking OAP. Alarms are triggered when metrics breach configured thresholds.
+func alarmQueryTool() *mcp.Tool {
+	schema := InferSchema[AlarmQueryRequest]()
+	WithEnum(schema, "scope", "All", "Service", "ServiceInstance", "Endpoint", "Process",
+		"ServiceRelation", "ServiceInstanceRelation", "EndpointRelation", "ProcessRelation")
+	WithEnum(schema, "step", stepEnum...)
+
+	return &mcp.Tool{
+		Name: "query_alarms",
+		Description: `Query alarms from SkyWalking OAP. Alarms are triggered when metrics breach configured thresholds.
 
 Examples:
 - {"start": "-1h"}: All alarms in the last hour
 - {"scope": "Service", "start": "-30m"}: Service-level alarms in the last 30 minutes
 - {"keyword": "timeout", "start": "-1h"}: Alarms whose message contains "timeout"
 - {"tags": [{"key": "level", "value": "critical"}], "start": "-1h"}: Alarms with a specific tag`,
-	queryAlarms,
-	mcp.WithString("scope",
-		mcp.Enum("All", "Service", "ServiceInstance", "Endpoint", "Process",
-			"ServiceRelation", "ServiceInstanceRelation", "EndpointRelation", "ProcessRelation"),
-		mcp.Description("Scope to filter alarms.")),
-	mcp.WithString("keyword", mcp.Description("Keyword to filter alarm messages.")),
-	mcp.WithArray("tags",
-		mcp.Description("Array of alarm tags to filter by, each with key and value."),
-		mcp.Items(map[string]any{
-			"type": "object",
-			"properties": map[string]any{
-				"key":   map[string]any{"type": "string"},
-				"value": map[string]any{"type": "string"},
-			},
-			"required": []string{"key", "value"},
-		}),
-	),
-	mcp.WithString("start", mcp.Description("Start time for the query.")),
-	mcp.WithString("end", mcp.Description("End time for the query. Default is now.")),
-	mcp.WithString("step", mcp.Enum("SECOND", "MINUTE", "HOUR", "DAY"),
-		mcp.Description("Time step granularity. If not specified, uses adaptive step sizing.")),
-	mcp.WithNumber("page_num", mcp.Description("Page number, default 1.")),
-	mcp.WithNumber("page_size", mcp.Description("Page size, default 15.")),
-)
+		InputSchema: schema,
+		Annotations: &mcp.ToolAnnotations{Title: "query_alarms", IdempotentHint: true},
+	}
+}
diff --git a/internal/tools/alarm_test.go b/internal/tools/alarm_test.go
index a23110c..09d22ab 100644
--- a/internal/tools/alarm_test.go
+++ b/internal/tools/alarm_test.go
@@ -20,9 +20,49 @@ import (
 	"testing"
 	"time"
 
+	"github.com/google/jsonschema-go/jsonschema"
 	api "skywalking.apache.org/repo/goapi/query"
 )
 
+// The jsonschema struct tag carries only descriptions, so enums are applied to
+// the inferred schema afterwards. Dropping that step fails silently.
+func TestAlarmQueryToolSchema(t *testing.T) {
+	schema, ok := alarmQueryTool().InputSchema.(*jsonschema.Schema)
+	if !ok {
+		t.Fatalf("input schema type = %T", alarmQueryTool().InputSchema)
+	}
+
+	tests := []struct {
+		property  string
+		enumCount int
+	}{
+		{property: "scope", enumCount: 9},
+		{property: "step", enumCount: 4},
+		{property: "keyword", enumCount: 0},
+		{property: "page_num", enumCount: 0},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.property, func(t *testing.T) {
+			property, ok := schema.Properties[tc.property]
+			if !ok {
+				t.Fatalf("property %q missing", tc.property)
+			}
+			if len(property.Enum) != tc.enumCount {
+				t.Fatalf("property %q enum count = %d, want %d", tc.property, len(property.Enum), tc.enumCount)
+			}
+			if property.Description == "" {
+				t.Fatalf("property %q description is empty", tc.property)
+			}
+		})
+	}
+
+	// Every field is omitempty, so none of them may be required.
+	if len(schema.Required) != 0 {
+		t.Fatalf("required = %v, want none", schema.Required)
+	}
+}
+
 func TestBuildAlarmQueryCondition(t *testing.T) {
 	timeCtx := TimeContext{
 		NowUTC:   time.Date(2026, 3, 31, 12, 0, 0, 0, time.UTC),
diff --git a/internal/tools/event.go b/internal/tools/event.go
index fb89082..872f943 100644
--- a/internal/tools/event.go
+++ b/internal/tools/event.go
@@ -22,34 +22,33 @@ import (
 	"encoding/json"
 	"fmt"
 
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 	api "skywalking.apache.org/repo/goapi/query"
 
 	swevent "github.com/apache/skywalking-cli/pkg/graphql/event"
 )
 
 // AddEventTools registers event-related tools with the MCP server
-func AddEventTools(s *server.MCPServer) {
-	EventQueryTool.Register(s)
+func AddEventTools(s *mcp.Server) {
+	mcp.AddTool(s, eventQueryTool(), queryEvents)
 }
 
 const orderASC = "ASC"
 
 type EventQueryRequest struct {
-	UUID            string `json:"uuid,omitempty"`
-	Service         string `json:"service,omitempty"`
-	ServiceInstance string `json:"service_instance,omitempty"`
-	Endpoint        string `json:"endpoint,omitempty"`
-	Name            string `json:"name,omitempty"`
-	Type            string `json:"type,omitempty"`
-	Layer           string `json:"layer,omitempty"`
-	Start           string `json:"start,omitempty"`
-	End             string `json:"end,omitempty"`
-	Step            string `json:"step,omitempty"`
-	Order           string `json:"order,omitempty"`
-	PageNum         int    `json:"page_num,omitempty"`
-	PageSize        int    `json:"page_size,omitempty"`
+	UUID            string `json:"uuid,omitempty" jsonschema:"Filter by event UUID."`
+	Service         string `json:"service,omitempty" jsonschema:"Service name to filter events."`
+	ServiceInstance string `json:"service_instance,omitempty" jsonschema:"Service instance name to filter events."`
+	Endpoint        string `json:"endpoint,omitempty" jsonschema:"Endpoint name to filter events."`
+	Name            string `json:"name,omitempty" jsonschema:"Event name to filter."`
+	Type            string `json:"type,omitempty" jsonschema:"Event type: Normal or Error."`
+	Layer           string `json:"layer,omitempty" jsonschema:"Layer to filter events."`
+	Start           string `json:"start,omitempty" jsonschema:"Start time for the query."`
+	End             string `json:"end,omitempty" jsonschema:"End time for the query. Default is now."`
+	Step            string `json:"step,omitempty" jsonschema:"Time step granularity. If not specified, uses adaptive step sizing."`
+	Order           string `json:"order,omitempty" jsonschema:"Order events by time: ASC (oldest first) or DES (newest first, default)."`
+	PageNum         int    `json:"page_num,omitempty" jsonschema:"Page number, default 1."`
+	PageSize        int    `json:"page_size,omitempty" jsonschema:"Page size, default 15."`
 }
 
 func buildEventQueryCondition(req *EventQueryRequest, timeCtx TimeContext) *api.EventQueryCondition {
@@ -96,45 +95,38 @@ func buildEventQueryCondition(req *EventQueryRequest, timeCtx TimeContext) *api.
 	return cond
 }
 
-func queryEvents(ctx context.Context, req *EventQueryRequest) (*mcp.CallToolResult, error) {
+func queryEvents(ctx context.Context, _ *mcp.CallToolRequest, req EventQueryRequest) (*mcp.CallToolResult, any, error) {
 	timeCtx := GetTimeContext(ctx)
-	cond := buildEventQueryCondition(req, timeCtx)
+	cond := buildEventQueryCondition(&req, timeCtx)
 
 	events, err := swevent.Events(ctx, cond)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("failed to query events: %v", err)), nil
+		return ResultError(fmt.Sprintf("failed to query events: %v", err)), nil, nil
 	}
 
 	jsonBytes, err := json.Marshal(events)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil
+		return ResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil, nil
 	}
-	return mcp.NewToolResultText(string(jsonBytes)), nil
+	return ResultText(string(jsonBytes)), nil, nil
 }
 
-var EventQueryTool = NewTool(
-	"query_events",
-	`Query events from SkyWalking OAP. Events record changes or incidents on a service, instance, or endpoint (e.g. deployments, restarts, scaling).
+func eventQueryTool() *mcp.Tool {
+	schema := InferSchema[EventQueryRequest]()
+	WithEnum(schema, "type", "Normal", "Error")
+	WithEnum(schema, "step", stepEnum...)
+	WithEnum(schema, "order", orderASC, "DES")
+
+	return &mcp.Tool{
+		Name: "query_events",
+		Description: `Query events from SkyWalking OAP.
+Events record changes or incidents on a service, instance, or endpoint (e.g. deployments, restarts, scaling).
 
 Examples:
 - {"service": "Your_ApplicationName", "start": "-1h"}: Recent events for a service
 - {"type": "Error", "start": "-30m"}: Error events in the last 30 minutes
 - {"service": "Your_ApplicationName", "type": "Normal"}: Normal events for a service`,
-	queryEvents,
-	mcp.WithString("uuid", mcp.Description("Filter by event UUID.")),
-	mcp.WithString("service", mcp.Description("Service name to filter events.")),
-	mcp.WithString("service_instance", mcp.Description("Service instance name to filter events.")),
-	mcp.WithString("endpoint", mcp.Description("Endpoint name to filter events.")),
-	mcp.WithString("name", mcp.Description("Event name to filter.")),
-	mcp.WithString("type", mcp.Enum("Normal", "Error"),
-		mcp.Description("Event type: Normal or Error.")),
-	mcp.WithString("layer", mcp.Description("Layer to filter events.")),
-	mcp.WithString("start", mcp.Description("Start time for the query.")),
-	mcp.WithString("end", mcp.Description("End time for the query. Default is now.")),
-	mcp.WithString("step", mcp.Enum("SECOND", "MINUTE", "HOUR", "DAY"),
-		mcp.Description("Time step granularity. If not specified, uses adaptive step sizing.")),
-	mcp.WithString("order", mcp.Enum(orderASC, "DES"),
-		mcp.Description("Order events by time: ASC (oldest first) or DES (newest first, default).")),
-	mcp.WithNumber("page_num", mcp.Description("Page number, default 1.")),
-	mcp.WithNumber("page_size", mcp.Description("Page size, default 15.")),
-)
+		InputSchema: schema,
+		Annotations: &mcp.ToolAnnotations{Title: "query_events", IdempotentHint: true},
+	}
+}
diff --git a/internal/tools/io.go b/internal/tools/io.go
deleted file mode 100644
index 7c2d70f..0000000
--- a/internal/tools/io.go
+++ /dev/null
@@ -1,96 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements.  See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership.  The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License.  You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-package tools
-
-import (
-	"bytes"
-	"io"
-	"regexp"
-
-	log "github.com/sirupsen/logrus"
-)
-
-// sensitiveFieldPattern matches JSON fields whose values should be redacted in logs.
-var sensitiveFieldPattern = regexp.MustCompile(`(?i)("(?:authorization|password|token|secret)"\s*:\s*")((?:[^"\\]|\\.)*)(")`) //nolint:lll // regex must be on one line
-
-// redactSensitiveData masks values of sensitive JSON fields before logging.
-func redactSensitiveData(data string) string {
-	return sensitiveFieldPattern.ReplaceAllString(data, `${1}[REDACTED]${3}`)
-}
-
-// IOLogger is a wrapper around io.Reader and io.Writer that logs complete
-// newline-delimited JSON-RPC messages. Partial chunks are held in per-direction
-// buffers until a newline arrives, ensuring the redaction regex always sees a
-// full message and secrets split across read boundaries are never partially logged.
-type IOLogger struct {
-	reader   io.Reader
-	writer   io.Writer
-	logger   *log.Logger
-	readBuf  bytes.Buffer
-	writeBuf bytes.Buffer
-}
-
-// NewIOLogger creates a new IOLogger instance
-func NewIOLogger(r io.Reader, w io.Writer, logger *log.Logger) *IOLogger {
-	return &IOLogger{
-		reader: r,
-		writer: w,
-		logger: logger,
-	}
-}
-
-// logCompleteLines drains newline-terminated lines from buf, redacts each one,
-// and logs it under the given direction label. Any trailing partial line is left
-// in buf for the next call.
-func (l *IOLogger) logCompleteLines(buf *bytes.Buffer, direction string) {
-	data := buf.Bytes()
-	for {
-		idx := bytes.IndexByte(data, '\n')
-		if idx < 0 {
-			break
-		}
-		line := bytes.TrimRight(data[:idx], "\r")
-		l.logger.Infof("[%s]: %s", direction, redactSensitiveData(string(line)))
-		data = data[idx+1:]
-	}
-	buf.Reset()
-	buf.Write(data)
-}
-
-// Read reads data from the underlying io.Reader and logs complete lines.
-func (l *IOLogger) Read(p []byte) (n int, err error) {
-	if l.reader == nil {
-		return 0, io.EOF
-	}
-	n, err = l.reader.Read(p)
-	if n > 0 {
-		l.readBuf.Write(p[:n])
-		l.logCompleteLines(&l.readBuf, "stdin")
-	}
-	return n, err
-}
-
-// Write writes data to the underlying io.Writer and logs complete lines.
-func (l *IOLogger) Write(p []byte) (n int, err error) {
-	if l.writer == nil {
-		return 0, io.ErrClosedPipe
-	}
-	l.writeBuf.Write(p)
-	l.logCompleteLines(&l.writeBuf, "stdout")
-	return l.writer.Write(p)
-}
diff --git a/internal/tools/log.go b/internal/tools/log.go
index 414b78e..e2804b4 100644
--- a/internal/tools/log.go
+++ b/internal/tools/log.go
@@ -22,16 +22,15 @@ import (
 	"encoding/json"
 	"fmt"
 
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 	api "skywalking.apache.org/repo/goapi/query"
 
 	swlog "github.com/apache/skywalking-cli/pkg/graphql/log"
 )
 
 // AddLogTools registers log-related tools with the MCP server
-func AddLogTools(mcp *server.MCPServer) {
-	LogQueryTool.Register(mcp)
+func AddLogTools(s *mcp.Server) {
+	mcp.AddTool(s, logQueryTool(), queryLogs)
 }
 
 type LogTag struct {
@@ -40,20 +39,20 @@ type LogTag struct {
 }
 
 type LogQueryRequest struct {
-	ServiceID         string   `json:"service_id,omitempty"`
-	ServiceInstanceID string   `json:"service_instance_id,omitempty"`
-	EndpointID        string   `json:"endpoint_id,omitempty"`
-	TraceID           string   `json:"trace_id,omitempty"`
-	SegmentID         string   `json:"segment_id,omitempty"`
-	SpanID            *int     `json:"span_id,omitempty"`
-	Tags              []LogTag `json:"tags,omitempty"`
-	Start             string   `json:"start,omitempty"`
-	End               string   `json:"end,omitempty"`
+	ServiceID         string   `json:"service_id,omitempty" jsonschema:"Service ID to filter logs."`
+	ServiceInstanceID string   `json:"service_instance_id,omitempty" jsonschema:"Service instance ID to filter logs."`
+	EndpointID        string   `json:"endpoint_id,omitempty" jsonschema:"Endpoint ID to filter logs."`
+	TraceID           string   `json:"trace_id,omitempty" jsonschema:"Related trace ID to filter logs by trace scope."`
+	SegmentID         string   `json:"segment_id,omitempty" jsonschema:"Related segment ID to narrow logs to a specific segment within a trace."`
+	SpanID            *int     `json:"span_id,omitempty" jsonschema:"Related span ID to narrow logs to a specific span within a segment."`
+	Tags              []LogTag `json:"tags,omitempty" jsonschema:"Array of log tags, each with key and value."`
+	Start             string   `json:"start,omitempty" jsonschema:"Start time for the query."`
+	End               string   `json:"end,omitempty" jsonschema:"End time for the query. Default is now."`
 	Step              string   `json:"step,omitempty"`
-	Cold              bool     `json:"cold,omitempty"`
-	PageNum           int      `json:"page_num,omitempty"`
-	PageSize          int      `json:"page_size,omitempty"`
-	QueryOrder        string   `json:"query_order,omitempty"`
+	Cold              bool     `json:"cold,omitempty" jsonschema:"Whether to query from cold-stage storage."`
+	PageNum           int      `json:"page_num,omitempty" jsonschema:"Page number, default 1."`
+	PageSize          int      `json:"page_size,omitempty" jsonschema:"Page size, default 15."`
+	QueryOrder        string   `json:"query_order,omitempty" jsonschema:"Order logs by timestamp: ASC (oldest first) or DES (newest first, default)."`
 }
 
 // buildLogQueryCondition builds the log query condition from request parameters
@@ -108,25 +107,35 @@ func buildLogQueryCondition(req *LogQueryRequest, timeCtx TimeContext) *api.LogQ
 }
 
 // queryLogs queries logs from SkyWalking OAP
-func queryLogs(ctx context.Context, req *LogQueryRequest) (*mcp.CallToolResult, error) {
+func queryLogs(ctx context.Context, _ *mcp.CallToolRequest, req LogQueryRequest) (*mcp.CallToolResult, any, error) {
 	timeCtx := GetTimeContext(ctx)
-	cond := buildLogQueryCondition(req, timeCtx)
+	cond := buildLogQueryCondition(&req, timeCtx)
 
 	logs, err := swlog.Logs(ctx, cond)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("failed to query logs: %v", err)), nil
+		return ResultError(fmt.Sprintf("failed to query logs: %v", err)), nil, nil
 	}
 
 	jsonBytes, err := json.Marshal(logs)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil
+		return ResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil, nil
 	}
-	return mcp.NewToolResultText(string(jsonBytes)), nil
+	return ResultText(string(jsonBytes)), nil, nil
 }
 
-var LogQueryTool = NewTool(
-	"query_logs",
-	`Query logs from SkyWalking OAP with flexible filters.
+func logQueryTool() *mcp.Tool {
+	schema := InferSchema[LogQueryRequest]()
+	WithEnum(schema, "step", stepEnum...)
+	WithEnum(schema, "query_order", "ASC", "DES")
+	WithDescriptions(schema, map[string]string{
+		"step": "Time step granularity: SECOND, MINUTE, HOUR, DAY. " +
+			"If not specified, uses adaptive step sizing: " +
+			"SECOND (<1h), MINUTE (1h-24h), HOUR (1d-7d), DAY (>7d)",
+	})
+
+	return &mcp.Tool{
+		Name: "query_logs",
+		Description: `Query logs from SkyWalking OAP with flexible filters.
 
 Workflow:
 1. Use this tool to find logs matching specific criteria
@@ -140,33 +149,7 @@ Examples:
 - {"trace_id": "abc123..."}: Query logs related to a specific trace
 - {"trace_id": "abc123...", "segment_id": "seg456...", "span_id": 0}: Query logs for a specific span
 - {"tags": [{"key": "level", "value": "ERROR"}], "cold": true}: Query error logs from cold storage`,
-	queryLogs,
-	mcp.WithString("service_id", mcp.Description("Service ID to filter logs.")),
-	mcp.WithString("service_instance_id", mcp.Description("Service instance ID to filter logs.")),
-	mcp.WithString("endpoint_id", mcp.Description("Endpoint ID to filter logs.")),
-	mcp.WithString("trace_id", mcp.Description("Related trace ID to filter logs by trace scope.")),
-	mcp.WithString("segment_id", mcp.Description("Related segment ID to narrow logs to a specific segment within a trace.")),
-	mcp.WithNumber("span_id", mcp.Description("Related span ID to narrow logs to a specific span within a segment.")),
-	mcp.WithArray("tags",
-		mcp.Description("Array of log tags, each with key and value."),
-		mcp.Items(map[string]any{
-			"type": "object",
-			"properties": map[string]any{
-				"key":   map[string]any{"type": "string"},
-				"value": map[string]any{"type": "string"},
-			},
-			"required": []string{"key", "value"},
-		}),
-	),
-	mcp.WithString("start", mcp.Description("Start time for the query.")),
-	mcp.WithString("end", mcp.Description("End time for the query. Default is now.")),
-	mcp.WithString("step", mcp.Enum("SECOND", "MINUTE", "HOUR", "DAY"),
-		mcp.Description("Time step granularity: SECOND, MINUTE, HOUR, DAY. "+
-			"If not specified, uses adaptive step sizing: "+
-			"SECOND (<1h), MINUTE (1h-24h), HOUR (1d-7d), DAY (>7d)")),
-	mcp.WithBoolean("cold", mcp.Description("Whether to query from cold-stage storage.")),
-	mcp.WithNumber("page_num", mcp.Description("Page number, default 1.")),
-	mcp.WithNumber("page_size", mcp.Description("Page size, default 15.")),
-	mcp.WithString("query_order", mcp.Enum("ASC", "DES"),
-		mcp.Description("Order logs by timestamp: ASC (oldest first) or DES (newest first, default).")),
-)
+		InputSchema: schema,
+		Annotations: &mcp.ToolAnnotations{Title: "query_logs", IdempotentHint: true},
+	}
+}
diff --git a/internal/tools/metadata.go b/internal/tools/metadata.go
index 1122323..f4a90f8 100644
--- a/internal/tools/metadata.go
+++ b/internal/tools/metadata.go
@@ -22,52 +22,61 @@ import (
 	"encoding/json"
 	"fmt"
 
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 
 	"github.com/apache/skywalking-cli/pkg/graphql/metadata"
 	api "skywalking.apache.org/repo/goapi/query"
 )
 
 // AddMetadataTools registers metadata-related tools with the MCP server
-func AddMetadataTools(s *server.MCPServer) {
-	ListLayersTool.Register(s)
-	ListServicesTool.Register(s)
-	ListInstancesTool.Register(s)
-	ListEndpointsTool.Register(s)
-	ListProcessesTool.Register(s)
+func AddMetadataTools(s *mcp.Server) {
+	mcp.AddTool(s, listLayersTool(), listLayers)
+	mcp.AddTool(s, listServicesTool(), listServices)
+	mcp.AddTool(s, listInstancesTool(), listInstances)
+	mcp.AddTool(s, listEndpointsTool(), listEndpoints)
+	mcp.AddTool(s, listProcessesTool(), listProcesses)
 }
 
+// Time-window descriptions shared by the metadata tools. They live here rather
+// than in jsonschema struct tags because a tag is a single line and these
+// exceed the line limit.
+const (
+	descStartTime       = `Start time for the query. Examples: "2024-01-01 12:00:00", "-1h" (1 hour ago), "-30m" (30 minutes ago).`
+	descEndTime         = `End time for the query. Examples: "2024-01-01 13:00:00", "now". Defaults to current time if omitted.`
+	descStepGranularity = "Time step granularity. If not specified, uses adaptive sizing: " +
+		"SECOND (<1h), MINUTE (1h-24h), HOUR (1d-7d), DAY (>7d)."
+)
+
 // ListLayersRequest defines the parameters for the list_layers tool (no parameters needed)
 type ListLayersRequest struct{}
 
 // listLayers queries available layers from SkyWalking OAP
-func listLayers(ctx context.Context, _ *ListLayersRequest) (*mcp.CallToolResult, error) {
+func listLayers(ctx context.Context, _ *mcp.CallToolRequest, _ ListLayersRequest) (*mcp.CallToolResult, any, error) {
 	layers, err := metadata.ListLayers(ctx)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("failed to list layers: %v", err)), nil
+		return ResultError(fmt.Sprintf("failed to list layers: %v", err)), nil, nil
 	}
 
 	jsonBytes, err := json.Marshal(layers)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil
+		return ResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil, nil
 	}
-	return mcp.NewToolResultText(string(jsonBytes)), nil
+	return ResultText(string(jsonBytes)), nil, nil
 }
 
 // ListEndpointsRequest defines the parameters for the list_endpoints tool
 type ListEndpointsRequest struct {
-	ServiceID string `json:"service_id"`
-	Keyword   string `json:"keyword"`
-	Limit     int    `json:"limit"`
-	Start     string `json:"start"`
-	End       string `json:"end"`
-	Step      string `json:"step"`
-	Cold      bool   `json:"cold"`
+	ServiceID string `json:"service_id,omitempty" jsonschema:"The service ID to list endpoints for. Use list_services to obtain a service ID."`
+	Keyword   string `json:"keyword,omitempty" jsonschema:"Keyword to filter endpoints by name. Leave empty to list all endpoints."`
+	Limit     int    `json:"limit,omitempty" jsonschema:"Maximum number of endpoints to return. Defaults to 100."`
+	Start     string `json:"start,omitempty"`
+	End       string `json:"end,omitempty"`
+	Step      string `json:"step,omitempty"`
+	Cold      bool   `json:"cold,omitempty" jsonschema:"Whether to query from cold-stage storage. Set to true for historical data queries."`
 }
 
 // listEndpoints searches endpoints for a given service from SkyWalking OAP
-func listEndpoints(ctx context.Context, req *ListEndpointsRequest) (*mcp.CallToolResult, error) {
+func listEndpoints(ctx context.Context, _ *mcp.CallToolRequest, req ListEndpointsRequest) (*mcp.CallToolResult, any, error) {
 	limit := req.Limit
 	if limit <= 0 {
 		limit = 100
@@ -80,20 +89,29 @@ func listEndpoints(ctx context.Context, req *ListEndpointsRequest) (*mcp.CallToo
 	}
 	endpoints, err := metadata.SearchEndpoints(ctx, req.ServiceID, req.Keyword, limit, durationPtr)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("failed to list endpoints: %v", err)), nil
+		return ResultError(fmt.Sprintf("failed to list endpoints: %v", err)), nil, nil
 	}
 
 	jsonBytes, err := json.Marshal(endpoints)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil
+		return ResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil, nil
 	}
-	return mcp.NewToolResultText(string(jsonBytes)), nil
+	return ResultText(string(jsonBytes)), nil, nil
 }
 
-// ListEndpointsTool lists endpoints of a service in SkyWalking OAP
-var ListEndpointsTool = NewTool(
-	"list_endpoints",
-	`List endpoints of a service registered in SkyWalking OAP.
+func listEndpointsTool() *mcp.Tool {
+	schema := InferSchema[ListEndpointsRequest]()
+	WithRequired(schema, "service_id")
+	WithEnum(schema, "step", stepEnum...)
+	WithDescriptions(schema, map[string]string{
+		"start": `Start time for the query. Examples: "2024-01-01 12:00:00", "-1h" (1 hour ago).`,
+		"end":   descEndTime,
+		"step":  descStepGranularity,
+	})
+
+	return &mcp.Tool{
+		Name: "list_endpoints",
+		Description: `List endpoints of a service registered in SkyWalking OAP.
 
 An endpoint represents an individual API path or operation exposed by a service.
 Use list_services to obtain a service ID before calling this tool.
@@ -109,61 +127,49 @@ Workflow:
 Examples:
 - {"service_id": "abc123"}: List all endpoints of a service
 - {"service_id": "abc123", "keyword": "/api/user", "limit": 20}: Search endpoints by keyword`,
-	listEndpoints,
-	mcp.WithTitleAnnotation("List service endpoints"),
-	mcp.WithString("service_id", mcp.Required(),
-		mcp.Description("The service ID to list endpoints for. Use list_services to obtain a service ID."),
-	),
-	mcp.WithString("keyword",
-		mcp.Description("Keyword to filter endpoints by name. Leave empty to list all endpoints."),
-	),
-	mcp.WithNumber("limit",
-		mcp.Description("Maximum number of endpoints to return. Defaults to 100."),
-	),
-	mcp.WithString("start",
-		mcp.Description(`Start time for the query. Examples: "2024-01-01 12:00:00", "-1h" (1 hour ago).`),
-	),
-	mcp.WithString("end",
-		mcp.Description(`End time for the query. Examples: "2024-01-01 13:00:00", "now". Defaults to current time if omitted.`),
-	),
-	mcp.WithString("step",
-		mcp.Enum("SECOND", "MINUTE", "HOUR", "DAY"),
-		mcp.Description("Time step granularity. If not specified, uses adaptive sizing: SECOND (<1h), MINUTE (1h-24h), HOUR (1d-7d), DAY (>7d)."),
-	),
-	mcp.WithBoolean("cold",
-		mcp.Description("Whether to query from cold-stage storage. Set to true for historical data queries."),
-	),
-)
+		InputSchema: schema,
+		Annotations: &mcp.ToolAnnotations{Title: "List service endpoints", IdempotentHint: true},
+	}
+}
 
 // ListProcessesRequest defines the parameters for the list_processes tool
 type ListProcessesRequest struct {
-	InstanceID string `json:"instance_id"`
-	Start      string `json:"start"`
-	End        string `json:"end"`
-	Step       string `json:"step"`
-	Cold       bool   `json:"cold"`
+	InstanceID string `json:"instance_id,omitempty" jsonschema:"The instance ID to list processes for. Use list_instances to obtain an instance ID."`
+	Start      string `json:"start,omitempty"`
+	End        string `json:"end,omitempty"`
+	Step       string `json:"step,omitempty"`
+	Cold       bool   `json:"cold,omitempty" jsonschema:"Whether to query from cold-stage storage. Set to true for historical data queries."`
 }
 
 // listProcesses queries processes for a given service instance from SkyWalking OAP
-func listProcesses(ctx context.Context, req *ListProcessesRequest) (*mcp.CallToolResult, error) {
+func listProcesses(ctx context.Context, _ *mcp.CallToolRequest, req ListProcessesRequest) (*mcp.CallToolResult, any, error) {
 	timeCtx := GetTimeContext(ctx)
 	duration := BuildDurationWithContext(req.Start, req.End, req.Step, req.Cold, DefaultDuration, timeCtx)
 	processes, err := metadata.Processes(ctx, req.InstanceID, duration)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("failed to list processes: %v", err)), nil
+		return ResultError(fmt.Sprintf("failed to list processes: %v", err)), nil, nil
 	}
 
 	jsonBytes, err := json.Marshal(processes)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil
+		return ResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil, nil
 	}
-	return mcp.NewToolResultText(string(jsonBytes)), nil
+	return ResultText(string(jsonBytes)), nil, nil
 }
 
-// ListProcessesTool lists processes of a service instance in SkyWalking OAP
-var ListProcessesTool = NewTool(
-	"list_processes",
-	`List processes of a service instance registered in SkyWalking OAP.
+func listProcessesTool() *mcp.Tool {
+	schema := InferSchema[ListProcessesRequest]()
+	WithRequired(schema, "instance_id", "start")
+	WithEnum(schema, "step", stepEnum...)
+	WithDescriptions(schema, map[string]string{
+		"start": descStartTime,
+		"end":   descEndTime,
+		"step":  descStepGranularity,
+	})
+
+	return &mcp.Tool{
+		Name: "list_processes",
+		Description: `List processes of a service instance registered in SkyWalking OAP.
 
 A process represents an individual OS or language-level process running within a service instance.
 Use list_instances to obtain an instance ID before calling this tool.
@@ -180,55 +186,49 @@ Workflow:
 Examples:
 - {"instance_id": "abc123", "start": "-1h"}: List processes active in the past hour
 - {"instance_id": "abc123", "start": "2024-01-01 12:00", "end": "2024-01-01 13:00"}: List processes in a time range`,
-	listProcesses,
-	mcp.WithTitleAnnotation("List instance processes"),
-	mcp.WithString("instance_id", mcp.Required(),
-		mcp.Description("The instance ID to list processes for. Use list_instances to obtain an instance ID."),
-	),
-	mcp.WithString("start", mcp.Required(),
-		mcp.Description(`Start time for the query. Examples: "2024-01-01 12:00:00", "-1h" (1 hour ago), "-30m" (30 minutes ago).`),
-	),
-	mcp.WithString("end",
-		mcp.Description(`End time for the query. Examples: "2024-01-01 13:00:00", "now". Defaults to current time if omitted.`),
-	),
-	mcp.WithString("step",
-		mcp.Enum("SECOND", "MINUTE", "HOUR", "DAY"),
-		mcp.Description("Time step granularity. If not specified, uses adaptive sizing: SECOND (<1h), MINUTE (1h-24h), HOUR (1d-7d), DAY (>7d)."),
-	),
-	mcp.WithBoolean("cold",
-		mcp.Description("Whether to query from cold-stage storage. Set to true for historical data queries."),
-	),
-)
+		InputSchema: schema,
+		Annotations: &mcp.ToolAnnotations{Title: "List instance processes", IdempotentHint: true},
+	}
+}
 
 // ListInstancesRequest defines the parameters for the list_instances tool
 type ListInstancesRequest struct {
-	ServiceID string `json:"service_id"`
-	Start     string `json:"start"`
-	End       string `json:"end"`
-	Step      string `json:"step"`
-	Cold      bool   `json:"cold"`
+	ServiceID string `json:"service_id,omitempty" jsonschema:"The service ID to list instances for. Use list_services to obtain a service ID."`
+	Start     string `json:"start,omitempty"`
+	End       string `json:"end,omitempty"`
+	Step      string `json:"step,omitempty"`
+	Cold      bool   `json:"cold,omitempty" jsonschema:"Whether to query from cold-stage storage. Set to true for historical data queries."`
 }
 
 // listInstances queries service instances from SkyWalking OAP
-func listInstances(ctx context.Context, req *ListInstancesRequest) (*mcp.CallToolResult, error) {
+func listInstances(ctx context.Context, _ *mcp.CallToolRequest, req ListInstancesRequest) (*mcp.CallToolResult, any, error) {
 	timeCtx := GetTimeContext(ctx)
 	duration := BuildDurationWithContext(req.Start, req.End, req.Step, req.Cold, DefaultDuration, timeCtx)
 	instances, err := metadata.Instances(ctx, req.ServiceID, duration)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("failed to list instances: %v", err)), nil
+		return ResultError(fmt.Sprintf("failed to list instances: %v", err)), nil, nil
 	}
 
 	jsonBytes, err := json.Marshal(instances)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil
+		return ResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil, nil
 	}
-	return mcp.NewToolResultText(string(jsonBytes)), nil
+	return ResultText(string(jsonBytes)), nil, nil
 }
 
-// ListInstancesTool lists all instances of a service in SkyWalking OAP
-var ListInstancesTool = NewTool(
-	"list_instances",
-	`List all instances of a service registered in SkyWalking OAP.
+func listInstancesTool() *mcp.Tool {
+	schema := InferSchema[ListInstancesRequest]()
+	WithRequired(schema, "service_id", "start")
+	WithEnum(schema, "step", stepEnum...)
+	WithDescriptions(schema, map[string]string{
+		"start": descStartTime,
+		"end":   descEndTime,
+		"step":  descStepGranularity,
+	})
+
+	return &mcp.Tool{
+		Name: "list_instances",
+		Description: `List all instances of a service registered in SkyWalking OAP.
 
 A service instance represents an individual running process of a service (e.g. a pod or JVM process).
 Use list_services to obtain a service ID before calling this tool.
@@ -244,44 +244,32 @@ Workflow:
 Examples:
 - {"service_id": "abc123", "start": "2024-01-01 12:00", "end": "2024-01-01 13:00"}: List instances in a time range
 - {"service_id": "abc123", "start": "-1h"}: List instances active in the past hour`,
-	listInstances,
-	mcp.WithTitleAnnotation("List service instances"),
-	mcp.WithString("service_id", mcp.Required(),
-		mcp.Description("The service ID to list instances for. Use list_services to obtain a service ID."),
-	),
-	mcp.WithString("start", mcp.Required(),
-		mcp.Description(`Start time for the query. Examples: "2024-01-01 12:00:00", "-1h" (1 hour ago), "-30m" (30 minutes ago).`),
-	),
-	mcp.WithString("end",
-		mcp.Description(`End time for the query. Examples: "2024-01-01 13:00:00", "now". Defaults to current time if omitted.`),
-	),
-	mcp.WithString("step",
-		mcp.Enum("SECOND", "MINUTE", "HOUR", "DAY"),
-		mcp.Description("Time step granularity. If not specified, uses adaptive sizing: SECOND (<1h), MINUTE (1h-24h), HOUR (1d-7d), DAY (>7d)."),
-	),
-	mcp.WithBoolean("cold",
-		mcp.Description("Whether to query from cold-stage storage. Set to true for historical data queries."),
-	),
-)
+		InputSchema: schema,
+		Annotations: &mcp.ToolAnnotations{Title: "List service instances", IdempotentHint: true},
+	}
+}
 
 // listServices queries services for a given layer from SkyWalking OAP
-func listServices(ctx context.Context, req *ListServicesRequest) (*mcp.CallToolResult, error) {
+func listServices(ctx context.Context, _ *mcp.CallToolRequest, req ListServicesRequest) (*mcp.CallToolResult, any, error) {
 	services, err := metadata.ListLayerService(ctx, req.Layer)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("failed to list services: %v", err)), nil
+		return ResultError(fmt.Sprintf("failed to list services: %v", err)), nil, nil
 	}
 
 	jsonBytes, err := json.Marshal(services)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil
+		return ResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil, nil
 	}
-	return mcp.NewToolResultText(string(jsonBytes)), nil
+	return ResultText(string(jsonBytes)), nil, nil
 }
 
-// ListServicesTool lists all services for a given layer in SkyWalking OAP
-var ListServicesTool = NewTool(
-	"list_services",
-	`List all services registered in SkyWalking OAP under a specific layer.
+func listServicesTool() *mcp.Tool {
+	schema := InferSchema[ListServicesRequest]()
+	WithRequired(schema, "layer")
+
+	return &mcp.Tool{
+		Name: "list_services",
+		Description: `List all services registered in SkyWalking OAP under a specific layer.
 
 A service represents a logical grouping of monitored workloads. Each service belongs to one
 or more layers (e.g. GENERAL, MESH, K8S). Use list_layers first to discover available layers.
@@ -296,17 +284,15 @@ Workflow:
 Examples:
 - {"layer": "GENERAL"}: List all services in the GENERAL layer
 - {"layer": "MESH"}: List all services in the service mesh layer`,
-	listServices,
-	mcp.WithTitleAnnotation("List services by layer"),
-	mcp.WithString("layer", mcp.Required(),
-		mcp.Description("The layer to list services for. Use list_layers to get available layer names."),
-	),
-)
+		InputSchema: schema,
+		Annotations: &mcp.ToolAnnotations{Title: "List services by layer", IdempotentHint: true},
+	}
+}
 
-// ListLayersTool lists all available layers in SkyWalking OAP
-var ListLayersTool = NewTool(
-	"list_layers",
-	`List all available layers registered in SkyWalking OAP.
+func listLayersTool() *mcp.Tool {
+	return &mcp.Tool{
+		Name: "list_layers",
+		Description: `List all available layers registered in SkyWalking OAP.
 
 A layer represents a technology or deployment environment in SkyWalking's topology,
 such as GENERAL, MESH, K8S, OS_LINUX, etc. Layers are used to categorize services
@@ -318,6 +304,7 @@ Workflow:
 
 Examples:
 - {}: List all layers (no parameters required)`,
-	listLayers,
-	mcp.WithTitleAnnotation("List available layers"),
-)
+		InputSchema: InferSchema[ListLayersRequest](),
+		Annotations: &mcp.ToolAnnotations{Title: "List available layers", IdempotentHint: true},
+	}
+}
diff --git a/internal/tools/mqe.go b/internal/tools/mqe.go
index 032d456..ea871c2 100644
--- a/internal/tools/mqe.go
+++ b/internal/tools/mqe.go
@@ -32,17 +32,16 @@ import (
 	"unicode"
 	"unicode/utf8"
 
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 
 	"github.com/apache/skywalking-cli/pkg/contextkey"
 )
 
 // AddMQETools registers MQE-related tools with the MCP server
-func AddMQETools(mcp *server.MCPServer) {
-	MQEExpressionTool.Register(mcp)
-	MQEMetricsListTool.Register(mcp)
-	MQEMetricsTypeTool.Register(mcp)
+func AddMQETools(s *mcp.Server) {
+	mcp.AddTool(s, mqeExpressionTool(), executeMQEExpression)
+	mcp.AddTool(s, mqeMetricsListTool(), listMQEMetrics)
+	mcp.AddTool(s, mqeMetricsTypeTool(), getMQEMetricsType)
 }
 
 const (
@@ -150,7 +149,7 @@ func executeGraphQLWithContext(ctx context.Context, query string, variables map[
 
 // MQEExpressionRequest represents a request to execute MQE expression
 type MQEExpressionRequest struct {
-	Expression              string `json:"expression"`
+	Expression              string `json:"expression,omitempty"`
 	ServiceName             string `json:"service_name,omitempty"`
 	Layer                   string `json:"layer,omitempty"`
 	ServiceInstanceName     string `json:"service_instance_name,omitempty"`
@@ -178,12 +177,12 @@ type MQEMetricsListRequest struct {
 
 // MQEMetricsTypeRequest represents a request to get metric type
 type MQEMetricsTypeRequest struct {
-	MetricName string `json:"metric_name"`
+	MetricName string `json:"metric_name,omitempty"`
 }
 
 // ListServicesRequest represents a request to list services
 type ListServicesRequest struct {
-	Layer string `json:"layer"`
+	Layer string `json:"layer,omitempty" jsonschema:"The layer to list services for. Use list_layers to get available layer names."`
 }
 
 // getServiceInfo queries service information using the specified layer
@@ -330,15 +329,15 @@ func buildMQEEntity(ctx context.Context, req *MQEExpressionRequest) map[string]i
 }
 
 // executeMQEExpression executes MQE expression query
-func executeMQEExpression(ctx context.Context, req *MQEExpressionRequest) (*mcp.CallToolResult, error) {
+func executeMQEExpression(ctx context.Context, _ *mcp.CallToolRequest, req MQEExpressionRequest) (*mcp.CallToolResult, any, error) {
 	if req.Expression == "" {
-		return mcp.NewToolResultError("expression is required"), nil
+		return ResultError("expression is required"), nil, nil
 	}
-	if err := validateMQEExpressionRequest(req); err != nil {
-		return mcp.NewToolResultError(err.Error()), nil
+	if err := validateMQEExpressionRequest(&req); err != nil {
+		return ResultError(err.Error()), nil, nil
 	}
 
-	entity := buildMQEEntity(ctx, req)
+	entity := buildMQEEntity(ctx, &req)
 	timeCtx := GetTimeContext(ctx)
 
 	duration := BuildDurationWithContext(req.Start, req.End, req.Step, req.Cold, DefaultDuration, timeCtx)
@@ -404,20 +403,20 @@ func executeMQEExpression(ctx context.Context, req *MQEExpressionRequest) (*mcp.
 
 	result, err := executeGraphQLWithContext(ctx, query, variables)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("failed to execute MQE expression: %v", err)), nil
+		return ResultError(fmt.Sprintf("failed to execute MQE expression: %v", err)), nil, nil
 	}
 
 	jsonBytes, err := json.Marshal(result.Data)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil
+		return ResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil, nil
 	}
-	return mcp.NewToolResultText(string(jsonBytes)), nil
+	return ResultText(string(jsonBytes)), nil, nil
 }
 
 // listMQEMetrics lists available metrics
-func listMQEMetrics(ctx context.Context, req *MQEMetricsListRequest) (*mcp.CallToolResult, error) {
-	if err := validateMQEMetricsListRequest(req); err != nil {
-		return mcp.NewToolResultError(err.Error()), nil
+func listMQEMetrics(ctx context.Context, _ *mcp.CallToolRequest, req MQEMetricsListRequest) (*mcp.CallToolResult, any, error) {
+	if err := validateMQEMetricsListRequest(&req); err != nil {
+		return ResultError(err.Error()), nil, nil
 	}
 
 	// GraphQL query for listing metrics
@@ -432,48 +431,29 @@ func listMQEMetrics(ctx context.Context, req *MQEMetricsListRequest) (*mcp.CallT
 	`
 
 	variables := map[string]interface{}{}
-	if req != nil && req.Regex != "" {
+	if req.Regex != "" {
 		variables["regex"] = req.Regex
 	}
 
 	result, err := executeGraphQLWithContext(ctx, query, variables)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("failed to list metrics: %v", err)), nil
+		return ResultError(fmt.Sprintf("failed to list metrics: %v", err)), nil, nil
 	}
 
 	jsonBytes, err := json.Marshal(result.Data)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil
+		return ResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil, nil
 	}
-	return mcp.NewToolResultText(string(jsonBytes)), nil
-}
-
-// ListMQEMetricsInternal is an exported function for internal use by resources package
-func ListMQEMetricsInternal(ctx context.Context, regex *string) (string, error) {
-	var req *MQEMetricsListRequest
-	if regex != nil {
-		req = &MQEMetricsListRequest{Regex: *regex}
-	}
-	result, err := listMQEMetrics(ctx, req)
-	if err != nil {
-		return "", err
-	}
-
-	// Extract the text content from the tool result
-	if textResult, ok := result.Content[0].(mcp.TextContent); ok {
-		return textResult.Text, nil
-	}
-
-	return "", fmt.Errorf("unexpected result format")
+	return ResultText(string(jsonBytes)), nil, nil
 }
 
 // getMQEMetricsType gets metric type information
-func getMQEMetricsType(ctx context.Context, req *MQEMetricsTypeRequest) (*mcp.CallToolResult, error) {
+func getMQEMetricsType(ctx context.Context, _ *mcp.CallToolRequest, req MQEMetricsTypeRequest) (*mcp.CallToolResult, any, error) {
 	if req.MetricName == "" {
-		return mcp.NewToolResultError("metric_name must be provided"), nil
+		return ResultError("metric_name must be provided"), nil, nil
 	}
 	if err := validateMetricName(req.MetricName); err != nil {
-		return mcp.NewToolResultError(err.Error()), nil
+		return ResultError(err.Error()), nil, nil
 	}
 
 	// GraphQL query for getting metric type
@@ -489,14 +469,14 @@ func getMQEMetricsType(ctx context.Context, req *MQEMetricsTypeRequest) (*mcp.Ca
 
 	result, err := executeGraphQLWithContext(ctx, query, variables)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("failed to get metric type: %v", err)), nil
+		return ResultError(fmt.Sprintf("failed to get metric type: %v", err)), nil, nil
 	}
 
 	jsonBytes, err := json.Marshal(result.Data)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil
+		return ResultError(fmt.Sprintf("failed to marshal result: %v", err)), nil, nil
 	}
-	return mcp.NewToolResultText(string(jsonBytes)), nil
+	return ResultText(string(jsonBytes)), nil, nil
 }
 
 func validateMQEExpressionRequest(req *MQEExpressionRequest) error {
@@ -647,9 +627,44 @@ func nestingDepth(value string) int {
 	return maxDepth
 }
 
-var MQEExpressionTool = NewTool(
-	"execute_mqe_expression",
-	`Execute MQE (Metrics Query Expression) to query and calculate metrics data.
+func mqeExpressionTool() *mcp.Tool {
+	schema := InferSchema[MQEExpressionRequest]()
+	WithRequired(schema, "expression")
+	WithEnum(schema, "step", "SECOND", "MINUTE", "HOUR", "DAY", "MONTH")
+	WithDescriptions(schema, map[string]string{
+		"expression": "MQE expression to execute (required). " +
+			"Examples: `service_sla`, `avg(service_cpm)`, `service_sla * 100`, `service_percentile{p='50,75,90,95,99'}`",
+		"service_name": "Service name for entity filtering",
+		"layer": "Service layer for entity filtering. " +
+			"Examples: `GENERAL` (default), `MESH`, `K8S_SERVICE`, `DATABASE`, `VIRTUAL_DATABASE`. " +
+			"Defaults to GENERAL if not specified",
+		"service_instance_name": "Service instance name for entity filtering",
+		"endpoint_name":         "Endpoint name for entity filtering",
+		"process_name":          "Process name for entity filtering",
+		"normal": "Whether the service is normal (has agent installed). " +
+			"If not specified, will be auto-detected based on service layer",
+		"dest_service_name": "Destination service name for relation metrics",
+		"dest_layer": "Destination service layer for relation metrics. " +
+			"Examples: `GENERAL`, `MESH`, `K8S_SERVICE`, `DATABASE`",
+		"dest_service_instance_name": "Destination service instance name for relation metrics",
+		"dest_endpoint_name":         "Destination endpoint name for relation metrics",
+		"dest_process_name":          "Destination process name for relation metrics",
+		"dest_normal":                "Whether the destination service is normal",
+		"start":                      "Start time for the query. Examples: `2025-07-06 12:00:00`, `-1h` (1 hour ago), `-30m` (30 minutes ago)",
+		"end":                        "End time for the query. Examples: `2025-07-06 13:00:00`, `now`, `-10m` (10 minutes ago)",
+		"step": "Time step between start time and end time: " +
+			"SECOND (second-level), MINUTE (minute-level), HOUR (hour-level), " +
+			"DAY (day-level), MONTH (month-level). " +
+			"If not specified, uses adaptive step sizing: " +
+			"SECOND (<1h), MINUTE (1h-24h), HOUR (1d-7d), DAY (>7d)",
+		"cold":        "Whether to query from cold-stage storage",
+		"debug":       "Enable query tracing and debugging",
+		"dump_db_rsp": "Dump database response for debugging",
+	})
+
+	return &mcp.Tool{
+		Name: "execute_mqe_expression",
+		Description: `Execute MQE (Metrics Query Expression) to query and calculate metrics data.
 
 MQE is SkyWalking's powerful query language that allows you to:
 - Query metrics with labels: service_percentile{p='50,75,90,95,99'}
@@ -696,45 +711,20 @@ Examples:
 - {expression: "service_cpm", start: "now", end: "+24h"}: Query CPM for next 24 hours (useful for capacity planning)
 - {expression: "top_n(service_cpm, 10, des)", start: "2025-07-06 16:00:00", end: "2025-07-06 17:00:00", 
 	step: "MINUTE"}: Top 10 services by CPM with minute granularity`,
-	executeMQEExpression,
-	mcp.WithString("expression", mcp.Required(),
-		mcp.Description("MQE expression to execute (required). "+
-			"Examples: `service_sla`, `avg(service_cpm)`, `service_sla * 100`, `service_percentile{p='50,75,90,95,99'}`")),
-	mcp.WithString("service_name", mcp.Description("Service name for entity filtering")),
-	mcp.WithString("layer",
-		mcp.Description("Service layer for entity filtering. "+
-			"Examples: `GENERAL` (default), `MESH`, `K8S_SERVICE`, `DATABASE`, `VIRTUAL_DATABASE`. "+
-			"Defaults to GENERAL if not specified")),
-	mcp.WithString("service_instance_name", mcp.Description("Service instance name for entity filtering")),
-	mcp.WithString("endpoint_name", mcp.Description("Endpoint name for entity filtering")),
-	mcp.WithString("process_name", mcp.Description("Process name for entity filtering")),
-	mcp.WithBoolean("normal",
-		mcp.Description("Whether the service is normal (has agent installed). "+
-			"If not specified, will be auto-detected based on service layer")),
-	mcp.WithString("dest_service_name", mcp.Description("Destination service name for relation metrics")),
-	mcp.WithString("dest_layer",
-		mcp.Description("Destination service layer for relation metrics. "+
-			"Examples: `GENERAL`, `MESH`, `K8S_SERVICE`, `DATABASE`")),
-	mcp.WithString("dest_service_instance_name", mcp.Description("Destination service instance name for relation metrics")),
-	mcp.WithString("dest_endpoint_name", mcp.Description("Destination endpoint name for relation metrics")),
-	mcp.WithString("dest_process_name", mcp.Description("Destination process name for relation metrics")),
-	mcp.WithBoolean("dest_normal", mcp.Description("Whether the destination service is normal")),
-	mcp.WithString("start", mcp.Description("Start time for the query. Examples: `2025-07-06 12:00:00`, `-1h` (1 hour ago), `-30m` (30 minutes ago)")),
-	mcp.WithString("end", mcp.Description("End time for the query. Examples: `2025-07-06 13:00:00`, `now`, `-10m` (10 minutes ago)")),
-	mcp.WithString("step", mcp.Enum("SECOND", "MINUTE", "HOUR", "DAY", "MONTH"),
-		mcp.Description("Time step between start time and end time: "+
-			"SECOND (second-level), MINUTE (minute-level), HOUR (hour-level), "+
-			"DAY (day-level), MONTH (month-level). "+
-			"If not specified, uses adaptive step sizing: "+
-			"SECOND (<1h), MINUTE (1h-24h), HOUR (1d-7d), DAY (>7d)")),
-	mcp.WithBoolean("cold", mcp.Description("Whether to query from cold-stage storage")),
-	mcp.WithBoolean("debug", mcp.Description("Enable query tracing and debugging")),
-	mcp.WithBoolean("dump_db_rsp", mcp.Description("Dump database response for debugging")),
-)
+		InputSchema: schema,
+		Annotations: &mcp.ToolAnnotations{Title: "execute_mqe_expression", IdempotentHint: true},
+	}
+}
+
+func mqeMetricsListTool() *mcp.Tool {
+	schema := InferSchema[MQEMetricsListRequest]()
+	WithDescriptions(schema, map[string]string{
+		"regex": "Optional regex pattern to filter metrics by name. Examples: `service_.*`, `.*_cpm`, `endpoint_.*`",
+	})
 
-var MQEMetricsListTool = NewTool(
-	"list_mqe_metrics",
-	`List available metrics in SkyWalking that can be used in MQE expressions.
+	return &mcp.Tool{
+		Name: "list_mqe_metrics",
+		Description: `List available metrics in SkyWalking that can be used in MQE expressions.
 
 This tool helps you discover what metrics are available for querying and their metadata information 
 including metric type and catalog. You can optionally provide a regex pattern to filter the metrics by name.
@@ -763,13 +753,22 @@ Examples:
 - {regex: ".*_cpm"}: List all CPM (calls per minute) metrics
 - {regex: ".*percentile.*"}: List all percentile metrics
 - {}: List all available metrics`,
-	listMQEMetrics,
-	mcp.WithString("regex", mcp.Description("Optional regex pattern to filter metrics by name. Examples: `service_.*`, `.*_cpm`, `endpoint_.*`")),
-)
+		InputSchema: schema,
+		Annotations: &mcp.ToolAnnotations{Title: "list_mqe_metrics", IdempotentHint: true},
+	}
+}
+
+func mqeMetricsTypeTool() *mcp.Tool {
+	schema := InferSchema[MQEMetricsTypeRequest]()
+	WithRequired(schema, "metric_name")
+	WithDescriptions(schema, map[string]string{
+		"metric_name": "Name of the metric to get type information for (required). " +
+			"Examples: `service_sla`, `service_percentile`, `endpoint_cpm`",
+	})
 
-var MQEMetricsTypeTool = NewTool(
-	"get_mqe_metric_type",
-	`Get type information for a specific metric.
+	return &mcp.Tool{
+		Name: "get_mqe_metric_type",
+		Description: `Get type information for a specific metric.
 
 This tool returns the type and catalog information for a given metric name, which helps understand 
 what kind of data the metric contains and how it should be used in MQE expressions.
@@ -800,8 +799,7 @@ Examples:
 - {metric_name: "service_cpm"}: Get type info for service CPM metric
 - {metric_name: "service_percentile"}: Get type info for service percentile metric
 - {metric_name: "endpoint_sla"}: Get type info for endpoint SLA metric`,
-	getMQEMetricsType,
-	mcp.WithString("metric_name", mcp.Required(),
-		mcp.Description("Name of the metric to get type information for (required). "+
-			"Examples: `service_sla`, `service_percentile`, `endpoint_cpm`")),
-)
+		InputSchema: schema,
+		Annotations: &mcp.ToolAnnotations{Title: "get_mqe_metric_type", IdempotentHint: true},
+	}
+}
diff --git a/internal/tools/mqe_test.go b/internal/tools/mqe_test.go
index 2c1a1c9..5ca7fd5 100644
--- a/internal/tools/mqe_test.go
+++ b/internal/tools/mqe_test.go
@@ -24,7 +24,8 @@ import (
 	"testing"
 
 	"github.com/apache/skywalking-cli/pkg/contextkey"
-	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/google/jsonschema-go/jsonschema"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 )
 
 func TestValidateMQEExpressionRequestRejectsDeeplyNestedExpression(t *testing.T) {
@@ -52,13 +53,37 @@ func TestValidateMetricNameRejectsInvalidCharacters(t *testing.T) {
 	}
 }
 
+func TestMQEExpressionToolSchema(t *testing.T) {
+	schema, ok := mqeExpressionTool().InputSchema.(*jsonschema.Schema)
+	if !ok {
+		t.Fatalf("input schema type = %T", mqeExpressionTool().InputSchema)
+	}
+
+	for _, property := range []string{"expression", "service_name", "debug"} {
+		prop, found := schema.Properties[property]
+		if !found {
+			t.Fatalf("property %q missing", property)
+		}
+		if prop.Description == "" {
+			t.Fatalf("property %q description is empty", property)
+		}
+	}
+
+	if got := schema.Required; len(got) != 1 || got[0] != "expression" {
+		t.Fatalf("required = %v, want [expression]", got)
+	}
+	if got := len(schema.Properties["step"].Enum); got != 5 {
+		t.Fatalf("step enum count = %d, want 5", got)
+	}
+}
+
 func TestExecuteMQEExpressionRejectsOverlongEntityField(t *testing.T) {
 	req := &MQEExpressionRequest{
 		Expression:  "service_cpm",
 		ServiceName: strings.Repeat("a", maxMQEEntityFieldLen+1),
 	}
 
-	result, err := executeMQEExpression(context.Background(), req)
+	result, _, err := executeMQEExpression(context.Background(), nil, *req)
 	if err != nil {
 		t.Fatalf("executeMQEExpression returned error: %v", err)
 	}
@@ -109,7 +134,7 @@ func assertToolResultContains(t *testing.T, result *mcp.CallToolResult, want str
 	if len(result.Content) == 0 {
 		t.Fatal("tool result had no content")
 	}
-	text, ok := result.Content[0].(mcp.TextContent)
+	text, ok := result.Content[0].(*mcp.TextContent)
 	if !ok {
 		t.Fatalf("unexpected content type: %T", result.Content[0])
 	}
diff --git a/internal/tools/result.go b/internal/tools/result.go
new file mode 100644
index 0000000..5fed334
--- /dev/null
+++ b/internal/tools/result.go
@@ -0,0 +1,119 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package tools
+
+import (
+	"fmt"
+
+	"github.com/google/jsonschema-go/jsonschema"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
+)
+
+func ResultText(text string) *mcp.CallToolResult {
+	return &mcp.CallToolResult{
+		Content: []mcp.Content{&mcp.TextContent{Text: text}},
+	}
+}
+
+// ResultError reports an expected query failure to the model instead of
+// surfacing it as a protocol error.
+func ResultError(text string) *mcp.CallToolResult {
+	return &mcp.CallToolResult{
+		IsError: true,
+		Content: []mcp.Content{&mcp.TextContent{Text: text}},
+	}
+}
+
+// InferSchema derives the JSON schema of a tool's input type. Registration
+// happens at startup, so an inference failure is a programming error.
+//
+// A parameterless input infers to a schema without "properties" at all, which
+// OpenAI's strict mode rejects, so the empty object is filled in.
+func InferSchema[T any]() *jsonschema.Schema {
+	schema, err := jsonschema.For[T](nil)
+	if err != nil {
+		panic(fmt.Sprintf("infer schema for %T: %v", *new(T), err))
+	}
+	if schema.Properties == nil {
+		schema.Properties = map[string]*jsonschema.Schema{}
+	}
+	// Inference marks structs additionalProperties:false and the SDK validates
+	// inputs before the handler runs. The previous SDK ignored unknown fields
+	// and clients do send extras, so restore the lenient behavior at every
+	// nesting level.
+	relaxAdditionalProperties(schema)
+	return schema
+}
+
+// relaxAdditionalProperties removes the additionalProperties:false constraint
+// (represented as {"not": {}}) from schema and every nested object schema.
+func relaxAdditionalProperties(schema *jsonschema.Schema) {
+	if schema == nil {
+		return
+	}
+	if schema.AdditionalProperties != nil && schema.AdditionalProperties.Not != nil {
+		schema.AdditionalProperties = nil
+	}
+	for _, property := range schema.Properties {
+		relaxAdditionalProperties(property)
+	}
+	relaxAdditionalProperties(schema.Items)
+}
+
+// stepEnum lists the time-step granularities shared by the query tools. The
+// MQE tool additionally accepts MONTH.
+var stepEnum = []string{"SECOND", "MINUTE", "HOUR", "DAY"}
+
+// WithRequired declares which properties a tool requires. Inference derives
+// requiredness from the absence of omitempty, which ties the API contract to
+// serialization behavior; declaring it here keeps the contract explicit and
+// reviewable.
+func WithRequired(schema *jsonschema.Schema, properties ...string) {
+	for _, property := range properties {
+		if _, ok := schema.Properties[property]; !ok {
+			panic(fmt.Sprintf("required on unknown property %q", property))
+		}
+	}
+	schema.Required = properties
+}
+
+// WithDescriptions sets property descriptions that a struct tag cannot hold,
+// such as ones containing backticks.
+func WithDescriptions(schema *jsonschema.Schema, descriptions map[string]string) {
+	for property, description := range descriptions {
+		prop, ok := schema.Properties[property]
+		if !ok {
+			panic(fmt.Sprintf("description on unknown property %q", property))
+		}
+		prop.Description = description
+	}
+}
+
+// WithEnum constrains a property to a fixed set of values. The jsonschema
+// struct tag only carries a description, so enums are applied to the inferred
+// schema instead.
+func WithEnum(schema *jsonschema.Schema, property string, values ...string) {
+	prop, ok := schema.Properties[property]
+	if !ok {
+		panic(fmt.Sprintf("enum on unknown property %q", property))
+	}
+	prop.Enum = make([]any, len(values))
+	for i, v := range values {
+		prop.Enum[i] = v
+	}
+}
diff --git a/internal/tools/result_test.go b/internal/tools/result_test.go
new file mode 100644
index 0000000..7640bdc
--- /dev/null
+++ b/internal/tools/result_test.go
@@ -0,0 +1,41 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package tools
+
+import (
+	"testing"
+)
+
+// The SDK validates inputs against the inferred schema before the handler
+// runs; the previous SDK ignored unknown fields, so the schema must not
+// reject them at any nesting level.
+func TestInferSchemaAllowsUnknownFieldsAtEveryLevel(t *testing.T) {
+	schema := InferSchema[LogQueryRequest]()
+
+	if schema.AdditionalProperties != nil {
+		t.Fatal("top-level additionalProperties not removed")
+	}
+
+	items := schema.Properties["tags"].Items
+	if items == nil {
+		t.Fatal("tags items schema missing")
+	}
+	if items.AdditionalProperties != nil {
+		t.Fatal("nested tag additionalProperties not removed")
+	}
+}
diff --git a/internal/tools/tools.go b/internal/tools/tools.go
deleted file mode 100644
index 37e9b7d..0000000
--- a/internal/tools/tools.go
+++ /dev/null
@@ -1,102 +0,0 @@
-// Licensed to Apache Software Foundation (ASF) under one or more contributor
-// license agreements. See the NOTICE file distributed with
-// this work for additional information regarding copyright
-// ownership. Apache Software Foundation (ASF) licenses this file to you under
-// the Apache License, Version 2.0 (the "License"); you may
-// not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied.  See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-package tools
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
-)
-
-type Tool[T any, R any] struct {
-	Name        string
-	Description string
-	Handler     func(ctx context.Context, args *T) (R, error)
-	Options     []mcp.ToolOption
-}
-
-func NewTool[T any, R any](
-	name, desc string,
-	handler func(ctx context.Context, args *T) (R, error),
-	options ...mcp.ToolOption,
-) *Tool[T, R] {
-	return &Tool[T, R]{
-		Name:        name,
-		Description: desc,
-		Handler:     handler,
-		Options:     options,
-	}
-}
-
-// Register registers the tool with the given MCP server.
-func (t *Tool[T, R]) Register(server *server.MCPServer) {
-	tool, handler, err := ConvertTool(t.Name, t.Description, t.Handler, t.Options...)
-	if err != nil {
-		panic(err)
-	}
-
-	server.AddTool(tool, handler)
-}
-
-func ConvertTool[T any, R any](
-	name string,
-	desc string,
-	handlerFunc func(ctx context.Context, args *T) (R, error),
-	options ...mcp.ToolOption,
-) (mcp.Tool, server.ToolHandlerFunc, error) {
-	baseOptions := []mcp.ToolOption{
-		mcp.WithDescription(desc),
-		mcp.WithTitleAnnotation(name),
-		mcp.WithIdempotentHintAnnotation(true), // we assume tools are idempotent by default
-	}
-
-	baseOptions = append(baseOptions, options...)
-	tool := mcp.NewTool(name, baseOptions...)
-
-	handler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-		var args T
-		if err := request.BindArguments(&args); err != nil {
-			return nil, fmt.Errorf("failed to bind arguments: %w", err)
-		}
-
-		result, err := handlerFunc(ctx, &args)
-		if err != nil {
-			return nil, err
-		}
-
-		switch v := any(result).(type) {
-		case *mcp.CallToolResult:
-			return v, nil
-		case mcp.CallToolResult:
-			return &v, nil
-		case nil:
-			return nil, nil
-		default:
-			jsonBytes, err := json.Marshal(v)
-			if err != nil {
-				return nil, fmt.Errorf("failed to marshal return value: %s", err)
-			}
-			return mcp.NewToolResultText(string(jsonBytes)), nil
-		}
-	}
-
-	return tool, handler, nil
-}
diff --git a/internal/tools/topology.go b/internal/tools/topology.go
index f64a1f0..8b19bf9 100644
--- a/internal/tools/topology.go
+++ b/internal/tools/topology.go
@@ -23,8 +23,7 @@ import (
 	"fmt"
 
 	"github.com/machinebox/graphql"
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 	api "skywalking.apache.org/repo/goapi/query"
 
 	"github.com/apache/skywalking-cli/pkg/graphql/client"
@@ -32,11 +31,11 @@ import (
 )
 
 // AddTopologyTools registers topology-related tools with the MCP server
-func AddTopologyTools(s *server.MCPServer) {
-	ServicesTopologyTool.Register(s)
-	InstancesTopologyTool.Register(s)
-	EndpointsTopologyTool.Register(s)
-	ProcessesTopologyTool.Register(s)
+func AddTopologyTools(s *mcp.Server) {
+	mcp.AddTool(s, servicesTopologyTool(), queryServicesTopology)
+	mcp.AddTool(s, instancesTopologyTool(), queryInstancesTopology)
+	mcp.AddTool(s, endpointsTopologyTool(), queryEndpointsTopology)
+	mcp.AddTool(s, processesTopologyTool(), queryProcessesTopology)
 }
 
 const getServicesTopologyGQL = `
@@ -57,14 +56,14 @@ func servicesTopology(ctx context.Context, serviceIDs []string, duration api.Dur
 }
 
 type ServicesTopologyRequest struct {
-	ServiceIDs []string `json:"service_ids,omitempty"`
+	ServiceIDs []string `json:"service_ids,omitempty" jsonschema:"List of service IDs to scope the topology. If empty, the global topology is returned."`
 	Layer      string   `json:"layer,omitempty"`
-	Start      string   `json:"start,omitempty"`
-	End        string   `json:"end,omitempty"`
-	Step       string   `json:"step,omitempty"`
+	Start      string   `json:"start,omitempty" jsonschema:"Start time for the query."`
+	End        string   `json:"end,omitempty" jsonschema:"End time for the query. Default is now."`
+	Step       string   `json:"step,omitempty" jsonschema:"Time step granularity. If not specified, uses adaptive step sizing."`
 }
 
-func queryServicesTopology(ctx context.Context, req *ServicesTopologyRequest) (*mcp.CallToolResult, error) {
+func queryServicesTopology(ctx context.Context, _ *mcp.CallToolRequest, req ServicesTopologyRequest) (*mcp.CallToolResult, any, error) {
 	timeCtx := GetTimeContext(ctx)
 	duration := BuildDurationWithContext(req.Start, req.End, req.Step, false, DefaultDuration, timeCtx)
 
@@ -82,149 +81,154 @@ func queryServicesTopology(ctx context.Context, req *ServicesTopologyRequest) (*
 	}
 
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("failed to query topology: %v", err)), nil
+		return ResultError(fmt.Sprintf("failed to query topology: %v", err)), nil, nil
 	}
 
 	jsonBytes, err := json.Marshal(topology)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil
+		return ResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil, nil
 	}
-	return mcp.NewToolResultText(string(jsonBytes)), nil
+	return ResultText(string(jsonBytes)), nil, nil
 }
 
 type InstancesTopologyRequest struct {
-	ClientServiceID string `json:"client_service_id"`
-	ServerServiceID string `json:"server_service_id"`
-	Start           string `json:"start,omitempty"`
-	End             string `json:"end,omitempty"`
-	Step            string `json:"step,omitempty"`
+	ClientServiceID string `json:"client_service_id,omitempty" jsonschema:"The ID of the client (upstream) service."`
+	ServerServiceID string `json:"server_service_id,omitempty" jsonschema:"The ID of the server (downstream) service."`
+	Start           string `json:"start,omitempty" jsonschema:"Start time for the query."`
+	End             string `json:"end,omitempty" jsonschema:"End time for the query. Default is now."`
+	Step            string `json:"step,omitempty" jsonschema:"Time step granularity. If not specified, uses adaptive step sizing."`
 }
 
-func queryInstancesTopology(ctx context.Context, req *InstancesTopologyRequest) (*mcp.CallToolResult, error) {
+func queryInstancesTopology(ctx context.Context, _ *mcp.CallToolRequest, req InstancesTopologyRequest) (*mcp.CallToolResult, any, error) {
 	timeCtx := GetTimeContext(ctx)
 	duration := BuildDurationWithContext(req.Start, req.End, req.Step, false, DefaultDuration, timeCtx)
 
 	topology, err := dependency.InstanceTopology(ctx, req.ClientServiceID, req.ServerServiceID, duration)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("failed to query instances topology: %v", err)), nil
+		return ResultError(fmt.Sprintf("failed to query instances topology: %v", err)), nil, nil
 	}
 
 	jsonBytes, err := json.Marshal(topology)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil
+		return ResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil, nil
 	}
-	return mcp.NewToolResultText(string(jsonBytes)), nil
+	return ResultText(string(jsonBytes)), nil, nil
 }
 
 type EndpointsTopologyRequest struct {
-	EndpointID string `json:"endpoint_id"`
-	Start      string `json:"start,omitempty"`
-	End        string `json:"end,omitempty"`
-	Step       string `json:"step,omitempty"`
+	EndpointID string `json:"endpoint_id,omitempty" jsonschema:"The ID of the endpoint to query dependencies for."`
+	Start      string `json:"start,omitempty" jsonschema:"Start time for the query."`
+	End        string `json:"end,omitempty" jsonschema:"End time for the query. Default is now."`
+	Step       string `json:"step,omitempty" jsonschema:"Time step granularity. If not specified, uses adaptive step sizing."`
 }
 
-func queryEndpointsTopology(ctx context.Context, req *EndpointsTopologyRequest) (*mcp.CallToolResult, error) {
+func queryEndpointsTopology(ctx context.Context, _ *mcp.CallToolRequest, req EndpointsTopologyRequest) (*mcp.CallToolResult, any, error) {
 	timeCtx := GetTimeContext(ctx)
 	duration := BuildDurationWithContext(req.Start, req.End, req.Step, false, DefaultDuration, timeCtx)
 
 	topology, err := dependency.EndpointDependency(ctx, req.EndpointID, duration)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("failed to query endpoints topology: %v", err)), nil
+		return ResultError(fmt.Sprintf("failed to query endpoints topology: %v", err)), nil, nil
 	}
 
 	jsonBytes, err := json.Marshal(topology)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil
+		return ResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil, nil
 	}
-	return mcp.NewToolResultText(string(jsonBytes)), nil
+	return ResultText(string(jsonBytes)), nil, nil
 }
 
 type ProcessesTopologyRequest struct {
-	ServiceInstanceID string `json:"service_instance_id"`
-	Start             string `json:"start,omitempty"`
-	End               string `json:"end,omitempty"`
-	Step              string `json:"step,omitempty"`
+	ServiceInstanceID string `json:"service_instance_id,omitempty" jsonschema:"The ID of the service instance to query process topology for."`
+	Start             string `json:"start,omitempty" jsonschema:"Start time for the query."`
+	End               string `json:"end,omitempty" jsonschema:"End time for the query. Default is now."`
+	Step              string `json:"step,omitempty" jsonschema:"Time step granularity. If not specified, uses adaptive step sizing."`
 }
 
-func queryProcessesTopology(ctx context.Context, req *ProcessesTopologyRequest) (*mcp.CallToolResult, error) {
+func queryProcessesTopology(ctx context.Context, _ *mcp.CallToolRequest, req ProcessesTopologyRequest) (*mcp.CallToolResult, any, error) {
 	timeCtx := GetTimeContext(ctx)
 	duration := BuildDurationWithContext(req.Start, req.End, req.Step, false, DefaultDuration, timeCtx)
 
 	topology, err := dependency.ProcessTopology(ctx, req.ServiceInstanceID, duration)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("failed to query processes topology: %v", err)), nil
+		return ResultError(fmt.Sprintf("failed to query processes topology: %v", err)), nil, nil
 	}
 
 	jsonBytes, err := json.Marshal(topology)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil
+		return ResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil, nil
 	}
-	return mcp.NewToolResultText(string(jsonBytes)), nil
+	return ResultText(string(jsonBytes)), nil, nil
 }
 
-var ProcessesTopologyTool = NewTool(
-	"query_processes_topology",
-	`Query the process topology for a given service instance from SkyWalking OAP (getProcessTopology).
+func processesTopologyTool() *mcp.Tool {
+	schema := InferSchema[ProcessesTopologyRequest]()
+	WithRequired(schema, "service_instance_id")
+	WithEnum(schema, "step", stepEnum...)
+
+	return &mcp.Tool{
+		Name: "query_processes_topology",
+		Description: `Query the process topology for a given service instance from SkyWalking OAP (getProcessTopology).
 
 Returns the topology of processes running within the given service instance, including process nodes and the calls between them.
 
 Examples:
 - {"service_instance_id": "instance-id-1"}: Process topology for the last 30 minutes
 - {"service_instance_id": "instance-id-1", "start": "-1h"}: Process topology for the last hour`,
-	queryProcessesTopology,
-	mcp.WithString("service_instance_id",
-		mcp.Required(),
-		mcp.Description("The ID of the service instance to query process topology for.")),
-	mcp.WithString("start", mcp.Description("Start time for the query.")),
-	mcp.WithString("end", mcp.Description("End time for the query. Default is now.")),
-	mcp.WithString("step", mcp.Enum("SECOND", "MINUTE", "HOUR", "DAY"),
-		mcp.Description("Time step granularity. If not specified, uses adaptive step sizing.")),
-)
+		InputSchema: schema,
+		Annotations: &mcp.ToolAnnotations{Title: "query_processes_topology", IdempotentHint: true},
+	}
+}
 
-var EndpointsTopologyTool = NewTool(
-	"query_endpoints_topology",
-	`Query the endpoint dependency topology for a given endpoint from SkyWalking OAP (getEndpointDependencies).
+func endpointsTopologyTool() *mcp.Tool {
+	schema := InferSchema[EndpointsTopologyRequest]()
+	WithRequired(schema, "endpoint_id")
+	WithEnum(schema, "step", stepEnum...)
+
+	return &mcp.Tool{
+		Name: "query_endpoints_topology",
+		Description: `Query the endpoint dependency topology for a given endpoint from SkyWalking OAP (getEndpointDependencies).
 
 Returns the topology of endpoints that the given endpoint depends on or is depended upon by, including endpoint nodes and the calls between them.
 
 Examples:
 - {"endpoint_id": "ep-id-1"}: Endpoint topology for the last 30 minutes
 - {"endpoint_id": "ep-id-1", "start": "-1h"}: Endpoint topology for the last hour`,
-	queryEndpointsTopology,
-	mcp.WithString("endpoint_id",
-		mcp.Required(),
-		mcp.Description("The ID of the endpoint to query dependencies for.")),
-	mcp.WithString("start", mcp.Description("Start time for the query.")),
-	mcp.WithString("end", mcp.Description("End time for the query. Default is now.")),
-	mcp.WithString("step", mcp.Enum("SECOND", "MINUTE", "HOUR", "DAY"),
-		mcp.Description("Time step granularity. If not specified, uses adaptive step sizing.")),
-)
+		InputSchema: schema,
+		Annotations: &mcp.ToolAnnotations{Title: "query_endpoints_topology", IdempotentHint: true},
+	}
+}
 
-var InstancesTopologyTool = NewTool(
-	"query_instances_topology",
-	`Query the service instance topology between two services from SkyWalking OAP (getServiceInstanceTopology).
+func instancesTopologyTool() *mcp.Tool {
+	schema := InferSchema[InstancesTopologyRequest]()
+	WithRequired(schema, "client_service_id", "server_service_id")
+	WithEnum(schema, "step", stepEnum...)
+
+	return &mcp.Tool{
+		Name: "query_instances_topology",
+		Description: `Query the service instance topology between two services from SkyWalking OAP (getServiceInstanceTopology).
 
 Returns the topology of service instances for the given client and server services, including instance nodes and the calls between them.
 
 Examples:
 - {"client_service_id": "svc-id-1", "server_service_id": "svc-id-2"}: Instance topology for the last 30 minutes
 - {"client_service_id": "svc-id-1", "server_service_id": "svc-id-2", "start": "-1h"}: Instance topology for the last hour`,
-	queryInstancesTopology,
-	mcp.WithString("client_service_id",
-		mcp.Required(),
-		mcp.Description("The ID of the client (upstream) service.")),
-	mcp.WithString("server_service_id",
-		mcp.Required(),
-		mcp.Description("The ID of the server (downstream) service.")),
-	mcp.WithString("start", mcp.Description("Start time for the query.")),
-	mcp.WithString("end", mcp.Description("End time for the query. Default is now.")),
-	mcp.WithString("step", mcp.Enum("SECOND", "MINUTE", "HOUR", "DAY"),
-		mcp.Description("Time step granularity. If not specified, uses adaptive step sizing.")),
-)
+		InputSchema: schema,
+		Annotations: &mcp.ToolAnnotations{Title: "query_instances_topology", IdempotentHint: true},
+	}
+}
 
-var ServicesTopologyTool = NewTool(
-	"query_services_topology",
-	`Query the service topology from SkyWalking OAP.
+func servicesTopologyTool() *mcp.Tool {
+	schema := InferSchema[ServicesTopologyRequest]()
+	WithEnum(schema, "step", stepEnum...)
+	WithDescriptions(schema, map[string]string{
+		"layer": "Layer to filter the global topology (e.g. GENERAL, MESH). " +
+			"Only used when service_ids is empty.",
+	})
+
+	return &mcp.Tool{
+		Name: "query_services_topology",
+		Description: `Query the service topology from SkyWalking OAP.
 
 - If service_ids is provided, returns the topology scoped to those specific services (getServicesTopology).
 - Otherwise, returns the global topology across all services (getGlobalTopology), optionally filtered by layer.
@@ -233,15 +237,7 @@ Examples:
 - {}: Global topology for the last 30 minutes
 - {"layer": "GENERAL"}: Global topology for a specific layer
 - {"service_ids": ["svc-id-1", "svc-id-2"], "start": "-1h"}: Topology for specific services`,
-	queryServicesTopology,
-	mcp.WithArray("service_ids",
-		mcp.Description("List of service IDs to scope the topology. If empty, the global topology is returned."),
-		mcp.WithStringItems(),
-	),
-	mcp.WithString("layer",
-		mcp.Description("Layer to filter the global topology (e.g. GENERAL, MESH). Only used when service_ids is empty.")),
-	mcp.WithString("start", mcp.Description("Start time for the query.")),
-	mcp.WithString("end", mcp.Description("End time for the query. Default is now.")),
-	mcp.WithString("step", mcp.Enum("SECOND", "MINUTE", "HOUR", "DAY"),
-		mcp.Description("Time step granularity. If not specified, uses adaptive step sizing.")),
-)
+		InputSchema: schema,
+		Annotations: &mcp.ToolAnnotations{Title: "query_services_topology", IdempotentHint: true},
+	}
+}
diff --git a/internal/tools/trace.go b/internal/tools/trace.go
index 785e65a..009b081 100644
--- a/internal/tools/trace.go
+++ b/internal/tools/trace.go
@@ -25,16 +25,15 @@ import (
 	"sort"
 
 	"github.com/machinebox/graphql"
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 	api "skywalking.apache.org/repo/goapi/query"
 
 	"github.com/apache/skywalking-cli/pkg/graphql/client"
 )
 
 // AddTraceTools registers trace-related tools with the MCP server
-func AddTraceTools(mcp *server.MCPServer) {
-	TracesQueryTool.Register(mcp)
+func AddTraceTools(s *mcp.Server) {
+	mcp.AddTool(s, tracesQueryTool(), searchTraces)
 }
 
 // View constants
@@ -477,9 +476,9 @@ func buildQueryCondition(req *TracesQueryRequest, timeCtx TimeContext) (*api.Tra
 }
 
 // searchTraces fetches traces based on query conditions
-func searchTraces(ctx context.Context, req *TracesQueryRequest) (*mcp.CallToolResult, error) {
-	if err := validateTracesQueryRequest(req); err != nil {
-		return mcp.NewToolResultError(err.Error()), nil
+func searchTraces(ctx context.Context, _ *mcp.CallToolRequest, req TracesQueryRequest) (*mcp.CallToolResult, any, error) {
+	if err := validateTracesQueryRequest(&req); err != nil {
+		return ResultError(err.Error()), nil, nil
 	}
 
 	// Set default view
@@ -489,9 +488,9 @@ func searchTraces(ctx context.Context, req *TracesQueryRequest) (*mcp.CallToolRe
 
 	// Build query condition
 	timeCtx := GetTimeContext(ctx)
-	condition, err := buildQueryCondition(req, timeCtx)
+	condition, err := buildQueryCondition(&req, timeCtx)
 	if err != nil {
-		return mcp.NewToolResultError(err.Error()), nil
+		return ResultError(err.Error()), nil, nil
 	}
 
 	// Execute the query, automatically selecting the trace protocol supported by
@@ -499,16 +498,16 @@ func searchTraces(ctx context.Context, req *TracesQueryRequest) (*mcp.CallToolRe
 	// queryBasicTraces + queryTrace (v1) for Elasticsearch/JDBC and older OAP.
 	traceList, err := queryTracesAuto(ctx, condition)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf(ErrFailedToQueryTraces, err)), nil
+		return ResultError(fmt.Sprintf(ErrFailedToQueryTraces, err)), nil, nil
 	}
 
-	return processTracesResult(&traceList, req.View, req.SlowTraceThreshold)
+	return processTracesResult(&traceList, req.View, req.SlowTraceThreshold), nil, nil
 }
 
 // processTracesResult handles the common logic for processing traces query results
-func processTracesResult(traces *api.TraceList, view string, slowTraceThreshold int64) (*mcp.CallToolResult, error) {
+func processTracesResult(traces *api.TraceList, view string, slowTraceThreshold int64) *mcp.CallToolResult {
 	if traces == nil || len(traces.Traces) == 0 {
-		return mcp.NewToolResultError(ErrNoTracesFound), nil
+		return ResultError(ErrNoTracesFound)
 	}
 
 	var result interface{}
@@ -520,14 +519,14 @@ func processTracesResult(traces *api.TraceList, view string, slowTraceThreshold
 	case ViewFull:
 		result = traces
 	default:
-		return mcp.NewToolResultError(fmt.Sprintf(ErrInvalidView, view, ViewFull, ViewSummary, ViewErrorsOnly)), nil
+		return ResultError(fmt.Sprintf(ErrInvalidView, view, ViewFull, ViewSummary, ViewErrorsOnly))
 	}
 
 	jsonBytes, err := json.Marshal(result)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf(ErrMarshalFailed, err)), nil
+		return ResultError(fmt.Sprintf(ErrMarshalFailed, err))
 	}
-	return mcp.NewToolResultText(string(jsonBytes)), nil
+	return ResultText(string(jsonBytes))
 }
 
 // processTraceItem processes a single TraceV2 item and updates summary statistics
@@ -684,9 +683,46 @@ func filterErrorTraces(traces *api.TraceList) []BasicTraceSummary {
 }
 
 // TracesQueryTool is a tool for querying traces with various conditions
-var TracesQueryTool = NewTool(
-	"query_traces",
-	`This tool queries traces from SkyWalking OAP based on various conditions and provides intelligent data processing for LLM analysis.
+func tracesQueryTool() *mcp.Tool {
+	schema := InferSchema[TracesQueryRequest]()
+	WithEnum(schema, "step", stepEnum...)
+	WithEnum(schema, "trace_state", TraceStateSuccess, TraceStateError, TraceStateAll)
+	WithEnum(schema, "query_order", QueryOrderStartTime, QueryOrderDuration)
+	WithEnum(schema, "view", ViewSummary, ViewErrorsOnly, ViewFull)
+	WithDescriptions(schema, map[string]string{
+		"service_id":          "Service ID to filter traces. Use this to find traces from a specific service.",
+		"service_instance_id": "Service instance ID to filter traces. Use this to find traces from a specific instance.",
+		"trace_id":            "Specific trace ID to search for. Use this when you know the exact trace ID.",
+		"endpoint_id":         "Endpoint ID to filter traces. Use this to find traces for a specific endpoint.",
+		"start":               "Start time for the query. Examples: \"2023-01-01 12:00:00\", \"-1h\" (1 hour ago), \"-30m\" (30 minutes ago)",
+		"end": "End time for the query. Examples: \"2023-01-01 13:00:00\", \"now\"," +
+			" \"-10m\" (10 minutes ago) Defaults to current time if omitted.",
+		"step":               "Time step granularity. If not specified, uses adaptive sizing.",
+		"min_trace_duration": "Minimum trace duration in milliseconds. Use this to filter out fast traces.",
+		"max_trace_duration": "Maximum trace duration in milliseconds. Use this to filter out slow traces.",
+		"trace_state": `Filter traces by their state:
+- 'success': Only successful traces
+- 'error': Only traces with errors
+- 'all': All traces (default)`,
+		"query_order": `Sort order for results:
+- 'start_time': Oldest first
+- 'duration': Shortest first`,
+		"view": `Data presentation format:
+- 'full': (Default) Complete raw data for detailed analysis
+- 'summary': Intelligent summary with performance metrics and insights
+- 'errors_only': Focused list of error traces for troubleshooting`,
+		"slow_trace_threshold": "Optional threshold for identifying slow traces in milliseconds. " +
+			"Only when this parameter is set will slow traces be included in the summary. " +
+			"Traces with duration exceeding this threshold will be listed in slow_traces. " +
+			"Examples: 500 (0.5s), 2000 (2s), 5000 (5s)",
+		"tags": `Array of span tags to filter traces. Each tag should have 'key' and 'value' fields.
+Examples: [{"key": "http.method", "value": "POST"}, {"key": "http.status_code", "value": "500"}]`,
+		"cold": "Whether to query from cold-stage storage. Set to true for historical data queries.",
+	})
+
+	return &mcp.Tool{
+		Name: "query_traces",
+		Description: `This tool queries traces from SkyWalking OAP based on various conditions and provides intelligent data processing for LLM analysis.
 
 Workflow:
 1. Use this tool when you need to find traces matching specific criteria
@@ -738,76 +774,7 @@ Examples:
 - {"service_id": "Your_ApplicationName"}: Query with default 1-hour duration
 - {"tags": [{"key": "http.method", "value": "POST"}, {"key": "http.status_code", "value": "500"}], 
 	"start": "-1h", "end": "now"}: Find traces with specific HTTP tags`,
-	searchTraces,
-	mcp.WithTitleAnnotation("Query traces with intelligent analysis"),
-	mcp.WithString("service_id",
-		mcp.Description("Service ID to filter traces. Use this to find traces from a specific service."),
-	),
-	mcp.WithString("service_instance_id",
-		mcp.Description("Service instance ID to filter traces. Use this to find traces from a specific instance."),
-	),
-	mcp.WithString("trace_id",
-		mcp.Description("Specific trace ID to search for. Use this when you know the exact trace ID."),
-	),
-	mcp.WithString("endpoint_id",
-		mcp.Description("Endpoint ID to filter traces. Use this to find traces for a specific endpoint."),
-	),
-	mcp.WithString("start",
-		mcp.Description("Start time for the query. Examples: \"2023-01-01 12:00:00\", \"-1h\" (1 hour ago), \"-30m\" (30 minutes ago)"),
-	),
-	mcp.WithString("end",
-		mcp.Description("End time for the query. Examples: \"2023-01-01 13:00:00\", \"now\","+
-			" \"-10m\" (10 minutes ago) Defaults to current time if omitted."),
-	),
-	mcp.WithString("step",
-		mcp.Enum("SECOND", "MINUTE", "HOUR", "DAY"),
-		mcp.Description("Time step granularity. If not specified, uses adaptive sizing."),
-	),
-	mcp.WithNumber("min_trace_duration",
-		mcp.Description("Minimum trace duration in milliseconds. Use this to filter out fast traces."),
-	),
-	mcp.WithNumber("max_trace_duration",
-		mcp.Description("Maximum trace duration in milliseconds. Use this to filter out slow traces."),
-	),
-	mcp.WithString("trace_state",
-		mcp.Enum(TraceStateSuccess, TraceStateError, TraceStateAll),
-		mcp.Description(`Filter traces by their state:
-- 'success': Only successful traces
-- 'error': Only traces with errors
-- 'all': All traces (default)`),
-	),
-	mcp.WithString("query_order",
-		mcp.Enum(QueryOrderStartTime, QueryOrderDuration),
-		mcp.Description(`Sort order for results:
-- 'start_time': Oldest first
-- 'duration': Shortest first`),
-	),
-	mcp.WithString("view",
-		mcp.Enum(ViewSummary, ViewErrorsOnly, ViewFull),
-		mcp.Description(`Data presentation format:
-- 'full': (Default) Complete raw data for detailed analysis
-- 'summary': Intelligent summary with performance metrics and insights
-- 'errors_only': Focused list of error traces for troubleshooting`),
-	),
-	mcp.WithNumber("slow_trace_threshold",
-		mcp.Description("Optional threshold for identifying slow traces in milliseconds. "+
-			"Only when this parameter is set will slow traces be included in the summary. "+
-			"Traces with duration exceeding this threshold will be listed in slow_traces. "+
-			"Examples: 500 (0.5s), 2000 (2s), 5000 (5s)"),
-	),
-	mcp.WithArray("tags",
-		mcp.Description(`Array of span tags to filter traces. Each tag should have 'key' and 'value' fields.
-Examples: [{"key": "http.method", "value": "POST"}, {"key": "http.status_code", "value": "500"}]`),
-		mcp.Items(map[string]any{
-			"type": "object",
-			"properties": map[string]any{
-				"key":   map[string]any{"type": "string"},
-				"value": map[string]any{"type": "string"},
-			},
-			"required": []string{"key", "value"},
-		}),
-	),
-	mcp.WithBoolean("cold",
-		mcp.Description("Whether to query from cold-stage storage. Set to true for historical data queries."),
-	),
-)
+		InputSchema: schema,
+		Annotations: &mcp.ToolAnnotations{Title: "Query traces with intelligent analysis", IdempotentHint: true},
+	}
+}
diff --git a/internal/tools/trace_test.go b/internal/tools/trace_test.go
index 7beaef3..0ba7146 100644
--- a/internal/tools/trace_test.go
+++ b/internal/tools/trace_test.go
@@ -26,7 +26,7 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/modelcontextprotocol/go-sdk/mcp"
 	api "skywalking.apache.org/repo/goapi/query"
 
 	"github.com/apache/skywalking-cli/pkg/contextkey"
@@ -127,14 +127,11 @@ func TestQueryTracesAutoUsesV1WhenV2Unsupported(t *testing.T) {
 	}
 
 	// The summary is derived from the fetched spans, including the error state.
-	result, err := processTracesResult(&list, ViewSummary, 0)
-	if err != nil {
-		t.Fatalf("processTracesResult returned error: %v", err)
-	}
+	result := processTracesResult(&list, ViewSummary, 0)
 	if result.IsError {
 		t.Fatalf("unexpected tool error: %v", result)
 	}
-	text, ok := result.Content[0].(mcp.TextContent)
+	text, ok := result.Content[0].(*mcp.TextContent)
 	if !ok {
 		t.Fatalf("unexpected content type: %T", result.Content[0])
 	}