loop: add /debug/tools handler for mcp and tool inspection
Adds a new debug endpoint at /debug/tools that displays all available
tools including their names, descriptions, JSON schemas, and metadata.
Shows both built-in Claude tools and MCP tools with proper type indication.
Includes nicely formatted HTML with syntax highlighting for schemas.
Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s10d266ddbc5a7fc0k
diff --git a/loop/server/loophttp.go b/loop/server/loophttp.go
index 11dd045..84e3615 100644
--- a/loop/server/loophttp.go
+++ b/loop/server/loophttp.go
@@ -30,6 +30,7 @@
"sketch.dev/claudetool/browse"
"sketch.dev/embedded"
"sketch.dev/git_tools"
+ "sketch.dev/llm"
"sketch.dev/llm/conversation"
"sketch.dev/loop"
"sketch.dev/loop/server/gzhandler"
@@ -1118,6 +1119,7 @@
<li><a href="pprof/trace">pprof/trace</a></li>
<li><a href="pprof/goroutine?debug=1">pprof/goroutine?debug=1</a></li>
<li><a href="conversation-history">conversation-history</a></li>
+ <li><a href="tools">tools</a></li>
</ul>
</body>
</html>
@@ -1153,9 +1155,101 @@
}
})
+ // Add tools debug handler
+ mux.HandleFunc("GET /debug/tools", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+
+ // Try to get the conversation and its tools
+ type ConvoProvider interface {
+ GetConvo() loop.ConvoInterface
+ }
+
+ if convoProvider, ok := agent.(ConvoProvider); ok {
+ convoInterface := convoProvider.GetConvo()
+
+ // Type assert to get the actual conversation
+ if convo, ok := convoInterface.(*conversation.Convo); ok {
+ // Render the tools debug page
+ renderToolsDebugPage(w, convo.Tools)
+ } else {
+ http.Error(w, "Unable to access conversation tools", http.StatusInternalServerError)
+ }
+ } else {
+ http.Error(w, "Agent does not support conversation debugging", http.StatusNotImplemented)
+ }
+ })
+
return mux
}
+// renderToolsDebugPage renders an HTML page showing all available tools
+func renderToolsDebugPage(w http.ResponseWriter, tools []*llm.Tool) {
+ fmt.Fprintf(w, `<!DOCTYPE html>
+<html>
+<head>
+ <title>Sketch Tools Debug</title>
+ <style>
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; margin: 40px; }
+ h1 { color: #333; }
+ .tool { background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 6px; padding: 20px; margin: 20px 0; }
+ .tool-name { font-size: 1.2em; font-weight: bold; color: #0366d6; margin-bottom: 8px; }
+ .tool-description { color: #586069; margin-bottom: 12px; }
+ .tool-schema { background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 4px; padding: 12px; font-family: 'SF Mono', Monaco, monospace; font-size: 12px; overflow-x: auto; }
+ .tool-meta { font-size: 0.9em; color: #656d76; margin-top: 8px; }
+ .summary { background: #e6f3ff; border-left: 4px solid #0366d6; padding: 16px; margin-bottom: 30px; }
+ </style>
+</head>
+<body>
+ <h1>Sketch Tools Debug</h1>
+ <div class="summary">
+ <strong>Total Tools Available:</strong> %d
+ </div>
+`, len(tools))
+
+ for i, tool := range tools {
+ fmt.Fprintf(w, ` <div class="tool">
+ <div class="tool-name">%d. %s</div>
+`, i+1, html.EscapeString(tool.Name))
+
+ if tool.Description != "" {
+ fmt.Fprintf(w, ` <div class="tool-description">%s</div>
+`, html.EscapeString(tool.Description))
+ }
+
+ // Display schema
+ if tool.InputSchema != nil {
+ // Pretty print the JSON schema
+ var schemaFormatted string
+ if prettySchema, err := json.MarshalIndent(json.RawMessage(tool.InputSchema), "", " "); err == nil {
+ schemaFormatted = string(prettySchema)
+ } else {
+ schemaFormatted = string(tool.InputSchema)
+ }
+ fmt.Fprintf(w, ` <div class="tool-schema">%s</div>
+`, html.EscapeString(schemaFormatted))
+ }
+
+ // Display metadata
+ var metaParts []string
+ if tool.Type != "" {
+ metaParts = append(metaParts, fmt.Sprintf("Type: %s", tool.Type))
+ }
+ if tool.EndsTurn {
+ metaParts = append(metaParts, "Ends Turn: true")
+ }
+ if len(metaParts) > 0 {
+ fmt.Fprintf(w, ` <div class="tool-meta">%s</div>
+`, html.EscapeString(strings.Join(metaParts, " | ")))
+ }
+
+ fmt.Fprintf(w, ` </div>
+`)
+ }
+
+ fmt.Fprintf(w, `</body>
+</html>`)
+}
+
// isValidGitSHA validates if a string looks like a valid git SHA hash.
// Git SHAs are hexadecimal strings of at least 4 characters but typically 7, 8, or 40 characters.
func isValidGitSHA(sha string) bool {