This is an automated email from the ASF dual-hosted git repository.
qiuxiafan 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 586f684 feat: add session-based SkyWalking URL and basic auth support
(#29)
586f684 is described below
commit 586f684aa0642f4b9850d930b2a2c4a368c98c36
Author: 吴晟 Wu Sheng <[email protected]>
AuthorDate: Fri Mar 13 20:12:16 2026 +0800
feat: add session-based SkyWalking URL and basic auth support (#29)
---
CLAUDE.md | 6 +-
README.md | 28 +++++++---
cmd/skywalking-mcp/main.go | 4 ++
internal/swmcp/server.go | 98 ++++++++++++++++++++++++++------
internal/swmcp/session.go | 136 +++++++++++++++++++++++++++++++++++++++++++++
internal/swmcp/stdio.go | 8 +--
internal/tools/mqe.go | 35 +++++++++---
7 files changed, 271 insertions(+), 44 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 52625b5..483758e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -41,9 +41,11 @@ No unit tests exist yet. CI runs license checks, lint, and
docker build.
Three MCP transport modes as cobra subcommands: `stdio`, `sse`, `streamable`.
The SkyWalking OAP URL is resolved in priority order:
-`--sw-url` flag > `SW_URL` env > `SW-URL` HTTP header >
`http://localhost:12800/graphql`
+`set_skywalking_url` session tool > `--sw-url` flag > `SW-URL` HTTP header >
`http://localhost:12800/graphql`
-Each transport injects the OAP URL into the request context via
`WithSkyWalkingURLAndInsecure()`. Tools extract it downstream using
`skywalking-cli`'s `contextkey.BaseURL{}`.
+Basic auth is configured via `--sw-username` / `--sw-password` flags. Both
flags (and the `set_skywalking_url` tool) support `${ENV_VAR}` syntax to
resolve credentials from environment variables (e.g. `--sw-password
${MY_SECRET}`).
+
+Each transport injects the OAP URL and auth into the request context via
`WithSkyWalkingURLAndInsecure()` and `WithSkyWalkingAuth()`. Tools extract them
downstream using `skywalking-cli`'s `contextkey.BaseURL{}`,
`contextkey.Username{}`, and `contextkey.Password{}`.
### Server Wiring (`internal/swmcp/server.go`)
diff --git a/README.md b/README.md
index 908331b..75701ef 100644
--- a/README.md
+++ b/README.md
@@ -36,13 +36,15 @@ Available Commands:
streamable Start Streamable 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
+ -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)
+ --sw-username string Username for basic auth to SkyWalking OAP
(supports ${ENV_VAR} syntax)
+ --sw-password string Password for basic auth to SkyWalking OAP
(supports ${ENV_VAR} syntax)
+ -v, --version version for swmcp
Use "swmcp [command] --help" for more information about a command.
```
@@ -53,6 +55,12 @@ You could start the MCP server with the following command:
# use stdio server
bin/swmcp stdio --sw-url http://localhost:12800
+# with basic auth (raw password)
+bin/swmcp stdio --sw-url http://localhost:12800 --sw-username admin
--sw-password admin
+
+# with basic auth (password from environment variable)
+bin/swmcp stdio --sw-url http://localhost:12800 --sw-username admin
--sw-password '${SW_PASSWORD}'
+
# or use SSE server
bin/swmcp sse --sse-address localhost:8000 --base-path /mcp --sw-url
http://localhost:12800
```
@@ -65,8 +73,9 @@ bin/swmcp sse --sse-address localhost:8000 --base-path /mcp
--sw-url http://loca
"skywalking": {
"command": "swmcp stdio",
"args": [
- "--sw-url",
- "http://localhost:12800"
+ "--sw-url", "http://localhost:12800",
+ "--sw-username", "admin",
+ "--sw-password", "${SW_PASSWORD}"
]
}
}
@@ -101,6 +110,7 @@ SkyWalking MCP provides the following tools to query and
analyze SkyWalking OAP
| Category | Tool Name | Description
|
|--------------|--------------------------------|---------------------------------------------------------------------------------------------------|
+| **Session** | `set_skywalking_url` | Set the SkyWalking OAP
server URL and optional basic auth credentials for the current session.
Supports `${ENV_VAR}` syntax for credentials. |
| **Trace** | `query_traces` | Query traces with
multi-condition filtering (service, endpoint, state, tags, and time range via
start/end/step). Supports `full`, `summary`, and `errors_only` views with
performance insights. |
| **Log** | `query_logs` | Query logs with filters for
service, instance, endpoint, trace ID, tags, and time range. Supports cold
storage and pagination. |
| **MQE** | `execute_mqe_expression` | Execute MQE (Metrics Query
Expression) to query and calculate metrics data. Supports calculations,
aggregations, TopN, trend analysis, and multiple result types. |
diff --git a/cmd/skywalking-mcp/main.go b/cmd/skywalking-mcp/main.go
index 8ad4014..66babdc 100644
--- a/cmd/skywalking-mcp/main.go
+++ b/cmd/skywalking-mcp/main.go
@@ -57,6 +57,8 @@ 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("sw-username", "", "Username for basic
auth to SkyWalking OAP (supports ${ENV_VAR} syntax)")
+ rootCmd.PersistentFlags().String("sw-password", "", "Password for basic
auth to SkyWalking OAP (supports ${ENV_VAR} syntax)")
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")
@@ -64,6 +66,8 @@ func init() {
// Bind flag to viper
_ = viper.BindPFlag("url", rootCmd.PersistentFlags().Lookup("sw-url"))
+ _ = viper.BindPFlag("username",
rootCmd.PersistentFlags().Lookup("sw-username"))
+ _ = viper.BindPFlag("password",
rootCmd.PersistentFlags().Lookup("sw-password"))
_ = 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"))
diff --git a/internal/swmcp/server.go b/internal/swmcp/server.go
index 21edfbc..6da1bf8 100644
--- a/internal/swmcp/server.go
+++ b/internal/swmcp/server.go
@@ -22,6 +22,7 @@ import (
"fmt"
"net/http"
"os"
+ "strings"
"github.com/mark3labs/mcp-go/server"
"github.com/sirupsen/logrus"
@@ -43,6 +44,7 @@ func newMCPServer() *server.MCPServer {
server.WithPromptCapabilities(true),
server.WithLogging(),
)
+ AddSessionTools(s)
tools.AddTraceTools(s)
tools.AddLogTools(s)
tools.AddMQETools(s)
@@ -73,14 +75,21 @@ func initLogger(logFilePath string) (*logrus.Logger, error)
{
return logrusLogger, nil
}
-// WithSkyWalkingURLAndInsecure adds SkyWalking URL and insecure flag to the
context
-// This ensures all downstream requests will have contextkey.BaseURL{} and
contextkey.Insecure{}
+// WithSkyWalkingURLAndInsecure adds SkyWalking URL and insecure flag to the
context.
+// This ensures all downstream requests will have contextkey.BaseURL{} and
contextkey.Insecure{}.
func WithSkyWalkingURLAndInsecure(ctx context.Context, url string, insecure
bool) context.Context {
ctx = context.WithValue(ctx, contextkey.BaseURL{}, url)
ctx = context.WithValue(ctx, contextkey.Insecure{}, insecure)
return ctx
}
+// WithSkyWalkingAuth adds username and password to the context for basic auth.
+func WithSkyWalkingAuth(ctx context.Context, username, password string)
context.Context {
+ ctx = context.WithValue(ctx, contextkey.Username{}, username)
+ ctx = context.WithValue(ctx, contextkey.Password{}, password)
+ return ctx
+}
+
// configuredSkyWalkingURL returns the configured SkyWalking OAP URL.
// The value is sourced from the CLI/config binding for `--sw-url`,
// falling back to the built-in default when unset.
@@ -92,6 +101,32 @@ func configuredSkyWalkingURL() string {
return tools.FinalizeURL(urlStr)
}
+// 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.
+func resolveEnvVar(value string) string {
+ trimmed := strings.TrimSpace(value)
+ if strings.HasPrefix(trimmed, "${") && strings.HasSuffix(trimmed, "}") {
+ envName := trimmed[2 : len(trimmed)-1]
+ return os.Getenv(envName)
+ }
+ return value
+}
+
+// configuredAuth returns the configured username and password from CLI flags
or env vars.
+func configuredAuth() (username, password string) {
+ return resolveEnvVar(viper.GetString("username")),
resolveEnvVar(viper.GetString("password"))
+}
+
+// withConfiguredAuth injects the configured auth credentials into the context
if present.
+func withConfiguredAuth(ctx context.Context) context.Context {
+ username, password := configuredAuth()
+ if username != "" {
+ ctx = WithSkyWalkingAuth(ctx, username, password)
+ }
+ return ctx
+}
+
// urlFromHeaders extracts URL for a request.
// URL is sourced from Header > configured value > Default.
func urlFromHeaders(req *http.Request) string {
@@ -103,32 +138,59 @@ func urlFromHeaders(req *http.Request) string {
return tools.FinalizeURL(urlStr)
}
-// WithSkyWalkingContextFromConfig injects the SkyWalking URL and insecure
-// settings from global configuration into the context.
-var WithSkyWalkingContextFromConfig server.StdioContextFunc = func(ctx
context.Context) context.Context {
- return WithSkyWalkingURLAndInsecure(ctx, configuredSkyWalkingURL(),
false)
-}
-
-// withSkyWalkingContextFromRequest is the shared logic for enriching context
from an http.Request.
-func withSkyWalkingContextFromRequest(ctx context.Context, req *http.Request)
context.Context {
- urlStr := urlFromHeaders(req)
- return WithSkyWalkingURLAndInsecure(ctx, urlStr, false)
+// applySessionOverrides checks for a session in the context and applies any
+// URL or auth overrides that were set via the set_skywalking_url tool.
+func applySessionOverrides(ctx context.Context) context.Context {
+ session := SessionFromContext(ctx)
+ if session == nil {
+ return ctx
+ }
+ if url := session.URL(); url != "" {
+ ctx = context.WithValue(ctx, contextkey.BaseURL{}, url)
+ }
+ if username := session.Username(); username != "" {
+ ctx = WithSkyWalkingAuth(ctx, username, session.Password())
+ }
+ return ctx
}
// EnhanceStdioContextFunc returns a StdioContextFunc that enriches the context
-// with SkyWalking settings from the global configuration.
+// with SkyWalking settings from the global configuration and a per-session
store.
func EnhanceStdioContextFunc() server.StdioContextFunc {
- return WithSkyWalkingContextFromConfig
+ session := &Session{}
+ return func(ctx context.Context) context.Context {
+ ctx = WithSession(ctx, session)
+ ctx = WithSkyWalkingURLAndInsecure(ctx,
configuredSkyWalkingURL(), false)
+ ctx = withConfiguredAuth(ctx)
+ ctx = applySessionOverrides(ctx)
+ return ctx
+ }
}
// EnhanceSSEContextFunc returns a SSEContextFunc that enriches the context
-// with SkyWalking settings from SSE request headers.
+// with SkyWalking settings from SSE request headers and a per-session store.
func EnhanceSSEContextFunc() server.SSEContextFunc {
- return withSkyWalkingContextFromRequest
+ session := &Session{}
+ return func(ctx context.Context, req *http.Request) context.Context {
+ ctx = WithSession(ctx, session)
+ urlStr := urlFromHeaders(req)
+ ctx = WithSkyWalkingURLAndInsecure(ctx, urlStr, false)
+ ctx = withConfiguredAuth(ctx)
+ ctx = applySessionOverrides(ctx)
+ return ctx
+ }
}
// EnhanceHTTPContextFunc returns a HTTPContextFunc that enriches the context
-// with SkyWalking settings from HTTP request headers.
+// with SkyWalking settings from HTTP request headers and a per-session store.
func EnhanceHTTPContextFunc() server.HTTPContextFunc {
- return withSkyWalkingContextFromRequest
+ session := &Session{}
+ return func(ctx context.Context, req *http.Request) context.Context {
+ ctx = WithSession(ctx, session)
+ urlStr := urlFromHeaders(req)
+ ctx = WithSkyWalkingURLAndInsecure(ctx, urlStr, false)
+ ctx = withConfiguredAuth(ctx)
+ ctx = applySessionOverrides(ctx)
+ return ctx
+ }
}
diff --git a/internal/swmcp/session.go b/internal/swmcp/session.go
new file mode 100644
index 0000000..847504a
--- /dev/null
+++ b/internal/swmcp/session.go
@@ -0,0 +1,136 @@
+// 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 (
+ "context"
+ "fmt"
+ "sync"
+
+ "github.com/mark3labs/mcp-go/mcp"
+ "github.com/mark3labs/mcp-go/server"
+
+ "github.com/apache/skywalking-mcp/internal/tools"
+)
+
+// sessionKey is the context key for looking up the session store.
+type sessionKey struct{}
+
+// Session holds per-session SkyWalking connection configuration.
+type Session struct {
+ mu sync.RWMutex
+ url string
+ username string
+ password string
+}
+
+// SetConnection updates the session's connection parameters.
+func (s *Session) SetConnection(url, username, password string) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.url = url
+ s.username = username
+ s.password = password
+}
+
+// URL returns the session's configured URL, or empty if not set.
+func (s *Session) URL() string {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ return s.url
+}
+
+// Username returns the session's configured username.
+func (s *Session) Username() string {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ return s.username
+}
+
+// Password returns the session's configured password.
+func (s *Session) Password() string {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ return s.password
+}
+
+// SessionFromContext retrieves the session from the context, or nil if not
present.
+func SessionFromContext(ctx context.Context) *Session {
+ s, _ := ctx.Value(sessionKey{}).(*Session)
+ return s
+}
+
+// WithSession attaches a session to the context.
+func WithSession(ctx context.Context, s *Session) context.Context {
+ return context.WithValue(ctx, sessionKey{}, s)
+}
+
+// SetSkyWalkingURLRequest represents the request for the set_skywalking_url
tool.
+type SetSkyWalkingURLRequest struct {
+ URL string `json:"url"`
+ Username string `json:"username,omitempty"`
+ Password string `json:"password,omitempty"`
+}
+
+func setSkyWalkingURL(ctx context.Context, req *SetSkyWalkingURLRequest)
(*mcp.CallToolResult, error) {
+ if req.URL == "" {
+ return mcp.NewToolResultError("url is required"), nil
+ }
+
+ session := SessionFromContext(ctx)
+ if session == nil {
+ return mcp.NewToolResultError("session not available"), nil
+ }
+
+ finalURL := tools.FinalizeURL(req.URL)
+ session.SetConnection(finalURL, resolveEnvVar(req.Username),
resolveEnvVar(req.Password))
+
+ msg := fmt.Sprintf("SkyWalking URL set to %s", finalURL)
+ if req.Username != "" {
+ msg += " with basic auth credentials"
+ }
+ return mcp.NewToolResultText(msg), nil
+}
+
+// AddSessionTools registers session management tools with the MCP server.
+func AddSessionTools(s *server.MCPServer) {
+ tool := tools.NewTool(
+ "set_skywalking_url",
+ `Set the SkyWalking OAP server URL and optional basic auth
credentials for this session.
+
+This tool configures the connection to SkyWalking OAP for all subsequent tool
calls in the current session.
+The URL and credentials persist for the lifetime of the session.
+
+Priority: session URL (set by this tool) > --sw-url flag > default
(http://localhost:12800/graphql)
+
+Credentials support raw values or environment variable references using
${ENV_VAR} syntax.
+
+Examples:
+- {"url": "http://demo.skywalking.apache.org:12800"}: Connect without auth
+- {"url": "http://oap.internal:12800", "username": "admin", "password":
"admin"}: Connect with basic auth
+- {"url": "https://skywalking.example.com:443", "username": "${SW_USER}",
"password": "${SW_PASS}"}: Auth via env vars`,
+ setSkyWalkingURL,
+ mcp.WithString("url", mcp.Required(),
+ mcp.Description("SkyWalking OAP server URL (required).
Example: http://localhost:12800")),
+ mcp.WithString("username",
+ mcp.Description("Username for basic auth (optional).
Supports ${ENV_VAR} syntax.")),
+ mcp.WithString("password",
+ mcp.Description("Password for basic auth (optional).
Supports ${ENV_VAR} syntax.")),
+ )
+ tool.Register(s)
+}
diff --git a/internal/swmcp/stdio.go b/internal/swmcp/stdio.go
index 33a3529..02abb4a 100644
--- a/internal/swmcp/stdio.go
+++ b/internal/swmcp/stdio.go
@@ -19,7 +19,6 @@ package swmcp
import (
"context"
- "errors"
"fmt"
"io"
"log"
@@ -42,13 +41,8 @@ func NewStdioServer() *cobra.Command {
Short: "Start stdio server",
Long: `Start a server that communicates via standard
input/output streams using JSON-RPC messages.`,
RunE: func(_ *cobra.Command, _ []string) error {
- url := viper.GetString("url")
- if url == "" {
- return errors.New("--sw-url must be specified")
- }
-
stdioServerConfig := config.StdioServerConfig{
- URL: url,
+ URL: viper.GetString("url"),
ReadOnly: viper.GetBool("read-only"),
LogFilePath: viper.GetString("log-file"),
LogCommands: viper.GetBool("log-command"),
diff --git a/internal/tools/mqe.go b/internal/tools/mqe.go
index f152d49..0f4189a 100644
--- a/internal/tools/mqe.go
+++ b/internal/tools/mqe.go
@@ -20,6 +20,7 @@ package tools
import (
"bytes"
"context"
+ "encoding/base64"
"encoding/json"
"fmt"
"io"
@@ -29,7 +30,8 @@ import (
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
- "github.com/spf13/viper"
+
+ "github.com/apache/skywalking-cli/pkg/contextkey"
)
// AddMQETools registers MQE-related tools with the MCP server
@@ -53,8 +55,17 @@ type GraphQLResponse struct {
} `json:"errors,omitempty"`
}
-// executeGraphQL executes a GraphQL query against SkyWalking OAP
-func executeGraphQL(ctx context.Context, url, query string, variables
map[string]interface{}) (*GraphQLResponse, error) {
+// getContextString safely extracts a string value from context.
+func getContextString(ctx context.Context, key any) string {
+ if v, ok := ctx.Value(key).(string); ok {
+ return v
+ }
+ return ""
+}
+
+// executeGraphQLWithContext executes a GraphQL query using URL and auth from
context.
+func executeGraphQLWithContext(ctx context.Context, query string, variables
map[string]interface{}) (*GraphQLResponse, error) {
+ url := getContextString(ctx, contextkey.BaseURL{})
url = FinalizeURL(url)
reqBody := GraphQLRequest{
@@ -74,6 +85,14 @@ func executeGraphQL(ctx context.Context, url, query string,
variables map[string
req.Header.Set("Content-Type", "application/json")
+ // Add basic auth from context if present
+ username := getContextString(ctx, contextkey.Username{})
+ password := getContextString(ctx, contextkey.Password{})
+ if username != "" && password != "" {
+ auth := "Basic " +
base64.StdEncoding.EncodeToString([]byte(username+":"+password))
+ req.Header.Set("Authorization", auth)
+ }
+
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
@@ -186,7 +205,7 @@ func getServiceByName(ctx context.Context, serviceName,
layer string) (*bool, er
"serviceId": serviceID,
}
- result, err := executeGraphQL(ctx, viper.GetString("url"), query,
variables)
+ result, err := executeGraphQLWithContext(ctx, query, variables)
if err != nil {
return nil, fmt.Errorf("failed to get service details: %w", err)
}
@@ -217,7 +236,7 @@ func findServiceID(ctx context.Context, serviceName, layer
string) (string, erro
"layer": layer,
}
- result, err := executeGraphQL(ctx, viper.GetString("url"), query,
variables)
+ result, err := executeGraphQLWithContext(ctx, query, variables)
if err != nil {
return "", err
}
@@ -353,7 +372,7 @@ func executeMQEExpression(ctx context.Context, req
*MQEExpressionRequest) (*mcp.
"dumpDBRsp": req.DumpDBRsp,
}
- result, err := executeGraphQL(ctx, viper.GetString("url"), query,
variables)
+ result, err := executeGraphQLWithContext(ctx, query, variables)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to execute
MQE expression: %v", err)), nil
}
@@ -383,7 +402,7 @@ func listMQEMetrics(ctx context.Context, req
*MQEMetricsListRequest) (*mcp.CallT
variables["regex"] = req.Regex
}
- result, err := executeGraphQL(ctx, viper.GetString("url"), query,
variables)
+ result, err := executeGraphQLWithContext(ctx, query, variables)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to list
metrics: %v", err)), nil
}
@@ -431,7 +450,7 @@ func getMQEMetricsType(ctx context.Context, req
*MQEMetricsTypeRequest) (*mcp.Ca
"name": req.MetricName,
}
- result, err := executeGraphQL(ctx, viper.GetString("url"), query,
variables)
+ result, err := executeGraphQLWithContext(ctx, query, variables)
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to get metric
type: %v", err)), nil
}