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

wusheng pushed a commit to branch feat/session-skywalking-url
in repository https://gitbox.apache.org/repos/asf/skywalking-mcp.git

commit 06dab7ea244f1f7ee616a05bfe13ae6b81c79629
Author: Wu Sheng <[email protected]>
AuthorDate: Fri Mar 13 17:48:57 2026 +0800

    feat: add session-based SkyWalking URL and basic auth support
    
    Allow AI agents to configure the SkyWalking OAP connection at runtime
    via a new set_skywalking_url tool, supporting per-session URL and basic
    auth overrides. MQE tools now read connection settings from context
    instead of global viper state, and --sw-url is no longer mandatory for
    stdio mode.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 cmd/skywalking-mcp/main.go |   4 ++
 internal/swmcp/server.go   |  85 ++++++++++++++++++++++------
 internal/swmcp/session.go  | 134 +++++++++++++++++++++++++++++++++++++++++++++
 internal/swmcp/stdio.go    |   8 +--
 internal/tools/mqe.go      |  35 +++++++++---
 5 files changed, 233 insertions(+), 33 deletions(-)

diff --git a/cmd/skywalking-mcp/main.go b/cmd/skywalking-mcp/main.go
index 8ad4014..2db219d 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")
+       rootCmd.PersistentFlags().String("sw-password", "", "Password for basic 
auth to SkyWalking OAP")
        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..ec052b8 100644
--- a/internal/swmcp/server.go
+++ b/internal/swmcp/server.go
@@ -43,6 +43,7 @@ func newMCPServer() *server.MCPServer {
                server.WithPromptCapabilities(true),
                server.WithLogging(),
        )
+       AddSessionTools(s)
        tools.AddTraceTools(s)
        tools.AddLogTools(s)
        tools.AddMQETools(s)
@@ -73,14 +74,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 +100,20 @@ func configuredSkyWalkingURL() string {
        return tools.FinalizeURL(urlStr)
 }
 
+// configuredAuth returns the configured username and password from CLI flags 
or env vars.
+func configuredAuth() (username, password string) {
+       return viper.GetString("username"), 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 +125,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..f9c4f27
--- /dev/null
+++ b/internal/swmcp/session.go
@@ -0,0 +1,134 @@
+// 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, req.Username, 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 > SW_URL env > 
default (localhost:12800)
+
+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": "skywalking", 
"password": "skywalking"}: HTTPS with auth`,
+               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)")),
+               mcp.WithString("password",
+                       mcp.Description("Password for basic auth (optional)")),
+       )
+       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
        }

Reply via email to