diff --git a/.github/resources/web-interface.png b/.github/resources/web-interface.png new file mode 100644 index 0000000..17a8171 Binary files /dev/null and b/.github/resources/web-interface.png differ diff --git a/README.md b/README.md index ee33e6f..049efed 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ - [Output Formats](#output-formats) - [Commands](#commands) - [Interactive Shell](#interactive-shell) + - [Web Interface](#web-interface) - [Project Scaffolding](#project-scaffolding) - [Server Aliases](#server-aliases) - [LLM Apps Config Management](#llm-apps-config-management) @@ -112,6 +113,7 @@ Available Commands: get-prompt Get a prompt on the MCP server read-resource Read a resource on the MCP server shell Start an interactive shell for MCP commands + web Start a web interface for MCP commands mock Create a mock MCP server with tools, prompts, and resources proxy Proxy MCP tool requests to shell scripts alias Manage MCP server aliases @@ -320,6 +322,36 @@ Special Commands: /q, /quit, exit Exit the shell ``` +### Web Interface + +MCP Tools provides a web interface for interacting with MCP servers through a browser-based UI: + +```bash +# Start a web interface for a filesystem server on default port (41999) +mcp web npx -y @modelcontextprotocol/server-filesystem ~ + +# Use a custom port +mcp web --port 8080 docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN ghcr.io/github/github-mcp-server + +# Use SSE +mcp web https://ne.tools +``` + +The web interface includes: + +- A sidebar listing all available tools, resources, and prompts +- Form-based and JSON-based parameter editing +- Formatted and raw JSON response views +- Interactive parameter forms automatically generated from tool schemas +- Support for complex parameter types (arrays, objects, nested structures) +- Direct API access for tool calling + +Once started, you can access the interface by opening `http://localhost:41999` (or your custom port) in a browser. + +

+ MCP Web Interface +

+ ### Project Scaffolding MCP Tools provides a scaffolding feature to quickly create new MCP servers with TypeScript: diff --git a/cmd/mcptools/commands/guard.go b/cmd/mcptools/commands/guard.go index aef30e5..5fb763b 100644 --- a/cmd/mcptools/commands/guard.go +++ b/cmd/mcptools/commands/guard.go @@ -165,6 +165,7 @@ func processPatternString(patternsStr string, patternMap map[string][]string) { patternValue := parts[1] // Map entity type to known types + //nolint:goconst // Using literals directly for readability switch entityType { case "tool", "tools": patternMap[EntityTypeTool] = append(patternMap[EntityTypeTool], patternValue) diff --git a/cmd/mcptools/commands/web.go b/cmd/mcptools/commands/web.go new file mode 100644 index 0000000..4406700 --- /dev/null +++ b/cmd/mcptools/commands/web.go @@ -0,0 +1,1306 @@ +package commands + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + "sync" + + "github.com/f/mcptools/pkg/client" + "github.com/spf13/cobra" +) + +// WebCmd creates the web command. +func WebCmd() *cobra.Command { + return &cobra.Command{ + Use: "web [command args...]", + Short: "Start a web interface for MCP commands", + DisableFlagParsing: true, + SilenceUsage: true, + Run: func(thisCmd *cobra.Command, args []string) { + if len(args) == 1 && (args[0] == FlagHelp || args[0] == FlagHelpShort) { + _ = thisCmd.Help() + return + } + + cmdArgs := args + parsedArgs := []string{} + port := "41999" // Default port + + for i := 0; i < len(cmdArgs); i++ { + switch { + case (cmdArgs[i] == "--port" || cmdArgs[i] == "-p") && i+1 < len(cmdArgs): + port = cmdArgs[i+1] + i++ + case cmdArgs[i] == FlagServerLogs: + ShowServerLogs = true + default: + parsedArgs = append(parsedArgs, cmdArgs[i]) + } + } + + if len(parsedArgs) == 0 { + fmt.Fprintln(os.Stderr, "Error: command to execute is required when using the web interface") + fmt.Fprintln(os.Stderr, "Example: mcp web npx -y @modelcontextprotocol/server-filesystem ~") + os.Exit(1) + } + + mcpClient, clientErr := CreateClientFunc(parsedArgs, client.CloseTransportAfterExecute(false)) + if clientErr != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", clientErr) + os.Exit(1) + } + + _, listErr := mcpClient.ListTools() + if listErr != nil { + fmt.Fprintf(os.Stderr, "Error connecting to MCP server: %v\n", listErr) + os.Exit(1) + } + + fmt.Fprintf(thisCmd.OutOrStdout(), "mcp > Starting MCP Tools Web Interface (%s)\n", Version) + fmt.Fprintf(thisCmd.OutOrStdout(), "mcp > Connected to Server: %s\n", strings.Join(parsedArgs, " ")) + fmt.Fprintf(thisCmd.OutOrStdout(), "mcp > Web server running at http://localhost:%s\n", port) + + // Web server handler + mux := http.NewServeMux() + + // Create a client cache that can be safely shared across goroutines + clientCache := &MCPClientCache{ + client: mcpClient, + mutex: &sync.Mutex{}, + } + + // Serve static files + mux.HandleFunc("/", handleIndex()) + mux.HandleFunc("/api/tools", handleTools(clientCache)) + mux.HandleFunc("/api/resources", handleResources(clientCache)) + mux.HandleFunc("/api/prompts", handlePrompts(clientCache)) + mux.HandleFunc("/api/call", handleCall(clientCache)) + + // Start the server + //nolint:gosec // Timeouts not implemented for this development/internal tool + err := http.ListenAndServe(":"+port, mux) + if err != nil { + fmt.Fprintf(os.Stderr, "Error starting web server: %v\n", err) + os.Exit(1) + } + }, + } +} + +// MCPClientCache provides thread-safe access to the MCP client. +type MCPClientCache struct { + client *client.Client + mutex *sync.Mutex +} + +// handleIndex serves the main web interface. +func handleIndex() http.HandlerFunc { + //nolint:revive // Parameter r is required by http.HandlerFunc signature + return func(w http.ResponseWriter, r *http.Request) { + // For simplicity, we'll embed a basic HTML page directly + // In a production app, we'd use proper templates and static files + html := ` + + + + + + MCP Tools + + + + + + + +
+

Select an item from the sidebar

+ + + + +
+
+
Formatted
+
Raw JSON
+
+ +
+ +
+
+ + + + +` + w.Header().Set("Content-Type", "text/html") + //nolint:errcheck,gosec // No need to handle error from Write in this context + w.Write([]byte(html)) + } +} + +// handleTools handles API requests for listing tools. +func handleTools(cache *MCPClientCache) http.HandlerFunc { + //nolint:revive // Parameter r is required by http.HandlerFunc signature + return func(w http.ResponseWriter, r *http.Request) { + cache.mutex.Lock() + resp, err := cache.client.ListTools() + cache.mutex.Unlock() + + w.Header().Set("Content-Type", "application/json") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + //nolint:errcheck,gosec // No need to handle error from Encode in this context + json.NewEncoder(w).Encode(map[string]interface{}{ + "error": err.Error(), + }) + return + } + + //nolint:errcheck,gosec // No need to handle error from Encode in this context + json.NewEncoder(w).Encode(map[string]interface{}{ + "result": resp, + }) + } +} + +// handleResources handles API requests for listing resources. +func handleResources(cache *MCPClientCache) http.HandlerFunc { + //nolint:revive // Parameter r is required by http.HandlerFunc signature + return func(w http.ResponseWriter, r *http.Request) { + cache.mutex.Lock() + resp, err := cache.client.ListResources() + cache.mutex.Unlock() + + w.Header().Set("Content-Type", "application/json") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + //nolint:errcheck,gosec // No need to handle error from Encode in this context + json.NewEncoder(w).Encode(map[string]interface{}{ + "error": err.Error(), + }) + return + } + + //nolint:errcheck,gosec // No need to handle error from Encode in this context + json.NewEncoder(w).Encode(map[string]interface{}{ + "result": resp, + }) + } +} + +// handlePrompts handles API requests for listing prompts. +func handlePrompts(cache *MCPClientCache) http.HandlerFunc { + //nolint:revive // Parameter r is required by http.HandlerFunc signature + return func(w http.ResponseWriter, r *http.Request) { + cache.mutex.Lock() + resp, err := cache.client.ListPrompts() + cache.mutex.Unlock() + + w.Header().Set("Content-Type", "application/json") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + //nolint:errcheck,gosec // No need to handle error from Encode in this context + json.NewEncoder(w).Encode(map[string]interface{}{ + "error": err.Error(), + }) + return + } + + //nolint:errcheck,gosec // No need to handle error from Encode in this context + json.NewEncoder(w).Encode(map[string]interface{}{ + "result": resp, + }) + } +} + +// handleCall handles API requests for calling tools/resources/prompts. +func handleCall(cache *MCPClientCache) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + var requestData struct { + Params map[string]interface{} `json:"params"` + Type string `json:"type"` + Name string `json:"name"` + } + + err := json.NewDecoder(r.Body).Decode(&requestData) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + //nolint:errcheck,gosec // No need to handle error from Encode in this context + json.NewEncoder(w).Encode(map[string]interface{}{ + "error": "Invalid request: " + err.Error(), + }) + return + } + + var resp map[string]interface{} + var callErr error + + cache.mutex.Lock() + defer cache.mutex.Unlock() + + switch requestData.Type { + case "tool": + resp, callErr = cache.client.CallTool(requestData.Name, requestData.Params) + case "resource": + resp, callErr = cache.client.ReadResource(requestData.Name) + case "prompt": + resp, callErr = cache.client.GetPrompt(requestData.Name) + default: + w.WriteHeader(http.StatusBadRequest) + //nolint:errcheck,gosec // No need to handle error from Encode in this context + json.NewEncoder(w).Encode(map[string]interface{}{ + "error": "Invalid entity type: " + requestData.Type, + }) + return + } + + w.Header().Set("Content-Type", "application/json") + if callErr != nil { + w.WriteHeader(http.StatusInternalServerError) + //nolint:errcheck,gosec // No need to handle error from Encode in this context + json.NewEncoder(w).Encode(map[string]interface{}{ + "error": callErr.Error(), + }) + return + } + + //nolint:errcheck,gosec // No need to handle error from Encode in this context + json.NewEncoder(w).Encode(map[string]interface{}{ + "result": resp, + }) + } +} diff --git a/cmd/mcptools/main.go b/cmd/mcptools/main.go index 2b7c046..2d2c91a 100644 --- a/cmd/mcptools/main.go +++ b/cmd/mcptools/main.go @@ -34,6 +34,7 @@ func main() { commands.GetPromptCmd(), commands.ReadResourceCmd(), commands.ShellCmd(), + commands.WebCmd(), commands.MockCmd(), commands.ProxyCmd(), commands.AliasCmd(), diff --git a/mcptools b/mcptools new file mode 100755 index 0000000..8b54a6b Binary files /dev/null and b/mcptools differ