package main import ( "crypto/rand" "crypto/subtle" "encoding/hex" "encoding/json" "io" "net/http" "fn-registry/functions/infra" ) const internalTokenHeader = "X-Internal-Token" // generateInternalToken returns a 32-byte hex token used by the kanban-mcp // subprocess to call back into /api/tool/{name}. Generated fresh per process. func generateInternalToken() string { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { panic("rand.Read: " + err.Error()) } return hex.EncodeToString(b) } // handleInternalTool exposes executeTool via HTTP for the MCP subprocess. // Auth: shared internal token in X-Internal-Token header. Constant-time compare. func handleInternalTool(db *DB, expectedToken string, logger *ChatLogger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { got := r.Header.Get(internalTokenHeader) if subtle.ConstantTimeCompare([]byte(got), []byte(expectedToken)) != 1 { infra.HTTPErrorResponse(w, infra.HTTPError{Status: http.StatusUnauthorized, Code: "unauthorized", Message: "invalid internal token"}) return } name := r.PathValue("name") if name == "" { infra.HTTPErrorResponse(w, infra.HTTPError{Status: http.StatusBadRequest, Code: "bad_request", Message: "tool name required"}) return } body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxBodyBytes)) if err != nil { infra.HTTPErrorResponse(w, infra.HTTPError{Status: http.StatusBadRequest, Code: "bad_request", Message: err.Error()}) return } if len(body) == 0 { body = []byte("{}") } input := json.RawMessage(body) if err := validateToolName(name); err != nil { infra.HTTPErrorResponse(w, infra.HTTPError{Status: http.StatusNotFound, Code: "unknown_tool", Message: err.Error()}) return } res := executeTool(db, name, input) if logger != nil { logger.Log(name, input, res) } // Always 200 — MCP-side maps res.OK to MCP isError. infra.HTTPJSONResponse(w, http.StatusOK, res) } }