diff --git a/.gitignore b/.gitignore index f71a4ea..87cd806 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,8 @@ go.work.sum # Build output bin/ +pipeboard +pipeboard.exe # Conductor workspace .context/ diff --git a/cmd/debug.go b/cmd/debug.go index 50d7d6b..176705c 100644 --- a/cmd/debug.go +++ b/cmd/debug.go @@ -15,21 +15,35 @@ 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)") @@ -37,12 +51,6 @@ func init() { 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") @@ -51,23 +59,33 @@ 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), ¶ms); 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) } @@ -75,41 +93,25 @@ func runDebugGoogleAds(cmd *cobra.Command, args []string) error { 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", @@ -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 { diff --git a/cmd/root.go b/cmd/root.go index 8910695..7e292b4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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 != "" { diff --git a/internal/client/rest.go b/internal/client/rest.go new file mode 100644 index 0000000..1f39625 --- /dev/null +++ b/internal/client/rest.go @@ -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 +}