mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-12 20:15:42 +00:00
155 lines
5.5 KiB
Go
155 lines
5.5 KiB
Go
package http
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
|
mcpbridge "github.com/nextlevelbuilder/goclaw/internal/mcp"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
// handleTestConnection tests an MCP server connection without saving it.
|
|
func (h *MCPHandler) handleTestConnection(w http.ResponseWriter, r *http.Request) {
|
|
locale := store.LocaleFromContext(r.Context())
|
|
var req struct {
|
|
ServerID *uuid.UUID `json:"server_id,omitempty"`
|
|
Transport string `json:"transport"`
|
|
Command string `json:"command"`
|
|
Args []string `json:"args"`
|
|
URL string `json:"url"`
|
|
Headers map[string]string `json:"headers"`
|
|
Env map[string]string `json:"env"`
|
|
}
|
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
|
return
|
|
}
|
|
if req.Transport == "" {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "transport")})
|
|
return
|
|
}
|
|
if err := mcpbridge.ValidateServerConfig(req.Transport, req.Command, req.Args, req.URL); err != nil {
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"success": false,
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// For an OAuth server, "test OAuth" means test with the OAuth token only:
|
|
// Authorization comes solely from the GLOBAL token — never from a body header
|
|
// fallback. Non-OAuth servers keep testing with the body headers/env as provided.
|
|
if req.ServerID != nil && h.store != nil {
|
|
if srv, err2 := h.store.GetServer(r.Context(), *req.ServerID); err2 == nil && srv != nil && mcpbridge.IsOAuthActive(srv.Settings) {
|
|
// Drop any body-supplied Authorization — no fallback for OAuth servers.
|
|
delete(req.Headers, "Authorization")
|
|
if h.oauthProvider != nil {
|
|
tenantID := store.TenantIDFromContext(r.Context())
|
|
if token, err3 := h.oauthProvider.GetValidToken(r.Context(), *req.ServerID, tenantID, ""); err3 == nil && token != "" {
|
|
if req.Headers == nil {
|
|
req.Headers = make(map[string]string)
|
|
}
|
|
req.Headers["Authorization"] = "Bearer " + token
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
tools, err := h.discoverTools(r.Context(), req.Transport, req.Command, req.Args, req.Env, req.URL, req.Headers)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"success": false,
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"success": true,
|
|
"tool_count": len(tools),
|
|
})
|
|
}
|
|
|
|
// handleListServerTools lists tools for a specific MCP server.
|
|
func (h *MCPHandler) handleListServerTools(w http.ResponseWriter, r *http.Request) {
|
|
locale := store.LocaleFromContext(r.Context())
|
|
id, err := uuid.Parse(r.PathValue("id"))
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidID, "server")})
|
|
return
|
|
}
|
|
|
|
srv, err := h.store.GetServer(r.Context(), id)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "server", id.String())})
|
|
return
|
|
}
|
|
|
|
// Try runtime Manager first — returns the original (bare, unprefixed) MCP
|
|
// tool names with their real descriptions, sourced from the live
|
|
// *BridgeTool for each registered tool. This must match the bare-name
|
|
// shape returned by the DiscoverTools fallback below: tool_allow grants
|
|
// saved from whichever shape this endpoint happens to return are matched
|
|
// against bare names by ListToolsForAgent's tool_cache lookups
|
|
// (internal/mcp/manager.go), so returning the registered/prefixed name
|
|
// here (as before) silently broke prompt-preview schemas for any server
|
|
// that was already live-connected when its grants were configured.
|
|
var tools []mcpbridge.ToolInfo
|
|
if h.mgr != nil {
|
|
if infos := h.mgr.ServerToolInfos(srv.Name); len(infos) > 0 {
|
|
tools = infos
|
|
}
|
|
}
|
|
|
|
// Fallback: on-demand discovery (returns names + descriptions).
|
|
if len(tools) == 0 && srv.Transport != "" {
|
|
var args []string
|
|
var env, headers map[string]string
|
|
_ = json.Unmarshal(srv.Args, &args)
|
|
_ = json.Unmarshal(srv.Env, &env)
|
|
_ = json.Unmarshal(srv.Headers, &headers)
|
|
|
|
discover := true
|
|
// OAuth servers: Authorization must come from the OAuth token. If no valid
|
|
// token is available (not authorized yet), do NOT discover via the server-level
|
|
// header fallback — return empty so the UI reflects "authorization required"
|
|
// instead of exposing tools the user can't actually call.
|
|
if mcpbridge.IsOAuthActive(srv.Settings) {
|
|
token := ""
|
|
if h.oauthProvider != nil {
|
|
tenantID := store.TenantIDFromContext(r.Context())
|
|
// Tool discovery is server-wide (the tool set is the same for every
|
|
// user), so always use the GLOBAL token — matching how srv.Headers/Env
|
|
// here are server-level, regardless of require_user_credentials.
|
|
token, _ = h.oauthProvider.GetValidToken(r.Context(), srv.ID, tenantID, "")
|
|
}
|
|
if token == "" {
|
|
discover = false
|
|
} else {
|
|
if headers == nil {
|
|
headers = make(map[string]string)
|
|
}
|
|
headers["Authorization"] = "Bearer " + token
|
|
}
|
|
}
|
|
|
|
if discover {
|
|
discovered, err := mcpbridge.DiscoverTools(r.Context(), srv.Transport, srv.Command, args, env, srv.URL, headers)
|
|
if err != nil {
|
|
slog.Warn("mcp.discover_tools", "server", srv.Name, "error", err)
|
|
} else {
|
|
tools = discovered
|
|
}
|
|
}
|
|
}
|
|
|
|
if tools == nil {
|
|
tools = []mcpbridge.ToolInfo{}
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"tools": tools})
|
|
}
|