Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ go.work.sum

# Build output
bin/
pipeboard
pipeboard.exe

# Conductor workspace
.context/
Expand Down
125 changes: 52 additions & 73 deletions cmd/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,42 @@ var debugCmd = &cobra.Command{
Hidden: true,
}

// --- Google Ads debug ---
// --- Meta Ads debug ---

var debugGoogleAdsCmd = &cobra.Command{
Use: "google-ads",
Short: "Send raw GAQL query to Google Ads API",
RunE: runDebugGoogleAds,
var debugMetaAdsCmd = &cobra.Command{
Use: "meta-ads",
Short: "Send raw Graph API query to Meta Ads API",
Long: `Send a raw Graph API request through Pipeboard (admin only).

Examples:
pipeboard debug meta-ads --path "me/adaccounts?fields=id,name"
pipeboard debug meta-ads --path "act_123/campaigns" --params '{"fields":"id,name,status"}'
pipeboard debug meta-ads --path "act_123/ads" --method POST --params '{"status":"PAUSED"}'`,
RunE: runDebugMetaAds,
}

var (
debugGoogleCustomerID string
debugGoogleLoginCustomerID string
debugGoogleGAQL string
debugMetaPath string
debugMetaMethod string
debugMetaParams string
debugMetaPageID string
)

func init() {
debugMetaAdsCmd.Flags().StringVar(&debugMetaPath, "path", "", "Graph API path, e.g. me/adaccounts or act_123/campaigns (required)")
debugMetaAdsCmd.Flags().StringVar(&debugMetaMethod, "method", "GET", "HTTP method: GET or POST")
debugMetaAdsCmd.Flags().StringVar(&debugMetaParams, "params", "", "JSON object of additional query/body params")
debugMetaAdsCmd.Flags().StringVar(&debugMetaPageID, "page-id", "", "Use this page's access token instead of user token")
debugMetaAdsCmd.MarkFlagRequired("path")
debugCmd.AddCommand(debugMetaAdsCmd)

debugGoogleAdsCmd.Flags().StringVar(&debugGoogleCustomerID, "customer-id", "", "Google Ads customer ID (required)")
debugGoogleAdsCmd.Flags().StringVar(&debugGoogleLoginCustomerID, "login-customer-id", "", "MCC login customer ID (optional)")
debugGoogleAdsCmd.Flags().StringVar(&debugGoogleGAQL, "gaql", "", "GAQL query to execute (required)")
debugGoogleAdsCmd.MarkFlagRequired("customer-id")
debugGoogleAdsCmd.MarkFlagRequired("gaql")
debugCmd.AddCommand(debugGoogleAdsCmd)

debugMetaAdsCmd.Flags().StringVar(&debugMetaAccountID, "account-id", "", "Meta ad account ID, e.g. act_123 (required)")
debugMetaAdsCmd.Flags().StringVar(&debugMetaPath, "path", "", "Graph API path, e.g. act_123/campaigns?fields=id,name (required)")
debugMetaAdsCmd.MarkFlagRequired("account-id")
debugMetaAdsCmd.MarkFlagRequired("path")
debugCmd.AddCommand(debugMetaAdsCmd)

debugTikTokAdsCmd.Flags().StringVar(&debugTikTokAdvertiserID, "advertiser-id", "", "TikTok advertiser ID (required)")
debugTikTokAdsCmd.Flags().StringVar(&debugTikTokEndpoint, "endpoint", "", "TikTok API endpoint path (required)")
debugTikTokAdsCmd.Flags().StringVar(&debugTikTokParams, "params", "{}", "JSON params for the request")
Expand All @@ -51,65 +59,59 @@ func init() {
debugCmd.AddCommand(debugTikTokAdsCmd)
}

func runDebugGoogleAds(cmd *cobra.Command, args []string) error {
func runDebugMetaAds(cmd *cobra.Command, args []string) error {
token, err := getToken()
if err != nil {
return err
}

c := client.New(apiURL, token)
// Parse optional params
var params map[string]string
if debugMetaParams != "" {
if err := json.Unmarshal([]byte(debugMetaParams), &params); err != nil {
return fmt.Errorf("invalid JSON in --params: %w", err)
}
}

params := map[string]interface{}{
"customer_id": debugGoogleCustomerID,
"gaql": debugGoogleGAQL,
body := map[string]interface{}{
"path": debugMetaPath,
"method": debugMetaMethod,
}
if debugGoogleLoginCustomerID != "" {
params["login_customer_id"] = debugGoogleLoginCustomerID
if params != nil {
body["params"] = params
}
if debugMetaPageID != "" {
body["page_id"] = debugMetaPageID
}

result, err := c.CallTool("google-ads-mcp", "debug_query", params)
c := client.NewREST(getWebAPIURL(), token)
result, err := c.Post("/api/debug/meta-ads", body)
if err != nil {
return fmt.Errorf("debug query failed: %w", err)
}

return printJSON(result)
}

// --- Meta Ads debug ---
// --- Google Ads debug (placeholder - not yet implemented server-side) ---

var debugMetaAdsCmd = &cobra.Command{
Use: "meta-ads",
Short: "Send raw Graph API query to Meta Ads API",
RunE: runDebugMetaAds,
var debugGoogleAdsCmd = &cobra.Command{
Use: "google-ads",
Short: "Send raw GAQL query to Google Ads API",
RunE: runDebugGoogleAds,
}

var (
debugMetaAccountID string
debugMetaPath string
debugGoogleCustomerID string
debugGoogleLoginCustomerID string
debugGoogleGAQL string
)

func runDebugMetaAds(cmd *cobra.Command, args []string) error {
token, err := getToken()
if err != nil {
return err
}

c := client.New(apiURL, token)

params := map[string]interface{}{
"account_id": debugMetaAccountID,
"path": debugMetaPath,
}

result, err := c.CallTool("meta-ads-mcp", "debug_query", params)
if err != nil {
return fmt.Errorf("debug query failed: %w", err)
}

return printJSON(result)
func runDebugGoogleAds(cmd *cobra.Command, args []string) error {
return fmt.Errorf("not yet implemented server-side. Coming soon")
}

// --- TikTok Ads debug ---
// --- TikTok Ads debug (placeholder - not yet implemented server-side) ---

var debugTikTokAdsCmd = &cobra.Command{
Use: "tiktok-ads",
Expand All @@ -124,30 +126,7 @@ var (
)

func runDebugTikTokAds(cmd *cobra.Command, args []string) error {
token, err := getToken()
if err != nil {
return err
}

c := client.New(apiURL, token)

var parsedParams map[string]interface{}
if err := json.Unmarshal([]byte(debugTikTokParams), &parsedParams); err != nil {
return fmt.Errorf("invalid JSON in --params: %w", err)
}

params := map[string]interface{}{
"advertiser_id": debugTikTokAdvertiserID,
"endpoint": debugTikTokEndpoint,
"params": parsedParams,
}

result, err := c.CallTool("tiktok-ads-mcp", "debug_query", params)
if err != nil {
return fmt.Errorf("debug query failed: %w", err)
}

return printJSON(result)
return fmt.Errorf("not yet implemented server-side. Coming soon")
}

func printJSON(v interface{}) error {
Expand Down
9 changes: 9 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ func getDefaultAPIURL() string {
return "https://mcp.pipeboard.co"
}

// getWebAPIURL returns the base URL for Pipeboard web API endpoints (not MCP).
// Debug and other REST endpoints live on the main domain, not on the MCP subdomain.
func getWebAPIURL() string {
if url := os.Getenv("PIPEBOARD_WEB_URL"); url != "" {
return url
}
return "https://pipeboard.co"
}

func getToken() (string, error) {
// 1. Check environment variable
if token := os.Getenv("PIPEBOARD_API_TOKEN"); token != "" {
Expand Down
68 changes: 68 additions & 0 deletions internal/client/rest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package client

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)

// RESTClient calls Pipeboard REST API endpoints.
type RESTClient struct {
baseURL string
token string
httpClient *http.Client
}

// NewREST creates a REST API client.
func NewREST(baseURL, token string) *RESTClient {
return &RESTClient{
baseURL: baseURL,
token: token,
httpClient: &http.Client{},
}
}

// Post sends a JSON POST request and returns the parsed response.
func (c *RESTClient) Post(path string, body interface{}) (map[string]interface{}, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("marshaling request: %w", err)
}

url := fmt.Sprintf("%s%s", c.baseURL, path)
req, err := http.NewRequest("POST", url, bytes.NewReader(data))
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}

req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.token)

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("sending request: %w", err)
}
defer resp.Body.Close()

respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response: %w", err)
}

var result map[string]interface{}
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
}

if resp.StatusCode == 401 || resp.StatusCode == 403 {
desc, _ := result["error_description"].(string)
if desc == "" {
desc, _ = result["error"].(string)
}
return nil, fmt.Errorf("%s (HTTP %d)", desc, resp.StatusCode)
}

return result, nil
}
Loading