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

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

commit 81d2676ab19f8873bb3e0b2d6b4edd6a32544e01
Author: Zixin Zhou <[email protected]>
AuthorDate: Thu Jun 5 00:32:18 2025 +0800

    polish the code
---
 Dockerfile                 | 61 ++++++++++++++++++++++++++++++
 Makefile                   |  8 ++--
 README.md                  | 94 +++++++++++++++++++++++++++++++++++++++++++++-
 cmd/skywalking-mcp/main.go | 10 +++--
 go.mod                     |  2 +-
 go.sum                     | 21 +++++++++++
 internal/config/config.go  | 21 +++++++++++
 internal/swmcp/server.go   | 24 ++++++++++++
 internal/swmcp/sse.go      | 88 ++++++++++++++++++++++++++++++++++++++++---
 internal/swmcp/stdio.go    | 71 +++++++++++++++++++++++-----------
 10 files changed, 365 insertions(+), 35 deletions(-)

diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..3e67b72
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,61 @@
+# 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.
+
+# Build stage
+FROM golang:1.24-bullseye AS builder
+
+# Default version
+ARG VERSION="dev"
+
+# Set the working directory
+WORKDIR /app
+
+# Copy go.mod and go.sum files
+COPY go.mod go.sum ./
+
+# Go get dependencies
+RUN go mod tidy
+
+# Copy the source code
+COPY . .
+
+# Build the application
+RUN CGO_ENABLED=0 go build \
+    -ldflags="-s -w -X main.version=${VERSION} -X main.commit=$(git rev-parse 
HEAD) -X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
+    -o bin/swmcp ./cmd/skywalking-mcp/main.go
+
+# Make a stage to run the app
+FROM debian:bullseye-slim
+
+# Install ca-certificates for HTTPS requests
+RUN apt-get update && apt-get install -y ca-certificates && rm -rf 
/var/lib/apt/lists/*
+
+# Create a non-root user
+RUN useradd -r -u 1000 -m skywalking-mcp
+
+# Set the working directory
+WORKDIR /app
+
+# Copy the binary from the builder stage
+COPY --from=builder --chown=1000:1000 /app/bin/swmcp /app/
+
+# Use the non-root user
+USER skywalking-mcp
+
+# Expose the port the app runs on
+EXPOSE 8000
+
+# Run the application, defaulting to SSE transport
+ENTRYPOINT ["/app/swmcp", "stdio"]
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 269e3ad..5892dc8 100644
--- a/Makefile
+++ b/Makefile
@@ -16,7 +16,6 @@
 #
 
 VERSION_PATH=main
-GIT_VERSION=$(shell git describe --tags --abbrev=0)-SNAPSHOT-$(shell git 
rev-parse --short HEAD)
 GIT_COMMIT=$(shell git rev-parse HEAD)
 BUILD_DATE=$(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
 MKDIR_P = mkdir -p
@@ -28,14 +27,17 @@ LICENSE_EYE = license-eye
 all: build ;
 
 .PHONY: build
-build:
+build: ## Build the binary.
        ${MKDIR_P} bin/
        CGO_ENABLED=0 go build -ldflags "\
-               -X ${VERSION_PATH}.version=${GIT_VERSION} \
                -X ${VERSION_PATH}.commit=${GIT_COMMIT} \
                -X ${VERSION_PATH}.date=${BUILD_DATE}" \
                -o bin/swmcp cmd/skywalking-mcp/main.go
 
+.PHONY: build-image
+build-image: ## Build the Docker image.
+       docker build -t skywalking-mcp:latest .
+
 $(GO_LINT):
        @$(GO_LINT) version > /dev/null 2>&1 || go install 
github.com/golangci/golangci-lint/cmd/[email protected]
 $(LICENSE_EYE):
diff --git a/README.md b/README.md
index bd82a70..3c73c89 100644
--- a/README.md
+++ b/README.md
@@ -3,11 +3,99 @@ Apache SkyWalking MCP
 
 <img src="http://skywalking.apache.org/assets/logo.svg"; alt="Sky Walking logo" 
height="90px" align="right" />
 
-**SkyWalking-MCP**: Apache SkyWalking MCP Server.
+**SkyWalking-MCP**: A [Model Context Protocol][mcp] (MCP) server for 
integrating AI agents with Skywalking OAP and the surrounding ecosystem.
 
 **SkyWalking**: an APM(application performance monitor) system, especially 
designed for
 microservices, cloud native and container-based (Docker, Kubernetes, Mesos) 
architectures.
 
+## Usage
+
+### From Source
+
+```bash
+# Clone the repository
+git clone https://github.com/apache/skywalking-mcp.git
+cd skywalking-mcp && go mod tidy
+
+# Build the project
+make
+```
+
+### Command-line Options
+
+```bash
+Usage:
+  swmcp [command]
+
+Available Commands:
+  completion  Generate the autocompletion script for the specified shell
+  help        Help about any command
+  sse         Start SSE server
+  stdio       Start stdio server
+
+Flags:
+  -h, --help               help for swmcp
+      --log-command        When true, log commands to the log file
+      --log-file string    Path to log file
+      --log-level string   Logging level (debug, info, warn, error) (default 
"info")
+      --read-only          Restrict the server to read-only operations
+      --sw-url string      Specify the OAP URL to connect to (e.g. 
http://localhost:12800)
+  -v, --version            version for swmcp
+
+Use "swmcp [command] --help" for more information about a command.
+```
+
+You could start the MCP server with the following command:
+
+```bash
+# use stdio server
+bin/swmcp stdio --sw-url http://localhost:12800
+
+# or use SSE server
+bin/swmcp sse --sse-address localhost:8000 --base-path /mcp --sw-url 
http://localhost:12800
+```
+
+### Usage with Cursor
+
+```json
+{
+  "mcpServers": {
+    "skywalking": {
+      "command": "swmcp stdio",
+      "args": [
+        "--sw-url",
+        "http://localhost:12800";
+      ]
+    }
+  }
+}
+```
+
+If using Docker:
+
+`make build-image` to build the Docker image, then configure the MCP server 
like this:
+
+```json
+{
+  "mcpServers": {
+    "skywalking": {
+      "command": "docker",
+      "args": [
+        "run",
+        "--rm",
+        "-i",
+        "-e",
+        "SW_URL",
+        "skywalking-mcp:latest"
+      ],
+      "env": {
+        "SW_URL": "http://localhost:12800";
+      }
+    }
+  }
+}
+```
+
 ## Contact Us
 * Submit [an issue](https://github.com/apache/skywalking/issues/new) by using 
[MCP] as title prefix.
 * Mail list: **[email protected]**. Mail to 
`[email protected]`, follow the reply to subscribe the mail 
list.
@@ -15,4 +103,6 @@ microservices, cloud native and container-based (Docker, 
Kubernetes, Mesos) arch
 * Twitter, [ASFSkyWalking](https://twitter.com/ASFSkyWalking)
 
 ## License
-[Apache 2.0 License.](/LICENSE)
\ No newline at end of file
+[Apache 2.0 License.](/LICENSE)
+
+[mcp]: https://modelcontextprotocol.io/
\ No newline at end of file
diff --git a/cmd/skywalking-mcp/main.go b/cmd/skywalking-mcp/main.go
index a4c0f22..0504f34 100644
--- a/cmd/skywalking-mcp/main.go
+++ b/cmd/skywalking-mcp/main.go
@@ -18,10 +18,13 @@
 package main
 
 import (
+       "context"
        "fmt"
        "log/slog"
        "os"
+       "os/signal"
        "strings"
+       "syscall"
 
        "github.com/spf13/cobra"
        "github.com/spf13/viper"
@@ -35,7 +38,7 @@ var (
        date    = "date"
 
        rootCmd = &cobra.Command{
-               Use:     "server",
+               Use:     "swmcp",
                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),
@@ -54,7 +57,6 @@ func init() {
 
        // Add global Flags
        rootCmd.PersistentFlags().String("sw-url", "", "Specify the OAP URL to 
connect to (e.g. http://localhost:12800)")
-       rootCmd.PersistentFlags().String("sse-addr", "localhost:8000", "Which 
address to listen on for SSE transport")
        rootCmd.PersistentFlags().String("log-level", "info", "Logging level 
(debug, info, warn, error)")
        rootCmd.PersistentFlags().Bool("read-only", false, "Restrict the server 
to read-only operations")
        rootCmd.PersistentFlags().Bool("log-command", false, "When true, log 
commands to the log file")
@@ -62,7 +64,6 @@ func init() {
 
        // Bind flag to viper
        _ = viper.BindPFlag("url", rootCmd.PersistentFlags().Lookup("sw-url"))
-       _ = viper.BindPFlag("sse-addr", 
rootCmd.PersistentFlags().Lookup("sse-addr"))
        _ = viper.BindPFlag("log-level", 
rootCmd.PersistentFlags().Lookup("log-level"))
        _ = viper.BindPFlag("read-only", 
rootCmd.PersistentFlags().Lookup("read-only"))
        _ = viper.BindPFlag("log-command", 
rootCmd.PersistentFlags().Lookup("log-command"))
@@ -72,6 +73,9 @@ func init() {
                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/go.mod b/go.mod
index 6682c1c..3f0da89 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/apache/skywalking-mcp
 go 1.24.3
 
 require (
-       github.com/apache/skywalking-cli v0.0.0-20250520051046-7bbac156e74e
+       github.com/apache/skywalking-cli v0.0.0-20250604010708-77b4c49e89c9
        github.com/mark3labs/mcp-go v0.31.0
        github.com/sirupsen/logrus v1.9.3
        github.com/spf13/cobra v1.9.1
diff --git a/go.sum b/go.sum
index f0e868f..f99c7a9 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,11 @@
+github.com/BurntSushi/toml v0.3.1/go.mod 
h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/apache/skywalking-cli v0.0.0-20250520051046-7bbac156e74e 
h1:JRsFyYSDXtI1wCy1POoXSw4jpIhLFHp9Lqp+xFz7+6g=
 github.com/apache/skywalking-cli v0.0.0-20250520051046-7bbac156e74e/go.mod 
h1:5S2bH5p65WLXmFufyX5JM1btkx6ail8wqdSkjy0hb3I=
+github.com/apache/skywalking-cli v0.0.0-20250604010708-77b4c49e89c9 
h1:loGzKlrRMY5r4qoCFA+cWSEvCP4hj1dnf+BzrGSDKQE=
+github.com/apache/skywalking-cli v0.0.0-20250604010708-77b4c49e89c9/go.mod 
h1:5S2bH5p65WLXmFufyX5JM1btkx6ail8wqdSkjy0hb3I=
+github.com/apache/skywalking-cli v0.9.0 
h1:qPLZqVii9rvS4TDKeWeqh80z1ZKpCdIiWEXFjlqEw2I=
+github.com/apache/skywalking-cli v0.9.0/go.mod 
h1:daosZFeQYSvZlF43IQiIrWfU0nhD1WOu8YVnT5eudOU=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod 
h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod 
h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/davecgh/go-spew v1.1.0/go.mod 
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod 
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -17,6 +23,7 @@ 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/inconshreveable/mousetrap v1.1.0 
h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod 
h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod 
h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 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=
@@ -27,17 +34,26 @@ github.com/mark3labs/mcp-go v0.31.0 
h1:4UxSV8aM770OPmTvaVe/b1rA2oZAjBMhGBfUgOGut
 github.com/mark3labs/mcp-go v0.31.0/go.mod 
h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
 github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
 github.com/matryer/is v1.4.0/go.mod 
h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
+github.com/mattn/go-runewidth v0.0.4/go.mod 
h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mum4k/termdash v0.10.0/go.mod 
h1:l3tO+lJi9LZqXRq7cu7h5/8rDIK3AzelSuq2v/KncxI=
+github.com/nsf/termbox-go v0.0.0-20190817171036-93860e161317/go.mod 
h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
+github.com/olekukonko/tablewriter v0.0.1/go.mod 
h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/olekukonko/tablewriter v0.0.2/go.mod 
h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ=
 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.8.1/go.mod 
h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod 
h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 
h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod 
h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rogpeppe/go-internal v1.9.0 
h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
 github.com/rogpeppe/go-internal v1.9.0/go.mod 
h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod 
h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod 
h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sagikazarmark/locafero v0.9.0 
h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
 github.com/sagikazarmark/locafero v0.9.0/go.mod 
h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod 
h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.4.2/go.mod 
h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.9.3 
h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 github.com/sirupsen/logrus v1.9.3/go.mod 
h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/sourcegraph/conc v0.3.0 
h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
@@ -53,15 +69,19 @@ github.com/spf13/pflag v1.0.6/go.mod 
h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
 github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
 github.com/spf13/viper v1.20.1/go.mod 
h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
 github.com/stretchr/objx v0.1.0/go.mod 
h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod 
h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod 
h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.7.0/go.mod 
h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.10.0 
h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod 
h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.6.0 
h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0/go.mod 
h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+github.com/urfave/cli v1.22.1/go.mod 
h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 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.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
 go.uber.org/multierr v1.11.0/go.mod 
h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod 
h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
 golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
@@ -70,6 +90,7 @@ golang.org/x/text v0.25.0/go.mod 
h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
 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=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod 
h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/config/config.go b/internal/config/config.go
index 0a4fe2c..81a0f57 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -47,3 +47,24 @@ type StdioServerConfig struct {
        // LogCommands indicates if we should log commands
        LogCommands bool
 }
+
+// SSEServerConfig holds the configuration for Stdio.
+type SSEServerConfig struct {
+       // SkyWalking OAP URL to target for API requests (e.g. localhost:12800)
+       URL string
+
+       // ReadOnly indicates if we should only offer read-only tools
+       ReadOnly bool
+
+       // Path to the log file if not stderr
+       LogFilePath string
+
+       // LogCommands indicates if we should log commands
+       LogCommands bool
+
+       // The host and port to start the sse server on
+       Address string
+
+       // Base path for the sse server
+       BasePath string
+}
diff --git a/internal/swmcp/server.go b/internal/swmcp/server.go
index ebdbaa0..880ce19 100644
--- a/internal/swmcp/server.go
+++ b/internal/swmcp/server.go
@@ -18,11 +18,17 @@
 package swmcp
 
 import (
+       "fmt"
+       "os"
+
        "github.com/apache/skywalking-mcp/internal/tools"
+       "github.com/sirupsen/logrus"
 
        "github.com/mark3labs/mcp-go/server"
 )
 
+// newMcpServer creates a new MCP server instance,
+// and we can add various tools and capabilities to it.
 func newMcpServer() *server.MCPServer {
        mcpServer := server.NewMCPServer(
                "skywalking-mcp",
@@ -34,3 +40,21 @@ func newMcpServer() *server.MCPServer {
 
        return mcpServer
 }
+
+func initLogger(logFilePath string) (*logrus.Logger, error) {
+       if logFilePath == "" {
+               return logrus.New(), nil
+       }
+
+       file, err := os.OpenFile(logFilePath, 
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
+       if err != nil {
+               return nil, fmt.Errorf("failed to open log file: %w", err)
+       }
+
+       logrusLogger := logrus.New()
+       logrusLogger.SetFormatter(&logrus.TextFormatter{})
+       logrusLogger.SetLevel(logrus.DebugLevel)
+       logrusLogger.SetOutput(file)
+
+       return logrusLogger, nil
+}
diff --git a/internal/swmcp/sse.go b/internal/swmcp/sse.go
index 6a63b77..040370d 100644
--- a/internal/swmcp/sse.go
+++ b/internal/swmcp/sse.go
@@ -18,27 +18,105 @@
 package swmcp
 
 import (
+       "context"
+       "errors"
        "fmt"
+       "log"
+       "net/http"
        "os"
+       "os/signal"
+       "syscall"
+       "time"
 
+       "github.com/apache/skywalking-mcp/internal/config"
        "github.com/mark3labs/mcp-go/server"
        "github.com/spf13/cobra"
+       "github.com/spf13/viper"
 )
 
 func NewSSEServer() *cobra.Command {
-       return &cobra.Command{
+       sseCmd := &cobra.Command{
                Use:   "sse",
                Short: "Start SSE server",
                Long:  `Start a server that listens for Server-Sent Events 
(SSE) on the specified address.`,
-               Run: func(_ *cobra.Command, _ []string) {
-                       if err := runSSEServer(); err != nil {
-                               _, _ = fmt.Fprintf(os.Stderr, "failed to run 
sse server: %v\n", err)
+               RunE: func(cmd *cobra.Command, _ []string) error {
+                       sseServerConfig := config.SSEServerConfig{
+                               Address:  viper.GetString("sse-address"),
+                               BasePath: viper.GetString("base-path"),
                        }
+
+                       return runSSEServer(context.Background(), 
sseServerConfig)
                },
        }
+
+       // Add SSE server specific flags
+       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")
+       _ = viper.BindPFlag("sse-address", sseCmd.Flags().Lookup("sse-address"))
+       _ = viper.BindPFlag("base-path", sseCmd.Flags().Lookup("base-path"))
+
+       return sseCmd
 }
 
 // runSSEServer starts a server that listens for Server-Sent Events (SSE) on 
the specified address.
-func runSSEServer() *server.SSEServer {
+func runSSEServer(ctx context.Context, cfg config.SSEServerConfig) error {
+       ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
+       defer stop()
+
+       logger, err := initLogger(cfg.LogFilePath)
+       if err != nil {
+               log.Fatal("Failed to initialize logger:", err)
+       }
+
+       sseServer := server.NewSSEServer(
+               newMcpServer(),
+               server.WithStaticBasePath(cfg.BasePath),
+               server.WithSSEContextFunc(EnhanceHTTPContextFunc()),
+       )
+       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) {
+                       errCh <- err // bubble up real crashes
+               }
+       }()
+
+       // Give the server a moment to start
+       time.Sleep(100 * time.Millisecond)
+
+       // Block until Ctrl-C or an internal error
+       select {
+       case <-ctx.Done():
+               // user hit Ctrl-C
+               _, _ = fmt.Fprintln(os.Stderr, "Received shutdown signal, 
stopping server...")
+       case err := <-errCh:
+               // HTTP server crashed
+               return fmt.Errorf("sse server error: %w", err)
+       }
+
+       // Graceful shutdown
+       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)
+               }
+       }
+
+       // 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/stdio.go b/internal/swmcp/stdio.go
index c513c64..6ae4fcd 100644
--- a/internal/swmcp/stdio.go
+++ b/internal/swmcp/stdio.go
@@ -24,6 +24,7 @@ import (
        "io"
        "log"
        "log/slog"
+       "net/http"
        "os"
        "os/signal"
        "strings"
@@ -31,7 +32,6 @@ import (
 
        "github.com/apache/skywalking-cli/pkg/contextkey"
        "github.com/mark3labs/mcp-go/server"
-       "github.com/sirupsen/logrus"
        "github.com/spf13/cobra"
        "github.com/spf13/viper"
 
@@ -66,22 +66,17 @@ func NewStdioServer() *cobra.Command {
 func runStdioServer(ctx context.Context, cfg config.StdioServerConfig) error {
        slog.Info("Start a server that communicates via standard input/output 
streams using JSON-RPC messages.")
        // Handle SIGINT and SIGTERM
-       _, stop := signal.NotifyContext(context.Background(), os.Interrupt, 
syscall.SIGTERM)
+       ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
        defer stop()
 
        stdioServer := server.NewStdioServer(newMcpServer())
 
-       logrusLogger := logrus.New()
-       if cfg.LogFilePath != "" {
-               file, err := os.OpenFile(cfg.LogFilePath, 
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
-               if err != nil {
-                       return fmt.Errorf("failed to open log file: %w", err)
-               }
-
-               logrusLogger.SetLevel(logrus.DebugLevel)
-               logrusLogger.SetOutput(file)
+       logger, err := initLogger(cfg.LogFilePath)
+       if err != nil {
+               log.Fatal("Failed to initialize logger:", err)
        }
-       stdLogger := log.New(logrusLogger.Writer(), "swmcp-stdioserver", 0)
+
+       stdLogger := log.New(logger.Writer(), "swmcp-stdio-server", 0)
        stdioServer.SetErrorLogger(stdLogger)
        stdioServer.SetContextFunc(EnhanceStdioContextFunc())
 
@@ -91,7 +86,7 @@ func runStdioServer(ctx context.Context, cfg 
config.StdioServerConfig) error {
                in, out := io.Reader(os.Stdin), io.Writer(os.Stdout)
 
                if cfg.LogCommands {
-                       loggedIO := tools.NewIOLogger(in, out, logrusLogger)
+                       loggedIO := tools.NewIOLogger(in, out, logger)
                        in, out = loggedIO, loggedIO
                }
 
@@ -99,12 +94,12 @@ func runStdioServer(ctx context.Context, cfg 
config.StdioServerConfig) error {
        }()
 
        // Output github-mcp-server string
-       _, _ = fmt.Fprintf(os.Stderr, "GitHub MCP Server running on stdio\n")
+       _, _ = fmt.Fprintf(os.Stderr, "SkyWalking MCP Server running on 
stdio\n")
 
        // Wait for shutdown signal
        select {
        case <-ctx.Done():
-               logrusLogger.Infof("shutting down server...")
+               logger.Infof("shutting down server...")
        case err := <-errC:
                if err != nil {
                        return fmt.Errorf("error running server: %w", err)
@@ -114,16 +109,33 @@ func runStdioServer(ctx context.Context, cfg 
config.StdioServerConfig) error {
        return nil
 }
 
-var ExtractSWInfoFromCfg server.StdioContextFunc = func(ctx context.Context) 
context.Context {
+var ExtractSWURLFromCfg server.StdioContextFunc = func(ctx context.Context) 
context.Context {
        urlStr := viper.GetString("url")
        if urlStr == "" {
                urlStr = config.DefaultSWURL
        }
 
+       // we need to ensure the URL ends with "/graphql"
        if !strings.HasSuffix(urlStr, "/graphql") {
                urlStr = strings.TrimRight(urlStr, "/") + "/graphql"
        }
-       return WithSkyWalkingURL(ctx, urlStr)
+       return WithSkyWalkingURLAndInsecure(ctx, urlStr)
+}
+
+var ExtractSWURLFromHeaders server.SSEContextFunc = func(ctx context.Context, 
req *http.Request) context.Context {
+       urlStr := req.Header.Get("SW-URL")
+       if urlStr == "" {
+               urlStr = viper.GetString("url")
+               if urlStr == "" {
+                       urlStr = config.DefaultSWURL
+               }
+       }
+
+       // we need to ensure the URL ends with "/graphql"
+       if !strings.HasSuffix(urlStr, "/graphql") {
+               urlStr = strings.TrimRight(urlStr, "/") + "/graphql"
+       }
+       return WithSkyWalkingURLAndInsecure(ctx, urlStr)
 }
 
 func EnhanceStdioContextFuncs(funcs ...server.StdioContextFunc) 
server.StdioContextFunc {
@@ -135,11 +147,28 @@ func EnhanceStdioContextFuncs(funcs 
...server.StdioContextFunc) server.StdioCont
        }
 }
 
-// WithSkyWalkingURL adds the SkyWalking URL to the context.
-func WithSkyWalkingURL(ctx context.Context, url string) context.Context {
-       return context.WithValue(ctx, contextkey.BaseURL{}, url)
+func EnhanceSSEContextFuncs(funcs ...server.SSEContextFunc) 
server.SSEContextFunc {
+       return func(ctx context.Context, r *http.Request) context.Context {
+               for _, f := range funcs {
+                       ctx = f(ctx, r)
+               }
+               return ctx
+       }
+}
+
+// WithSkyWalkingURLAndInsecure adds the SkyWalking URL and Insecure to the 
context.
+func WithSkyWalkingURLAndInsecure(ctx context.Context, url string) 
context.Context {
+       ctx = context.WithValue(ctx, contextkey.BaseURL{}, url)
+       ctx = context.WithValue(ctx, contextkey.Insecure{}, false)
+       return ctx
 }
 
+// EnhanceStdioContextFunc returns a StdioContextFunc that composes all the 
provided StdioContextFuncs.
 func EnhanceStdioContextFunc() server.StdioContextFunc {
-       return EnhanceStdioContextFuncs(ExtractSWInfoFromCfg)
+       return EnhanceStdioContextFuncs(ExtractSWURLFromCfg)
+}
+
+// EnhanceHTTPContextFunc returns a SSEContextFunc that composes all the 
provided HTTPContextFuncs.
+func EnhanceHTTPContextFunc() server.SSEContextFunc {
+       return EnhanceSSEContextFuncs(ExtractSWURLFromHeaders)
 }

Reply via email to