@buygolinks/mcp 0.16.0 → 0.17.1

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
@@ -63,7 +63,7 @@ Most MCP clients use the same shape. Point them at the `npx -y @buygolinks/mcp`
63
63
  | Tool | Description |
64
64
  |---|---|
65
65
  | `create_link` | Create a short link. The destination URL is passed through untouched (include your own affiliate tag). |
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`). |
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
67
  | `list_links` | List your links with click counts. |
68
68
  | `get_link_stats` | Per-link analytics (by day, country, device, browser, hour, weekday). |
69
69
  | `get_overview_stats` | Account-wide analytics. |
@@ -76,7 +76,7 @@ Most MCP clients use the same shape. Point them at the `npx -y @buygolinks/mcp`
76
76
  | `list_landers` / `create_lander` / `update_lander` / `delete_lander` | Manage landing pages. Lander URLs may contain `{clickid}` tokens which the resolver expands. |
77
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
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. |
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
80
  | `set_campaign_offers` / `set_campaign_landers` | Replace the campaign's offer/lander split. Items are `{ offer_id, weight }` / `{ lander_id, weight }`. |
81
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. |
82
82
  | `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.16.0",
3
+ "version": "0.17.1",
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
@@ -75,11 +75,12 @@ const TOOLS = [
75
75
  {
76
76
  name: 'update_link',
77
77
  description:
78
- 'Update an existing short link. Supports renaming (title), changing the campaign source label, swapping the branded domain it resolves under (domain_id), setting a memorable custom alias (alias), archiving/unarchiving, and password-protecting the link. Pass `alias` to give the link a memorable URL slug on its custom domain (e.g. alias="summer-sale" → https://click.example.com/summer-sale). Aliases require the link to be on a custom domain. Setting `alias` REPLACES any previous alias — the old alias stops resolving immediately and is freed for any other link to claim. The link\'s original random short code (link.code) keeps working forever regardless of alias changes. Pass alias="" or alias=null to remove the alias. Changing the link\'s `domain_id` clears the alias automatically. To set or change the password, pass `password` as a non-empty string. To remove password protection, pass an empty string for `password`. To move the link back to the platform default (buygolinks.com), pass `domain_id: null`. Returns the updated link row including `short_domain`, `alias`, and `password_protected` (boolean — the plaintext is never returned).',
78
+ 'Update an existing short link. Supports changing where it redirects (destination_url), renaming (title), changing the campaign source label, swapping the branded domain it resolves under (domain_id), setting a memorable custom alias (alias), archiving/unarchiving, and password-protecting the link. Pass `destination_url` to repoint the link at a new target — the short code, alias, and click history are preserved; only the redirect destination moves (the ASIN is re-extracted automatically for Amazon /dp/ URLs). Pass `alias` to give the link a memorable URL slug on its custom domain (e.g. alias="summer-sale" → https://click.example.com/summer-sale). Aliases require the link to be on a custom domain. Setting `alias` REPLACES any previous alias — the old alias stops resolving immediately and is freed for any other link to claim. The link\'s original random short code (link.code) keeps working forever regardless of alias changes. Pass alias="" or alias=null to remove the alias. Changing the link\'s `domain_id` clears the alias automatically. To set or change the password, pass `password` as a non-empty string. To remove password protection, pass an empty string for `password`. To move the link back to the platform default (buygolinks.com), pass `domain_id: null`. Returns the updated link row including `short_domain`, `alias`, and `password_protected` (boolean — the plaintext is never returned).',
79
79
  inputSchema: {
80
80
  type: 'object',
81
81
  properties: {
82
82
  link_id: { type: 'number', description: 'The numeric id of the link (from list_links / create_link).' },
83
+ destination_url: { type: 'string', description: 'New destination URL the short link redirects to (must be http(s)). The short code, alias, and click history are preserved.' },
83
84
  title: { type: 'string', description: 'New display title.' },
84
85
  source_label: { type: 'string', description: 'New source label (campaign tag).' },
85
86
  archived: { type: 'boolean', description: 'Set true to archive the link, false to unarchive.' },
@@ -193,7 +194,7 @@ const TOOLS = [
193
194
  {
194
195
  name: 'create_affiliate_network',
195
196
  description:
196
- '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. Returns the network row including postback_url.',
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.',
197
198
  inputSchema: {
198
199
  type: 'object',
199
200
  properties: {
@@ -201,6 +202,7 @@ const TOOLS = [
201
202
  conversion_param: { type: 'string', description: 'What the network calls the click id in its postback (default "clickid").' },
202
203
  payout_param: { type: 'string', description: 'What the network calls the payout amount (default "payout").' },
203
204
  currency_param: { type: 'string', description: 'What the network calls the currency code (default "currency").' },
205
+ 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.' },
204
206
  },
205
207
  required: ['name'],
206
208
  },
@@ -216,6 +218,7 @@ const TOOLS = [
216
218
  conversion_param: { type: 'string' },
217
219
  payout_param: { type: 'string' },
218
220
  currency_param: { type: 'string' },
221
+ txid_param: { type: 'string' },
219
222
  archived: { type: 'boolean' },
220
223
  },
221
224
  required: ['network_id'],
@@ -428,7 +431,7 @@ const TOOLS = [
428
431
  {
429
432
  name: 'create_campaign',
430
433
  description:
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.',
434
+ '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`) — the single, campaign-agnostic DEFINED lander click URL the user pastes into their lander\'s CTA button. It\'s the same on every campaign, so one lander can be reused across many; the campaign is resolved at click time from the visitor\'s `bgl_clk_{code}` cookie (short-link traffic) or the ad\'s `?utm_campaign={direct_slug}` that the lander forwards onto the CTA (direct-link). 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`, put `?utm_campaign={direct_slug}` on the ad URL, and have the lander forward that utm_campaign onto the CTA — `/click` resolves the campaign from it and the property tag stitches the page view too. `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.',
432
435
  inputSchema: {
433
436
  type: 'object',
434
437
  properties: {
@@ -488,6 +491,11 @@ const TOOLS = [
488
491
  description: 'Delete a campaign. The short link stops resolving immediately. Tracker events for this campaign are also deleted (cascade).',
489
492
  inputSchema: { type: 'object', properties: { campaign_id: { type: 'number' } }, required: ['campaign_id'] },
490
493
  },
494
+ {
495
+ name: 'duplicate_campaign',
496
+ description: 'Deep-copy a campaign: clones the campaign settings plus its full routing flow (source → landers → offers graph and split-test weights) and its offer/lander pools. The copy gets a fresh short link + postback token and a name like "COPY - {original}" (or "COPY (1) - {original}", "COPY (2) - …" when copying a copy). Stats are NOT copied; the direct-link slug is left blank (it must be unique). Returns the new campaign, including its short_url/click_url. Useful for spinning up a new geo/angle variant of a working campaign without rebuilding the flow.',
497
+ inputSchema: { type: 'object', properties: { campaign_id: { type: 'number' } }, required: ['campaign_id'] },
498
+ },
491
499
  {
492
500
  name: 'set_campaign_offers',
493
501
  description: 'Replace the campaign\'s offer split with the supplied items. Each item is { offer_id, weight }. Weight is an integer; higher = more traffic. To direct all traffic to one offer use weight=100 for that offer and don\'t include the others.',
@@ -735,6 +743,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
735
743
  }
736
744
  case 'update_link': {
737
745
  const body = {};
746
+ if (typeof args.destination_url === 'string') body.destination_url = args.destination_url;
738
747
  if (typeof args.title === 'string') body.title = args.title;
739
748
  if (typeof args.source_label === 'string') body.source_label = args.source_label;
740
749
  if (typeof args.archived === 'boolean') body.archived = args.archived;
@@ -796,6 +805,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
796
805
  if (typeof args.conversion_param === 'string') body.conversion_param = args.conversion_param;
797
806
  if (typeof args.payout_param === 'string') body.payout_param = args.payout_param;
798
807
  if (typeof args.currency_param === 'string') body.currency_param = args.currency_param;
808
+ if (typeof args.txid_param === 'string') body.txid_param = args.txid_param;
799
809
  result = await api('/api/affiliate-networks', { method: 'POST', body: JSON.stringify(body) });
800
810
  break;
801
811
  }
@@ -805,6 +815,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
805
815
  if (typeof args.conversion_param === 'string') body.conversion_param = args.conversion_param;
806
816
  if (typeof args.payout_param === 'string') body.payout_param = args.payout_param;
807
817
  if (typeof args.currency_param === 'string') body.currency_param = args.currency_param;
818
+ if (typeof args.txid_param === 'string') body.txid_param = args.txid_param;
808
819
  if (typeof args.archived === 'boolean') body.archived = args.archived;
809
820
  result = await api(`/api/affiliate-networks/${Number(args.network_id)}`, { method: 'PATCH', body: JSON.stringify(body) });
810
821
  break;
@@ -972,6 +983,10 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
972
983
  result = await api(`/api/campaigns/${Number(args.campaign_id)}`, { method: 'DELETE' });
973
984
  break;
974
985
  }
986
+ case 'duplicate_campaign': {
987
+ result = await api(`/api/campaigns/${Number(args.campaign_id)}/duplicate`, { method: 'POST' });
988
+ break;
989
+ }
975
990
  case 'set_campaign_offers': {
976
991
  result = await api(`/api/campaigns/${Number(args.campaign_id)}/offers`, {
977
992
  method: 'PUT', body: JSON.stringify({ items: args.items }),