@buygolinks/mcp 0.17.1 → 0.21.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.
package/README.md CHANGED
@@ -64,6 +64,7 @@ Most MCP clients use the same shape. Point them at the `npx -y @buygolinks/mcp`
64
64
  |---|---|
65
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 destination URL, title, source label, custom domain, password, archive state, or custom alias (e.g. `summer-sale` → `https://click.example.com/summer-sale`). Repointing the destination preserves the short code, alias, and click history. |
67
+ | `refresh_link_image` | Re-attempt fetching a link's product/preview image (Amazon PDP scrape for `/dp/` links, Open Graph otherwise). Use when a link has no thumbnail or a stale one. Returns `{ found: false }` if nothing could be fetched this time. |
67
68
  | `list_links` | List your links with click counts. |
68
69
  | `get_link_stats` | Per-link analytics (by day, country, device, browser, hour, weekday). |
69
70
  | `get_overview_stats` | Account-wide analytics. |
@@ -71,12 +72,12 @@ Most MCP clients use the same shape. Point them at the `npx -y @buygolinks/mcp`
71
72
  | `create_domain` | Register a new branded domain (e.g. `go.acme.com`). Returns the CNAME target. |
72
73
  | `verify_domain` | Force a fresh Cloudflare status check after DNS is added. |
73
74
  | `delete_domain` | Remove a branded domain. |
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
+ | `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. Set `amazon_tag` (e.g. `systemway-20`) on an Amazon network to auto-tag every Amazon link with `?tag=…` at click time. |
75
76
  | `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
77
  | `list_landers` / `create_lander` / `update_lander` / `delete_lander` | Manage landing pages. Lander URLs may contain `{clickid}` tokens which the resolver expands. |
77
78
  | `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
79
  | `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` / `duplicate_campaign` | Manage campaigns. `duplicate_campaign` deep-copies a campaign (settings + full routing flow + offer/lander pools) into a fresh short link named `COPY - {name}` (`COPY (1) - …` when copying a copy); stats and the unique direct-link slug are not carried over. 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`) — the single, campaign-agnostic lander CTA link (same on every campaign, so one lander serves many) that always resolves to an offer (never a dead page); the campaign is read from the visitor's cookie or the ad's `?utm_campaign={direct_slug}` forwarded onto the CTA. Set `direct_slug` to run traffic straight to a lander with no tracker hop (`?utm_campaign={direct_slug}` resolves the campaign at `/click` and stitches via the property tag); `fallback_url` is the never-dead-end guarantee. |
80
+ | `list_campaigns` / `create_campaign` / `get_campaign` / `update_campaign` / `delete_campaign` / `duplicate_campaign` | Manage campaigns. `duplicate_campaign` deep-copies a campaign (settings + full routing flow + offer/lander pools) into a fresh short link named `COPY - {name}` (`COPY (1) - …` when copying a copy); stats and the unique direct-link slug are not carried over. 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 the pixel's CAPI token** when the campaign converts (a pixel with no CAPI token can't fire back). `report_value_mode` controls the value reported back — `payout` (real), `fixed` (a set number), or `adjusted` (payout − `report_value_adjust_pct`% − `report_value_adjust_flat`) so you can withhold your margin. Each campaign also returns a `click_url` (`…/click`) — the single, campaign-agnostic lander CTA link (same on every campaign, so one lander serves many) that always resolves to an offer (never a dead page); the campaign is read from the visitor's cookie or the ad's `?utm_campaign={direct_slug}` forwarded onto the CTA. Set `direct_slug` to run traffic straight to a lander with no tracker hop (`?utm_campaign={direct_slug}` resolves the campaign at `/click` and stitches via the property tag); `fallback_url` is the never-dead-end guarantee. |
80
81
  | `set_campaign_offers` / `set_campaign_landers` | Replace the campaign's offer/lander split. Items are `{ offer_id, weight }` / `{ lander_id, weight }`. |
81
82
  | `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. |
82
83
  | `get_campaign_stats` | Full campaign report: visits, lander views, offer clicks, conversions, revenue, cost, profit, CTR/CR/EPC/ROI, breakdowns, recent events. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buygolinks/mcp",
3
- "version": "0.17.1",
3
+ "version": "0.21.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
@@ -91,6 +91,18 @@ const TOOLS = [
91
91
  required: ['link_id'],
92
92
  },
93
93
  },
94
+ {
95
+ name: 'refresh_link_image',
96
+ description:
97
+ 'Re-attempt fetching the product/preview image for an existing short link. Re-runs the same best-effort scrape used at link creation — the Amazon product-image scraper for Amazon /dp/ links, or a generic Open Graph (og:image / twitter:image) scrape for any other URL — and rehosts the result on buygolinks storage. Use this when a link was created without an image (the thumbnail shows a letter/placeholder) or to update a stale image. Returns { found: true, link } with the updated `product_image_url` on success, or { found: false } if no image could be fetched this time (the existing image, if any, is left untouched — try again later).',
98
+ inputSchema: {
99
+ type: 'object',
100
+ properties: {
101
+ link_id: { type: 'number', description: 'The numeric id of the link (from list_links / create_link).' },
102
+ },
103
+ required: ['link_id'],
104
+ },
105
+ },
94
106
  {
95
107
  name: 'list_links',
96
108
  description:
@@ -194,7 +206,7 @@ const TOOLS = [
194
206
  {
195
207
  name: 'create_affiliate_network',
196
208
  description:
197
- 'Add a new affiliate network. The server mints a per-network postback URL (https://buygolinks.com/postback/n/{token}) the user pastes into the network. The postback accepts ?{conversion_param}={clickid}&{payout_param}={payout}&{currency_param}={currency}&{txid_param}={txid} — payout, currency, and txid are optional. Returns the network row including postback_url.',
209
+ 'Add a new affiliate network. The server mints a per-network postback URL (https://buygolinks.com/postback/n/{token}) the user pastes into the network. The postback accepts ?{conversion_param}={clickid}&{payout_param}={payout}&{currency_param}={currency}&{txid_param}={txid} — payout, currency, and txid are optional. Returns the network row including postback_url. For Amazon Associates, set amazon_tag (e.g. "systemway-20"): every Amazon destination link the user adds is then auto-tagged with ?tag={amazon_tag} at click time (a tag already on the URL always wins).',
198
210
  inputSchema: {
199
211
  type: 'object',
200
212
  properties: {
@@ -203,13 +215,14 @@ const TOOLS = [
203
215
  payout_param: { type: 'string', description: 'What the network calls the payout amount (default "payout").' },
204
216
  currency_param: { type: 'string', description: 'What the network calls the currency code (default "currency").' },
205
217
  txid_param: { type: 'string', description: 'What the network calls the transaction/order id in its postback (default "txid"). Stored on the conversion for reconciliation/dedupe.' },
218
+ amazon_tag: { type: 'string', description: 'Amazon Associates tracking tag (e.g. "systemway-20"). When set, Amazon links are auto-converted to affiliate links by appending ?tag={amazon_tag} on each click.' },
206
219
  },
207
220
  required: ['name'],
208
221
  },
209
222
  },
210
223
  {
211
224
  name: 'update_affiliate_network',
212
- description: 'Update an affiliate network (rename or change postback param names). Pass archived=true to soft-archive.',
225
+ description: 'Update an affiliate network (rename, change postback param names, or set the Amazon Associates amazon_tag — pass an empty string to clear it). Pass archived=true to soft-archive.',
213
226
  inputSchema: {
214
227
  type: 'object',
215
228
  properties: {
@@ -219,6 +232,7 @@ const TOOLS = [
219
232
  payout_param: { type: 'string' },
220
233
  currency_param: { type: 'string' },
221
234
  txid_param: { type: 'string' },
235
+ amazon_tag: { type: 'string', description: 'Amazon Associates tracking tag (e.g. "systemway-20"); empty string clears it.' },
222
236
  archived: { type: 'boolean' },
223
237
  },
224
238
  required: ['network_id'],
@@ -446,6 +460,10 @@ const TOOLS = [
446
460
  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.' },
447
461
  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.' },
448
462
  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.' },
463
+ report_value_mode: { type: 'string', enum: ['payout', 'fixed', 'adjusted'], description: 'What revenue VALUE to report back to the ad platform on a CAPI conversion. "payout" (default) = the real conversion payout; "fixed" = always report report_value_fixed (e.g. 1); "adjusted" = real payout minus report_value_adjust_pct % then minus report_value_adjust_flat (so the user\'s margin/cost is withheld from what the platform optimizes against). Floored at 0.' },
464
+ report_value_fixed: { type: ['number', 'null'], description: 'Used when report_value_mode = "fixed": the exact value (USD) reported on every conversion regardless of real payout.' },
465
+ report_value_adjust_pct: { type: ['number', 'null'], description: 'Used when report_value_mode = "adjusted": percentage (0–100) subtracted from the real payout first (e.g. 10 reports payout − 10%).' },
466
+ report_value_adjust_flat: { type: ['number', 'null'], description: 'Used when report_value_mode = "adjusted": flat amount (USD) subtracted after the percentage.' },
449
467
  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.' },
450
468
  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.' },
451
469
  offer_ids: { type: 'array', items: { type: 'number' } },
@@ -479,6 +497,10 @@ const TOOLS = [
479
497
  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.' },
480
498
  conversion_pixel_id: { type: ['number', 'null'], description: 'Id of a tracking pixel to fire on conversion, or null to clear.' },
481
499
  conversion_event: { type: 'string', description: 'Event the conversion pixel fires (must be valid for that pixel\'s platform).' },
500
+ report_value_mode: { type: 'string', enum: ['payout', 'fixed', 'adjusted'], description: 'What revenue value to report back to the ad platform on conversion: "payout" (real), "fixed" (report_value_fixed), or "adjusted" (real payout − report_value_adjust_pct% − report_value_adjust_flat, floored at 0).' },
501
+ report_value_fixed: { type: ['number', 'null'], description: 'Fixed value (USD) reported on every conversion when report_value_mode = "fixed". Null to clear.' },
502
+ report_value_adjust_pct: { type: ['number', 'null'], description: 'Percentage (0–100) subtracted from real payout when report_value_mode = "adjusted". Null to clear.' },
503
+ report_value_adjust_flat: { type: ['number', 'null'], description: 'Flat USD amount subtracted (after the %) when report_value_mode = "adjusted". Null to clear.' },
482
504
  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.' },
483
505
  fallback_url: { type: ['string', 'null'], description: 'Never-dead-end fallback URL when no live offer exists, or null to use the platform default.' },
484
506
  archived: { type: 'boolean' },
@@ -680,6 +702,12 @@ const TOOLS = [
680
702
  '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.',
681
703
  inputSchema: { type: 'object', properties: {} },
682
704
  },
705
+ {
706
+ name: 'list_observed_campaign_tags',
707
+ description:
708
+ 'List the distinct `utm_campaign` values that have actually been SEEN arriving on the user\'s properties/visits over the last 90 days, most-active first. Each entry is `{ utm_campaign, events, utm_source, utm_medium, lander_url, claimed_by }` where `claimed_by` is `{ id, name }` of the BGL campaign whose `direct_slug` already owns that tag, or null if unclaimed. Use this to WIRE UP AN ALREADY-RUNNING DIRECT-LINK CAMPAIGN: when an ad on FB/Google points straight at a lander (no tracker hop) with `?utm_campaign={tag}`, that tag shows up here once the universal property tag reports a page view. Pick an unclaimed tag and set it as a campaign\'s `direct_slug` (via `update_campaign`) so BGL attributes that traffic — and its `/click` CTA — to the right campaign + offer. Skip tags whose `claimed_by` is non-null (another campaign already owns them).',
709
+ inputSchema: { type: 'object', properties: {} },
710
+ },
683
711
  {
684
712
  name: 'get_tracker_overview',
685
713
  description:
@@ -760,6 +788,12 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
760
788
  });
761
789
  break;
762
790
  }
791
+ case 'refresh_link_image': {
792
+ result = await api(`/api/links/${Number(args.link_id)}/refresh-image`, {
793
+ method: 'POST',
794
+ });
795
+ break;
796
+ }
763
797
  case 'list_links': {
764
798
  const qs = args?.include_archived ? '?include_archived=1' : '';
765
799
  result = await api(`/api/links${qs}`);
@@ -806,6 +840,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
806
840
  if (typeof args.payout_param === 'string') body.payout_param = args.payout_param;
807
841
  if (typeof args.currency_param === 'string') body.currency_param = args.currency_param;
808
842
  if (typeof args.txid_param === 'string') body.txid_param = args.txid_param;
843
+ if (typeof args.amazon_tag === 'string') body.amazon_tag = args.amazon_tag;
809
844
  result = await api('/api/affiliate-networks', { method: 'POST', body: JSON.stringify(body) });
810
845
  break;
811
846
  }
@@ -816,6 +851,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
816
851
  if (typeof args.payout_param === 'string') body.payout_param = args.payout_param;
817
852
  if (typeof args.currency_param === 'string') body.currency_param = args.currency_param;
818
853
  if (typeof args.txid_param === 'string') body.txid_param = args.txid_param;
854
+ if (typeof args.amazon_tag === 'string') body.amazon_tag = args.amazon_tag;
819
855
  if (typeof args.archived === 'boolean') body.archived = args.archived;
820
856
  result = await api(`/api/affiliate-networks/${Number(args.network_id)}`, { method: 'PATCH', body: JSON.stringify(body) });
821
857
  break;
@@ -950,6 +986,10 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
950
986
  if ('domain_id' in args) body.domain_id = args.domain_id;
951
987
  if ('traffic_source_id' in args) body.traffic_source_id = args.traffic_source_id;
952
988
  if ('conversion_pixel_id' in args) body.conversion_pixel_id = args.conversion_pixel_id;
989
+ if (typeof args.report_value_mode === 'string') body.report_value_mode = args.report_value_mode;
990
+ if ('report_value_fixed' in args) body.report_value_fixed = args.report_value_fixed;
991
+ if ('report_value_adjust_pct' in args) body.report_value_adjust_pct = args.report_value_adjust_pct;
992
+ if ('report_value_adjust_flat' in args) body.report_value_adjust_flat = args.report_value_adjust_flat;
953
993
  if ('direct_slug' in args) body.direct_slug = args.direct_slug;
954
994
  if ('fallback_url' in args) body.fallback_url = args.fallback_url;
955
995
  if (Array.isArray(args.offer_ids)) body.offer_ids = args.offer_ids;
@@ -973,6 +1013,10 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
973
1013
  if (typeof args.cost_value === 'number') body.cost_value = args.cost_value;
974
1014
  if ('domain_id' in args) body.domain_id = args.domain_id;
975
1015
  if ('conversion_pixel_id' in args) body.conversion_pixel_id = args.conversion_pixel_id;
1016
+ if (typeof args.report_value_mode === 'string') body.report_value_mode = args.report_value_mode;
1017
+ if ('report_value_fixed' in args) body.report_value_fixed = args.report_value_fixed;
1018
+ if ('report_value_adjust_pct' in args) body.report_value_adjust_pct = args.report_value_adjust_pct;
1019
+ if ('report_value_adjust_flat' in args) body.report_value_adjust_flat = args.report_value_adjust_flat;
976
1020
  if ('direct_slug' in args) body.direct_slug = args.direct_slug;
977
1021
  if ('fallback_url' in args) body.fallback_url = args.fallback_url;
978
1022
  if (typeof args.archived === 'boolean') body.archived = args.archived;
@@ -1058,6 +1102,10 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1058
1102
  result = await api('/api/property-pages');
1059
1103
  break;
1060
1104
  }
1105
+ case 'list_observed_campaign_tags': {
1106
+ result = await api('/api/observed-campaign-tags');
1107
+ break;
1108
+ }
1061
1109
 
1062
1110
  case 'get_tracker_overview': {
1063
1111
  const days = args?.days ? `?days=${Number(args.days)}` : '';