@buygolinks/mcp 0.9.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +10 -4
  2. package/package.json +1 -1
  3. package/src/index.js +306 -14
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @buygolinks/mcp
2
2
 
3
- Model Context Protocol (MCP) server for [buygolinks.com](https://buygolinks.com) — create and manage branded short links (with Amazon affiliate geo-routing) from any MCP-compatible AI agent: Claude Desktop, Cursor, Cline, Continue, Hermes, and others.
3
+ Model Context Protocol (MCP) server for [buygolinks.com](https://buygolinks.com) — create and manage branded short links, campaigns, offers, and analytics from any MCP-compatible AI agent: Claude Desktop, Cursor, Cline, Continue, Hermes, and others.
4
4
 
5
5
  ## Install
6
6
 
@@ -62,7 +62,7 @@ Most MCP clients use the same shape. Point them at the `npx -y @buygolinks/mcp`
62
62
 
63
63
  | Tool | Description |
64
64
  |---|---|
65
- | `create_link` | Create a short link. Amazon URLs auto-detect ASIN and trigger geo-routing. |
65
+ | `create_link` | Create a short link. The destination URL is passed through untouched (include your own affiliate tag). |
66
66
  | `update_link` | Edit a link's title, source label, custom domain, password, archive state, or custom alias (e.g. `summer-sale` → `https://click.example.com/summer-sale`). |
67
67
  | `list_links` | List your links with click counts. |
68
68
  | `get_link_stats` | Per-link analytics (by day, country, device, browser, hour, weekday). |
@@ -74,16 +74,22 @@ Most MCP clients use the same shape. Point them at the `npx -y @buygolinks/mcp`
74
74
  | `list_affiliate_networks` / `create_affiliate_network` / `update_affiliate_network` / `delete_affiliate_network` | Manage affiliate networks. Each network exposes a per-network postback URL (`/postback/n/{token}`) the user pastes into the network's S2S settings. |
75
75
  | `list_offers` / `create_offer` / `update_offer` / `delete_offer` | Manage destination URLs ("offers"). Optionally tied to an affiliate network so the right conversion-param name is propagated on outbound clicks. |
76
76
  | `list_landers` / `create_lander` / `update_lander` / `delete_lander` | Manage landing pages. Lander URLs may contain `{clickid}` tokens which the resolver expands. |
77
- | `list_campaigns` / `create_campaign` / `get_campaign` / `update_campaign` / `delete_campaign` | Manage campaigns. One campaign = one tracked short link + N offers (weighted) + N optional landers + a per-campaign postback URL + a pixel snippet. |
77
+ | `list_traffic_sources` / `create_traffic_source` / `update_traffic_source` / `delete_traffic_source` | Manage **traffic sources** — the platforms you buy traffic from (Facebook, TikTok, Google, …). Each has a `name`, optional `platform`, and `token_params` mapping the source's click macros onto sub1..sub5 (e.g. Facebook sub1 = Ad ID `{{ad.id}}`). `list_traffic_sources` returns a `presets` catalog. Campaigns reference one via `traffic_source_id`. |
78
+ | `list_tracking_pixels` / `create_tracking_pixel` / `update_tracking_pixel` / `delete_tracking_pixel` | Manage ad-platform pixels (Facebook, Google, TikTok, …). Each pixel has a `default_event` (e.g. `Purchase`, `Lead`, `CompletePayment`) and, for Facebook/TikTok, an optional server-side Conversions API `capi_token`. `list_tracking_pixels` also returns a `platforms` catalog with each platform's valid `events`, `defaultEvent`, and `capi` support. Pixels fire on short-link clicks and can be chosen as a campaign's conversion pixel. |
79
+ | `list_campaigns` / `create_campaign` / `get_campaign` / `update_campaign` / `delete_campaign` | Manage campaigns. One campaign = one tracked short link + N offers (weighted) + N optional landers + a per-campaign postback URL + a pixel snippet. A campaign references one saved traffic source via `traffic_source_id` and an optional `conversion_pixel_id` + `conversion_event` that fires (server-side via CAPI when configured) when the campaign converts. Each campaign also returns a `click_url` (`…/click?cmp={code}`) — the defined lander CTA link that always resolves to an offer (never a dead page). Set `direct_slug` to run traffic straight to a lander with no tracker hop (`?utm_campaign={direct_slug}` stitches via the property tag); `fallback_url` is the never-dead-end guarantee. |
78
80
  | `set_campaign_offers` / `set_campaign_landers` | Replace the campaign's offer/lander split. Items are `{ offer_id, weight }` / `{ lander_id, weight }`. |
81
+ | `get_campaign_flow` / `set_campaign_flow` | Read/replace the campaign's **routing flow graph** (n8n-style). Nodes are `source` (one entry) / `lander` / `offer`; weighted edges go source→lander, source→offer, or lander→offer. The resolver walks this graph per click so a specific lander routes to its own specific offers — use it to split-test offers, divert a lander to a different offer, or insert a landing page before an offer. Saving also re-derives the flat offer/lander pools. |
79
82
  | `get_campaign_stats` | Full campaign report: visits, lander views, offer clicks, conversions, revenue, cost, profit, CTR/CR/EPC/ROI, breakdowns, recent events. |
80
83
  | `list_properties` / `create_property` / `get_property` / `update_property` / `delete_property` | Manage **properties** — GTM/GA-style site containers. One property = one universal tag (`/t.js?p={key}`) the user installs on every page of their site. Every page auto-reports pageviews, time-on-page, scroll, outbound clicks, and conversions; campaign traffic auto-attributes by click. `create_property` requires an active custom domain (the tag is served first-party from the user's own domain so it tracks on iOS/Safari) and returns the bound `tracking_domain` + install `snippet`. |
81
84
  | `set_property_campaigns` | Link/unlink campaigns to a property (many-to-many, for the source split in reports). Items are `{ campaign_id }`. |
82
85
  | `get_property_stats` | Full property report: page views, unique visitors, outbound, conversions, revenue, avg time/scroll, top pages, source split (organic vs campaign), country/device/browser/referrer breakdowns, recent events. |
86
+ | `list_property_pages` | Distinct pages seen across all the user's properties (most-viewed first): `{ url, path, title, property_name, views }`. Any page here can be used as a campaign **lander** — pass its `url` to `create_lander`, then wire that lander into a campaign flow with `set_campaign_flow`. |
87
+ | `get_tracker_overview` | Unified Tracker dashboard across **both** streams (sites + campaigns) in one call: totals (page views / visitors / visits / offer clicks / conversions / revenue), daily activity, country/device/browser breakdowns, per-property + per-campaign rollups, a **per-UTM-campaign** rollup (auto-detected Google `utm_*` tags), and the 50 most recent events. The "is everything running and getting traffic?" view. |
88
+ | `get_tracker_events` | Unified event log across both streams with full **search / filter / sort** over auto-detected UTM tags. `q` free-text, `stream`, `event`, exact `utm_*` filters, `sort`/`dir` (ts\|revenue\|utm_campaign\|utm_source\|country), pagination. Returns events + `facets` (distinct UTM values) + `total`. |
83
89
 
84
90
  ## Example prompts
85
91
 
86
- > "Create a short link for `https://www.amazon.com/dp/B0CFG6RB6T` with the title 'Anker GaN charger'."
92
+ > "Create a short link for `https://www.amazon.com/dp/B0CFG6RB6T?tag=mytag-20` with the title 'Anker GaN charger'."
87
93
 
88
94
  > "How many clicks did I get last week? Which country drove the most?"
89
95
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buygolinks/mcp",
3
- "version": "0.9.0",
3
+ "version": "0.16.0",
4
4
  "description": "MCP server for buygolinks.com — create and manage branded short links with Amazon affiliate geo-routing from any MCP-compatible AI agent (Claude Desktop, Cursor, Cline, Continue, Hermes).",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -48,7 +48,7 @@ const TOOLS = [
48
48
  {
49
49
  name: 'create_link',
50
50
  description:
51
- 'Create a new branded short link on buygolinks.com. If the destination is an Amazon product URL (contains /dp/ASIN), the ASIN is auto-extracted and the link will geo-route to the visitor\'s local Amazon storefront with the owner\'s affiliate tag applied. By default, the link is created under the user\'s first active custom domain (if any); pass `domain_id` to pin it to a specific one. Returns the short URL and link metadata (including `short_domain`, the hostname the link will resolve at).',
51
+ 'Create a new branded short link on buygolinks.com. The link redirects to exactly the destination URL you provide — for Amazon links, include your own Associates tag in the URL (e.g. ?tag=yourtag-20); buygolinks passes it through untouched and does not rewrite or geo-route Amazon URLs. By default, the link is created under the user\'s first active custom domain (if any); pass `domain_id` to pin it to a specific one. Returns the short URL and link metadata (including `short_domain`, the hostname the link will resolve at).',
52
52
  inputSchema: {
53
53
  type: 'object',
54
54
  properties: {
@@ -58,11 +58,11 @@ const TOOLS = [
58
58
  },
59
59
  title: {
60
60
  type: 'string',
61
- description: 'Optional human-readable title for the link. If omitted on an Amazon URL, the product title is scraped.',
61
+ description: 'Optional human-readable title for the link.',
62
62
  },
63
63
  source_label: {
64
64
  type: 'string',
65
- description: 'Optional source label (campaign tag) — appears in ascsubtag on Amazon redirects for attribution.',
65
+ description: 'Optional source label (campaign tag) for your own attribution.',
66
66
  },
67
67
  domain_id: {
68
68
  type: 'number',
@@ -126,7 +126,7 @@ const TOOLS = [
126
126
  {
127
127
  name: 'get_overview_stats',
128
128
  description:
129
- 'Get account-wide click analytics across all of the user\'s links: total clicks, clicks by day, top countries, devices, browsers, referrers, peak hour and weekday. Use this for "how is my account doing" type questions.',
129
+ 'Get account-wide analytics for the user\'s branded short LINKS only: total link CLICKS, clicks by day, top countries, devices, browsers, referrers, peak hour and weekday. This covers the Links section ONLY — it does NOT include Tracker page views, site-property traffic, or campaign visits/conversions. For PAGE VIEWS (site properties) or campaign traffic use the Tracker tools instead: get_tracker_overview (clicks-vs-pageviews unified rollup), list_properties, or get_property_stats. If the user asks for "clicks AND page views" together, call BOTH this tool and get_tracker_overview and combine them — they read from separate event streams. Use this for "how are my links doing" type questions.',
130
130
  inputSchema: {
131
131
  type: 'object',
132
132
  properties: {
@@ -240,7 +240,7 @@ const TOOLS = [
240
240
  {
241
241
  name: 'create_offer',
242
242
  description:
243
- 'Create an offer (a destination URL: affiliate landing page, product page, etc). If the URL is an Amazon /dp/ link the ASIN is auto-extracted. Link the offer to an affiliate_network_id so the resolver knows which postback param name to use when redirecting (e.g. some networks expect ?aff_sub=… instead of ?clickid=…).',
243
+ 'Create an offer (a destination URL: affiliate landing page, product page, etc). The URL is used exactly as given when redirecting — include any affiliate tag yourself. Link the offer to an affiliate_network_id so the resolver knows which postback param name to use when redirecting (e.g. some networks expect ?aff_sub=… instead of ?clickid=…).',
244
244
  inputSchema: {
245
245
  type: 'object',
246
246
  properties: {
@@ -319,6 +319,105 @@ const TOOLS = [
319
319
  inputSchema: { type: 'object', properties: { lander_id: { type: 'number' } }, required: ['lander_id'] },
320
320
  },
321
321
 
322
+ // ─── Traffic sources ────────────────────────────────────────────────────
323
+ {
324
+ name: 'list_traffic_sources',
325
+ description:
326
+ 'List the user\'s traffic sources (the platforms they buy traffic from — Facebook, TikTok, Google, …). Each source has a `name`, an optional `platform` (pixel-platform key), and `token_params`: how the source\'s click macros map onto the Google UTM params the resolver captures (utm_source/medium/campaign/term/content), e.g. Facebook utm_campaign = `{{campaign.id}}`, utm_content = Ad ID `{{ad.id}}`. The response also includes a `presets` catalog of ready-made sources with standard macros. Campaigns reference a source via `traffic_source_id` (see create_campaign / update_campaign).',
327
+ inputSchema: { type: 'object', properties: {} },
328
+ },
329
+ {
330
+ name: 'create_traffic_source',
331
+ description:
332
+ 'Create a traffic source. `token_params` is an array of `{ slot, label, token }` where slot is one of utm_source/utm_medium/utm_campaign/utm_term/utm_content, label is a human name (e.g. "Campaign ID"), and token is the platform macro (e.g. "{{campaign.id}}"). Use `list_traffic_sources` first to copy a preset\'s token_params for the big platforms.',
333
+ inputSchema: {
334
+ type: 'object',
335
+ properties: {
336
+ name: { type: 'string', description: 'e.g. "Facebook".' },
337
+ platform: { type: ['string', 'null'], description: 'Optional pixel-platform key (facebook, google_ads, tiktok, bing, …) for the icon + conversion-pixel suggestion.' },
338
+ token_params: {
339
+ type: 'array',
340
+ items: {
341
+ type: 'object',
342
+ properties: {
343
+ slot: { type: 'string', enum: ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'] },
344
+ label: { type: 'string' },
345
+ token: { type: 'string' },
346
+ },
347
+ required: ['slot'],
348
+ },
349
+ },
350
+ },
351
+ required: ['name'],
352
+ },
353
+ },
354
+ {
355
+ name: 'update_traffic_source',
356
+ description: 'Update a traffic source\'s name, platform, token_params, or archive state. token_params replaces the whole set.',
357
+ inputSchema: {
358
+ type: 'object',
359
+ properties: {
360
+ traffic_source_id: { type: 'number' },
361
+ name: { type: 'string' },
362
+ platform: { type: ['string', 'null'] },
363
+ token_params: { type: 'array', items: { type: 'object', properties: { slot: { type: 'string' }, label: { type: 'string' }, token: { type: 'string' } }, required: ['slot'] } },
364
+ archived: { type: 'boolean' },
365
+ },
366
+ required: ['traffic_source_id'],
367
+ },
368
+ },
369
+ {
370
+ name: 'delete_traffic_source',
371
+ description: 'Delete a traffic source. Campaigns using it keep their data but their `traffic_source_id` is cleared.',
372
+ inputSchema: { type: 'object', properties: { traffic_source_id: { type: 'number' } }, required: ['traffic_source_id'] },
373
+ },
374
+
375
+ // ─── Tracking pixels ────────────────────────────────────────────────────
376
+ {
377
+ name: 'list_tracking_pixels',
378
+ description:
379
+ 'List the user\'s tracking pixels (Facebook, Google Ads, TikTok, etc). Each pixel has a platform, pixel_id, a default_event it fires, an active flag, and has_capi_token (whether a server-side Conversions API token is set — the token itself is never returned). The response also includes a `platforms` catalog with each platform\'s valid `events`, its `defaultEvent`, and `capi` (whether server-side conversions are supported). Pixels fire on short-link clicks (account-wide by default, or per-link selection) and can be chosen as a campaign\'s conversion pixel.',
380
+ inputSchema: { type: 'object', properties: {} },
381
+ },
382
+ {
383
+ name: 'create_tracking_pixel',
384
+ description:
385
+ 'Create a tracking pixel. `platform` is one of: facebook, google_ads, google_analytics, google_tag_manager, linkedin, twitter, tiktok, pinterest, snapchat, reddit, quora, bing, adroll. `default_event` must be a valid event for that platform (see the `platforms` catalog from list_tracking_pixels) — defaults to the platform default (e.g. Facebook "PageView"). For Facebook and TikTok you can pass a `capi_token` (server-side Conversions API access token) so campaign conversions fire server-to-server.',
386
+ inputSchema: {
387
+ type: 'object',
388
+ properties: {
389
+ platform: { type: 'string', description: 'Pixel platform key (facebook, google_ads, tiktok, …).' },
390
+ pixel_id: { type: 'string', description: 'The platform\'s pixel/dataset id.' },
391
+ label: { type: 'string', description: 'Optional friendly name.' },
392
+ default_event: { type: 'string', description: 'Event this pixel fires by default (must be valid for the platform).' },
393
+ capi_token: { type: 'string', description: 'Optional server-side Conversions API access token (Facebook/TikTok only).' },
394
+ active: { type: 'boolean', description: 'Whether the pixel is active. Defaults to true.' },
395
+ },
396
+ required: ['platform', 'pixel_id'],
397
+ },
398
+ },
399
+ {
400
+ name: 'update_tracking_pixel',
401
+ description: 'Update a tracking pixel\'s pixel_id, label, default_event, active flag, or CAPI token. Pass capi_token as an empty string or null to clear the stored token.',
402
+ inputSchema: {
403
+ type: 'object',
404
+ properties: {
405
+ pixel_id_row: { type: 'number', description: 'The id of the tracking_pixels row to update (from list_tracking_pixels).' },
406
+ pixel_id: { type: 'string', description: 'New platform pixel id.' },
407
+ label: { type: 'string' },
408
+ default_event: { type: 'string', description: 'New default event (must be valid for the pixel\'s platform).' },
409
+ capi_token: { type: ['string', 'null'], description: 'New Conversions API token, or empty/null to clear.' },
410
+ active: { type: 'boolean' },
411
+ },
412
+ required: ['pixel_id_row'],
413
+ },
414
+ },
415
+ {
416
+ name: 'delete_tracking_pixel',
417
+ description: 'Delete a tracking pixel. It is removed from any per-link selections and any campaign that used it as its conversion pixel (the campaign\'s conversion_pixel_id is cleared).',
418
+ inputSchema: { type: 'object', properties: { pixel_id_row: { type: 'number' } }, required: ['pixel_id_row'] },
419
+ },
420
+
322
421
  // ─── Tracker: campaigns ─────────────────────────────────────────────────
323
422
  {
324
423
  name: 'list_campaigns',
@@ -329,17 +428,23 @@ const TOOLS = [
329
428
  {
330
429
  name: 'create_campaign',
331
430
  description:
332
- 'Create a campaign. A campaign mints one short link (campaign.code) that the user pastes into Facebook / TikTok / etc. The resolver mints a click_id per visit, optionally routes through a lander (weighted), then redirects to an offer (weighted). Conversions come back via the campaign\'s postback URL or the affiliate network\'s postback URL. You can optionally attach offer_ids / lander_ids inline with matching offer_weights / lander_weights arrays for a one-call setup.',
431
+ 'Create a campaign. A campaign mints one short link (campaign.code) that the user pastes into Facebook / TikTok / etc. The resolver mints a click_id per visit, optionally routes through a lander (weighted), then redirects to an offer (weighted). The response also includes a `click_url` (`…/click?cmp={code}`) — the DEFINED lander click URL the user pastes into their lander\'s CTA button; it always resolves to an offer (chosen by the flow graph), never a lander, never a dead page. For DIRECT-LINK campaigns (ad points straight at the lander, no tracker hop), set `direct_slug` and put `?utm_campaign={direct_slug}` on the ad URL — the property tag then stitches the page view to this campaign. `fallback_url` guarantees the click never dead-ends. Conversions come back via the campaign\'s postback URL or the affiliate network\'s postback URL. You can optionally attach offer_ids / lander_ids inline with matching offer_weights / lander_weights arrays for a one-call setup.',
333
432
  inputSchema: {
334
433
  type: 'object',
335
434
  properties: {
336
435
  name: { type: 'string' },
337
- traffic_source_label: { type: 'string', description: 'Free text, e.g. "Facebook", "TikTok".' },
436
+ traffic_source_id: { type: ['number', 'null'], description: 'Id of one of the user\'s saved traffic sources (from list_traffic_sources) to attribute this campaign to. Preferred over the legacy `traffic_source` string. Mirrors the source name into the display label.' },
437
+ traffic_source: { type: 'string', description: 'Legacy structured traffic source key — one of: facebook, instagram, google, youtube, tiktok, microsoft, linkedin, twitter, pinterest, snapchat, reddit, native, email, organic, other. Prefer `traffic_source_id`.' },
438
+ traffic_source_label: { type: 'string', description: 'Free text display label.' },
338
439
  country: { type: 'string', description: 'ISO2 default geo.' },
339
440
  cost_model: { type: 'string', enum: ['not_tracked', 'cpc', 'cpm'] },
340
441
  cost_value: { type: 'number' },
341
442
  transition_mode: { type: 'string', enum: ['302', 'meta_refresh'] },
342
443
  domain_id: { type: ['number', 'null'], description: 'Optional id of one of the user\'s active custom domains (from list_domains). The campaign\'s short link will resolve at that hostname. If omitted, defaults to the user\'s first active custom domain, or buygolinks.com if they have none. Pass null to force the platform default.' },
444
+ conversion_pixel_id: { type: ['number', 'null'], description: 'Optional id of one of the user\'s tracking pixels (from list_tracking_pixels) to fire when this campaign converts (server-side via the pixel\'s CAPI token when set). Pass null for no conversion pixel.' },
445
+ conversion_event: { type: 'string', description: 'Event name to fire on conversion (e.g. "Purchase", "Lead", "CompletePayment"). Must be a valid event for the conversion pixel\'s platform; defaults to that pixel\'s default_event.' },
446
+ direct_slug: { type: ['string', 'null'], description: 'Optional direct-link tag, unique per user (lowercased alnum + dashes, e.g. "bsu-fb-cold"). For running traffic STRAIGHT to a lander with no tracker hop: put `?utm_campaign={direct_slug}` on the ad URL and the property tag stitches the page view to this campaign. Returns 409 (direct_slug_taken) if another campaign already uses it. Pass null to clear.' },
447
+ fallback_url: { type: ['string', 'null'], description: 'Optional URL the click falls back to when the flow yields no live offer — the never-dead-end guarantee. Pass null to use the platform default.' },
343
448
  offer_ids: { type: 'array', items: { type: 'number' } },
344
449
  offer_weights: { type: 'array', items: { type: 'number' } },
345
450
  lander_ids: { type: 'array', items: { type: 'number' } },
@@ -355,18 +460,24 @@ const TOOLS = [
355
460
  },
356
461
  {
357
462
  name: 'update_campaign',
358
- description: 'Update a campaign\'s name, country, cost model/value, transition mode, traffic source label, branded domain, or archive state. Use set_campaign_offers / set_campaign_landers to change the destination wiring. Pass domain_id to move the campaign\'s short link to a different custom domain, or null to send it back to buygolinks.com.',
463
+ description: 'Update a campaign\'s name, structured traffic source, country, cost model/value, transition mode, traffic source label, branded domain, conversion pixel/event, direct-link slug, fallback URL, or archive state. Use set_campaign_offers / set_campaign_landers (or set_campaign_flow) to change the destination wiring. Pass domain_id to move the campaign\'s short link to a different custom domain, or null to send it back to buygolinks.com.',
359
464
  inputSchema: {
360
465
  type: 'object',
361
466
  properties: {
362
467
  campaign_id: { type: 'number' },
363
468
  name: { type: 'string' },
469
+ traffic_source_id: { type: ['number', 'null'], description: 'Id of a saved traffic source (from list_traffic_sources), or null to clear. Preferred over `traffic_source`.' },
470
+ traffic_source: { type: ['string', 'null'], description: 'Legacy structured traffic source key or null to clear.' },
364
471
  traffic_source_label: { type: 'string' },
365
472
  country: { type: 'string' },
366
473
  cost_model: { type: 'string', enum: ['not_tracked', 'cpc', 'cpm'] },
367
474
  cost_value: { type: 'number' },
368
475
  transition_mode: { type: 'string', enum: ['302', 'meta_refresh'] },
369
476
  domain_id: { type: ['number', 'null'], description: 'Id of one of the user\'s active custom domains, or null to move the campaign back to buygolinks.com.' },
477
+ conversion_pixel_id: { type: ['number', 'null'], description: 'Id of a tracking pixel to fire on conversion, or null to clear.' },
478
+ conversion_event: { type: 'string', description: 'Event the conversion pixel fires (must be valid for that pixel\'s platform).' },
479
+ direct_slug: { type: ['string', 'null'], description: 'Direct-link tag for utm_campaign-based stitching (unique per user), or null to clear. Returns 409 (direct_slug_taken) on collision.' },
480
+ fallback_url: { type: ['string', 'null'], description: 'Never-dead-end fallback URL when no live offer exists, or null to use the platform default.' },
370
481
  archived: { type: 'boolean' },
371
482
  },
372
483
  required: ['campaign_id'],
@@ -415,6 +526,54 @@ const TOOLS = [
415
526
  required: ['campaign_id', 'items'],
416
527
  },
417
528
  },
529
+ {
530
+ name: 'get_campaign_flow',
531
+ description:
532
+ 'Get the campaign\'s routing flow graph (n8n-style). Returns { flow: { nodes, edges }, offers, landers }. Nodes are { id, kind: "source"|"lander"|"offer", ref_id, pos_x, pos_y, offer?, lander? } — exactly one "source" node (the traffic entry). Edges are { id, from_node_id, to_node_id, weight } and only go source→lander, source→offer (direct), or lander→offer; weight is the split-test weight. This graph is what the resolver walks per click, letting a specific lander route to its own specific offers.',
533
+ inputSchema: {
534
+ type: 'object',
535
+ properties: { campaign_id: { type: 'number' } },
536
+ required: ['campaign_id'],
537
+ },
538
+ },
539
+ {
540
+ name: 'set_campaign_flow',
541
+ description:
542
+ 'Replace the campaign\'s routing flow graph. Body: { nodes: [{ tmp_id, kind, ref_id, pos_x, pos_y }], edges: [{ from, to, weight }] }. tmp_id is any client-stable string key; edges reference nodes by tmp_id. Provide exactly one node with kind="source" (ref_id null) as the traffic entry; "lander" / "offer" nodes set ref_id to a lander/offer id you own. Edges must go source→lander, source→offer, or lander→offer. weight is an integer split-test weight (higher = more traffic). Saving also re-derives the flat offer/lander pools, so legacy reports stay consistent. Use this to add a second offer for a split test, divert a lander to a different offer, or insert a landing page before an offer.',
543
+ inputSchema: {
544
+ type: 'object',
545
+ properties: {
546
+ campaign_id: { type: 'number' },
547
+ nodes: {
548
+ type: 'array',
549
+ items: {
550
+ type: 'object',
551
+ properties: {
552
+ tmp_id: { type: 'string' },
553
+ kind: { type: 'string', enum: ['source', 'lander', 'offer'] },
554
+ ref_id: { type: ['number', 'null'] },
555
+ pos_x: { type: 'number' },
556
+ pos_y: { type: 'number' },
557
+ },
558
+ required: ['tmp_id', 'kind'],
559
+ },
560
+ },
561
+ edges: {
562
+ type: 'array',
563
+ items: {
564
+ type: 'object',
565
+ properties: {
566
+ from: { type: 'string' },
567
+ to: { type: 'string' },
568
+ weight: { type: 'number' },
569
+ },
570
+ required: ['from', 'to'],
571
+ },
572
+ },
573
+ },
574
+ required: ['campaign_id', 'nodes', 'edges'],
575
+ },
576
+ },
418
577
  {
419
578
  name: 'get_campaign_stats',
420
579
  description:
@@ -433,13 +592,13 @@ const TOOLS = [
433
592
  {
434
593
  name: 'list_properties',
435
594
  description:
436
- 'List the user\'s properties with quick-look stats (page_views / unique_visitors / conversions / revenue). A property is a whole-site container: the user installs ONE universal tag (returned as `snippet`) on every page of their site and every page auto-reports pageviews, time-on-page, scroll depth, outbound clicks, and conversions. Page events auto-attribute to a campaign when a visitor arrives carrying that campaign\'s click.',
595
+ 'List the user\'s properties with quick-look stats (page_views / unique_visitors / conversions / revenue). Best one-call tool for "page views per property" / "page views for each site". A property is a whole-site container: the user installs ONE universal tag (returned as `snippet`) on every page of their site and every page auto-reports pageviews, time-on-page, scroll depth, outbound clicks, and conversions. Page events auto-attribute to a campaign when a visitor arrives carrying that campaign\'s click.',
437
596
  inputSchema: { type: 'object', properties: {} },
438
597
  },
439
598
  {
440
599
  name: 'create_property',
441
600
  description:
442
- 'Create a property (a GTM/GA-style site container). Returns the property with its public property_key, the `tracking_domain` it\'s bound to, and an install `snippet` — a single <script> tag the user pastes into the <head> of every page of their site. REQUIRES an active custom domain: the tag is served from the user\'s own domain so it stays first-party to their site, which is required for tracking to work on iOS/Safari (a tag loaded cross-site from buygolinks.com gets its visitor cookie partitioned/blocked). Defaults to the user\'s first active custom domain; pass `domain_id` (from list_domains) to pin a specific one. If the user has no active custom domain, returns error `domain_required` — have them add one via create_domain first. To record conversions, the site sets window.bglConversion = true (optionally window.bglRevenue) on a thank-you page, or calls window.bglFireConversion(revenue).',
601
+ 'Create a property (a GTM/GA-style site container). Returns the property with its public property_key, the `tracking_domain` it\'s bound to, and an install `snippet` — a single GA/Facebook-style inline <script> block (a self-invoking function that sets up a conversion-queue stub and injects the async tag) the user pastes into the <head> of every page of their site. REQUIRES an active custom domain: the tag is served from the user\'s own domain so it stays first-party to their site, which is required for tracking to work on iOS/Safari (a tag loaded cross-site from buygolinks.com gets its visitor cookie partitioned/blocked). Defaults to the user\'s first active custom domain; pass `domain_id` (from list_domains) to pin a specific one. If the user has no active custom domain, returns error `domain_required` — have them add one via create_domain first. To record conversions, the site sets window.bglConversion = true (optionally window.bglRevenue) on a thank-you page, or calls window.bglFireConversion(revenue) — the queue stub makes that call safe even before the async tag finishes loading.',
443
602
  inputSchema: {
444
603
  type: 'object',
445
604
  properties: {
@@ -497,7 +656,7 @@ const TOOLS = [
497
656
  {
498
657
  name: 'get_property_stats',
499
658
  description:
500
- 'Get a property\'s GA-style report: totals (page_views / unique_visitors / outbound / conversions / revenue / avg_time_on_page_ms / avg_scroll_pct / CR), by_day, by_page (top paths with views+conversions+revenue), by_source (organic vs each stitched campaign), by_country / by_device / by_browser / by_referrer, and the 50 most recent events.',
659
+ 'Get a single property\'s GA-style PAGE VIEW report: totals (page_views / unique_visitors / outbound / conversions / revenue / avg_time_on_page_ms / avg_scroll_pct / CR), by_day, by_page (top paths with views+conversions+revenue), by_source (organic vs each stitched campaign), by_country / by_device / by_browser / by_referrer, and the 50 most recent events.',
501
660
  inputSchema: {
502
661
  type: 'object',
503
662
  properties: {
@@ -507,10 +666,50 @@ const TOOLS = [
507
666
  required: ['property_id'],
508
667
  },
509
668
  },
669
+ {
670
+ name: 'list_property_pages',
671
+ description:
672
+ 'List the distinct pages observed across ALL the user\'s properties (the pageview stream), most-viewed first. Each entry is `{ url, path, title, property_id, property_name, views }`. Any of these pages can be used as a campaign LANDER — pass its `url` to `create_lander` (the page title makes a good lander name) and then wire the resulting lander into a campaign flow via `set_campaign_flow`. Use this to turn a real page on the user\'s site into a lander without retyping its URL.',
673
+ inputSchema: { type: 'object', properties: {} },
674
+ },
675
+ {
676
+ name: 'get_tracker_overview',
677
+ description:
678
+ 'PAGE VIEWS live here. Get the unified Tracker dashboard rollup across BOTH event streams (site properties + campaigns) in one call. Returns totals (page_views / unique_visitors / visits / offer_clicks / conversions / revenue), a merged daily activity series (by_day), dimension breakdowns (by_country / by_device / by_browser), per-property and per-campaign rollups (the per-property rollup gives page_views for each site/property), a per-UTM-campaign rollup (utm_campaigns — auto-detected Google utm_source/medium/campaign/content tags with events/conversions/revenue), and the 50 most recent events across both streams. This is the go-to tool for "page views", "page views per property", "campaign traffic", and "is everything running and getting traffic?". NOTE: page_views here is Tracker site traffic and is SEPARATE from branded short-link clicks (get_overview_stats). If the user asks for "clicks AND page views" together, call BOTH this tool and get_overview_stats and combine them.',
679
+ inputSchema: {
680
+ type: 'object',
681
+ properties: {
682
+ days: { type: 'number', description: 'Window size in days (1–90). Defaults to 14.' },
683
+ },
684
+ },
685
+ },
686
+ {
687
+ name: 'get_tracker_events',
688
+ description:
689
+ 'Query the unified Tracker event log across BOTH streams (site properties + campaigns), with full search / filter / sort over auto-detected Google UTM tags. Every event carries utm_source / utm_medium / utm_campaign / utm_term / utm_content. Use `q` for free-text search (matches any utm value, country, device, browser, referrer, source name, event type); `stream` to scope to all|property|campaign; `event` to filter by event_type; the utm_* params for exact-match filtering; and `sort`+`dir` to order by ts|revenue|utm_campaign|utm_source|country. Returns the matching page of events plus `facets` (distinct utm values present in the window) to drive filter pickers, and `total` for the full match count.',
690
+ inputSchema: {
691
+ type: 'object',
692
+ properties: {
693
+ days: { type: 'number', description: 'Window size in days (1–90). Defaults to 14.' },
694
+ q: { type: 'string', description: 'Free-text search across utm_*, country, device, browser, referrer, source name, event type.' },
695
+ stream: { type: 'string', enum: ['all', 'property', 'campaign'], description: 'Scope to one stream. Defaults to all.' },
696
+ event: { type: 'string', description: 'Exact event_type filter, e.g. page_view, outbound, conversion, visit, lander_view, offer_click.' },
697
+ utm_source: { type: 'string', description: 'Exact utm_source filter.' },
698
+ utm_medium: { type: 'string', description: 'Exact utm_medium filter.' },
699
+ utm_campaign: { type: 'string', description: 'Exact utm_campaign filter.' },
700
+ utm_term: { type: 'string', description: 'Exact utm_term filter.' },
701
+ utm_content: { type: 'string', description: 'Exact utm_content filter.' },
702
+ sort: { type: 'string', enum: ['ts', 'revenue', 'utm_campaign', 'utm_source', 'country'], description: 'Sort field. Defaults to ts.' },
703
+ dir: { type: 'string', enum: ['asc', 'desc'], description: 'Sort direction. Defaults to desc.' },
704
+ limit: { type: 'number', description: 'Page size (1–500). Defaults to 200.' },
705
+ offset: { type: 'number', description: 'Pagination offset. Defaults to 0.' },
706
+ },
707
+ },
708
+ },
510
709
  ];
511
710
 
512
711
  const server = new Server(
513
- { name: 'buygolinks', version: '0.8.0' },
712
+ { name: 'buygolinks', version: '0.11.0' },
514
713
  { capabilities: { tools: {} } },
515
714
  );
516
715
 
@@ -671,6 +870,61 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
671
870
  break;
672
871
  }
673
872
 
873
+ // ── Traffic sources ──
874
+ case 'list_traffic_sources': {
875
+ result = await api('/api/traffic-sources');
876
+ break;
877
+ }
878
+ case 'create_traffic_source': {
879
+ const body = { name: args.name };
880
+ if ('platform' in args) body.platform = args.platform;
881
+ if (Array.isArray(args.token_params)) body.token_params = args.token_params;
882
+ result = await api('/api/traffic-sources', { method: 'POST', body: JSON.stringify(body) });
883
+ break;
884
+ }
885
+ case 'update_traffic_source': {
886
+ const body = {};
887
+ if (typeof args.name === 'string') body.name = args.name;
888
+ if ('platform' in args) body.platform = args.platform;
889
+ if (Array.isArray(args.token_params)) body.token_params = args.token_params;
890
+ if (typeof args.archived === 'boolean') body.archived = args.archived;
891
+ result = await api(`/api/traffic-sources/${Number(args.traffic_source_id)}`, { method: 'PATCH', body: JSON.stringify(body) });
892
+ break;
893
+ }
894
+ case 'delete_traffic_source': {
895
+ result = await api(`/api/traffic-sources/${Number(args.traffic_source_id)}`, { method: 'DELETE' });
896
+ break;
897
+ }
898
+
899
+ // ── Tracking pixels ──
900
+ case 'list_tracking_pixels': {
901
+ result = await api('/api/tracking-pixels');
902
+ break;
903
+ }
904
+ case 'create_tracking_pixel': {
905
+ const body = { platform: args.platform, pixel_id: args.pixel_id };
906
+ if (typeof args.label === 'string') body.label = args.label;
907
+ if (typeof args.default_event === 'string') body.default_event = args.default_event;
908
+ if (typeof args.capi_token === 'string') body.capi_token = args.capi_token;
909
+ if (typeof args.active === 'boolean') body.active = args.active;
910
+ result = await api('/api/tracking-pixels', { method: 'POST', body: JSON.stringify(body) });
911
+ break;
912
+ }
913
+ case 'update_tracking_pixel': {
914
+ const body = {};
915
+ if (typeof args.pixel_id === 'string') body.pixel_id = args.pixel_id;
916
+ if (typeof args.label === 'string') body.label = args.label;
917
+ if (typeof args.default_event === 'string') body.default_event = args.default_event;
918
+ if ('capi_token' in args) body.capi_token = args.capi_token;
919
+ if (typeof args.active === 'boolean') body.active = args.active;
920
+ result = await api(`/api/tracking-pixels/${Number(args.pixel_id_row)}`, { method: 'PATCH', body: JSON.stringify(body) });
921
+ break;
922
+ }
923
+ case 'delete_tracking_pixel': {
924
+ result = await api(`/api/tracking-pixels/${Number(args.pixel_id_row)}`, { method: 'DELETE' });
925
+ break;
926
+ }
927
+
674
928
  // ── Tracker: campaigns ──
675
929
  case 'list_campaigns': {
676
930
  result = await api('/api/campaigns');
@@ -678,11 +932,15 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
678
932
  }
679
933
  case 'create_campaign': {
680
934
  const body = { name: args.name };
681
- for (const k of ['traffic_source_label', 'country', 'cost_model', 'transition_mode']) {
935
+ for (const k of ['traffic_source', 'traffic_source_label', 'country', 'cost_model', 'transition_mode', 'conversion_event']) {
682
936
  if (typeof args[k] === 'string') body[k] = args[k];
683
937
  }
684
938
  if (typeof args.cost_value === 'number') body.cost_value = args.cost_value;
685
939
  if ('domain_id' in args) body.domain_id = args.domain_id;
940
+ if ('traffic_source_id' in args) body.traffic_source_id = args.traffic_source_id;
941
+ if ('conversion_pixel_id' in args) body.conversion_pixel_id = args.conversion_pixel_id;
942
+ if ('direct_slug' in args) body.direct_slug = args.direct_slug;
943
+ if ('fallback_url' in args) body.fallback_url = args.fallback_url;
686
944
  if (Array.isArray(args.offer_ids)) body.offer_ids = args.offer_ids;
687
945
  if (Array.isArray(args.offer_weights)) body.offer_weights = args.offer_weights;
688
946
  if (Array.isArray(args.lander_ids)) body.lander_ids = args.lander_ids;
@@ -696,11 +954,16 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
696
954
  }
697
955
  case 'update_campaign': {
698
956
  const body = {};
699
- for (const k of ['name', 'traffic_source_label', 'country', 'cost_model', 'transition_mode']) {
957
+ for (const k of ['name', 'traffic_source_label', 'country', 'cost_model', 'transition_mode', 'conversion_event']) {
700
958
  if (typeof args[k] === 'string') body[k] = args[k];
701
959
  }
960
+ if ('traffic_source' in args) body.traffic_source = args.traffic_source;
961
+ if ('traffic_source_id' in args) body.traffic_source_id = args.traffic_source_id;
702
962
  if (typeof args.cost_value === 'number') body.cost_value = args.cost_value;
703
963
  if ('domain_id' in args) body.domain_id = args.domain_id;
964
+ if ('conversion_pixel_id' in args) body.conversion_pixel_id = args.conversion_pixel_id;
965
+ if ('direct_slug' in args) body.direct_slug = args.direct_slug;
966
+ if ('fallback_url' in args) body.fallback_url = args.fallback_url;
704
967
  if (typeof args.archived === 'boolean') body.archived = args.archived;
705
968
  result = await api(`/api/campaigns/${Number(args.campaign_id)}`, { method: 'PATCH', body: JSON.stringify(body) });
706
969
  break;
@@ -721,6 +984,16 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
721
984
  });
722
985
  break;
723
986
  }
987
+ case 'get_campaign_flow': {
988
+ result = await api(`/api/campaigns/${Number(args.campaign_id)}/flow`);
989
+ break;
990
+ }
991
+ case 'set_campaign_flow': {
992
+ result = await api(`/api/campaigns/${Number(args.campaign_id)}/flow`, {
993
+ method: 'PUT', body: JSON.stringify({ nodes: args.nodes, edges: args.edges }),
994
+ });
995
+ break;
996
+ }
724
997
  case 'get_campaign_stats': {
725
998
  const days = args?.days ? `?days=${Number(args.days)}` : '';
726
999
  result = await api(`/api/campaigns/${Number(args.campaign_id)}/stats${days}`);
@@ -766,6 +1039,25 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
766
1039
  result = await api(`/api/properties/${Number(args.property_id)}/stats${days}`);
767
1040
  break;
768
1041
  }
1042
+ case 'list_property_pages': {
1043
+ result = await api('/api/property-pages');
1044
+ break;
1045
+ }
1046
+
1047
+ case 'get_tracker_overview': {
1048
+ const days = args?.days ? `?days=${Number(args.days)}` : '';
1049
+ result = await api(`/api/tracker/overview${days}`);
1050
+ break;
1051
+ }
1052
+ case 'get_tracker_events': {
1053
+ const qs = new URLSearchParams();
1054
+ for (const k of ['days', 'q', 'stream', 'event', 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'sort', 'dir', 'limit', 'offset']) {
1055
+ if (args?.[k] !== undefined && args[k] !== null && args[k] !== '') qs.set(k, String(args[k]));
1056
+ }
1057
+ const q = qs.toString();
1058
+ result = await api(`/api/tracker/events${q ? `?${q}` : ''}`);
1059
+ break;
1060
+ }
769
1061
 
770
1062
  default:
771
1063
  throw new Error(`unknown tool: ${name}`);