@buygolinks/mcp 0.1.1 → 0.8.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 +14 -0
  2. package/package.json +1 -1
  3. package/src/index.js +615 -7
package/README.md CHANGED
@@ -63,9 +63,23 @@ 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. Amazon URLs auto-detect ASIN and trigger geo-routing. |
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
67
  | `list_links` | List your links with click counts. |
67
68
  | `get_link_stats` | Per-link analytics (by day, country, device, browser, hour, weekday). |
68
69
  | `get_overview_stats` | Account-wide analytics. |
70
+ | `list_domains` | List your branded custom short-link domains with provisioning + SSL status. |
71
+ | `create_domain` | Register a new branded domain (e.g. `go.acme.com`). Returns the CNAME target. |
72
+ | `verify_domain` | Force a fresh Cloudflare status check after DNS is added. |
73
+ | `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_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
+ | `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. |
78
+ | `set_campaign_offers` / `set_campaign_landers` | Replace the campaign's offer/lander split. Items are `{ offer_id, weight }` / `{ lander_id, weight }`. |
79
+ | `get_campaign_stats` | Full campaign report: visits, lander views, offer clicks, conversions, revenue, cost, profit, CTR/CR/EPC/ROI, breakdowns, recent events. |
80
+ | `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` returns the install `snippet`. |
81
+ | `set_property_campaigns` | Link/unlink campaigns to a property (many-to-many, for the source split in reports). Items are `{ campaign_id }`. |
82
+ | `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. |
69
83
 
70
84
  ## Example prompts
71
85
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buygolinks/mcp",
3
- "version": "0.1.1",
3
+ "version": "0.8.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. Returns the short URL and link metadata.',
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).',
52
52
  inputSchema: {
53
53
  type: 'object',
54
54
  properties: {
@@ -64,10 +64,32 @@ const TOOLS = [
64
64
  type: 'string',
65
65
  description: 'Optional source label (campaign tag) — appears in ascsubtag on Amazon redirects for attribution.',
66
66
  },
67
+ domain_id: {
68
+ type: 'number',
69
+ description: 'Optional id of one of the user\'s active custom domains (from list_domains). If omitted, defaults to the user\'s first active custom domain, or buygolinks.com if they have none.',
70
+ },
67
71
  },
68
72
  required: ['destination_url'],
69
73
  },
70
74
  },
75
+ {
76
+ name: 'update_link',
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).',
79
+ inputSchema: {
80
+ type: 'object',
81
+ properties: {
82
+ link_id: { type: 'number', description: 'The numeric id of the link (from list_links / create_link).' },
83
+ title: { type: 'string', description: 'New display title.' },
84
+ source_label: { type: 'string', description: 'New source label (campaign tag).' },
85
+ archived: { type: 'boolean', description: 'Set true to archive the link, false to unarchive.' },
86
+ password: { type: 'string', description: 'Password to protect the link with. Empty string clears any existing password. Max 200 chars.' },
87
+ domain_id: { type: ['number', 'null'], description: 'Id of one of the user\'s active custom domains, or null to move the link back to buygolinks.com.' },
88
+ alias: { type: ['string', 'null'], description: 'Custom path alias on the link\'s custom domain. 1–32 chars; letters, numbers, hyphens, underscores; must start with letter/number. Empty string or null removes the alias. Replacing an alias retires the old one immediately. Requires the link to be on a custom domain.' },
89
+ },
90
+ required: ['link_id'],
91
+ },
92
+ },
71
93
  {
72
94
  name: 'list_links',
73
95
  description:
@@ -115,10 +137,378 @@ const TOOLS = [
115
137
  },
116
138
  },
117
139
  },
140
+ {
141
+ name: 'list_domains',
142
+ description:
143
+ 'List the user\'s branded custom short-link domains (e.g. go.acme.com). Returns each domain with its provisioning status (pending DNS / active / failed), SSL status, and the CNAME target the user needs to point their hostname at. Also returns the plan tier and the number of domains the plan allows.',
144
+ inputSchema: { type: 'object', properties: {} },
145
+ },
146
+ {
147
+ name: 'create_domain',
148
+ description:
149
+ 'Register a new branded custom domain so short links can resolve at the user\'s own hostname instead of buygolinks.com. After creation, the user must add a CNAME record at their DNS provider pointing the hostname at the returned cname_target. SSL is auto-issued by Cloudflare once DNS resolves. Free plan does not allow custom domains.',
150
+ inputSchema: {
151
+ type: 'object',
152
+ properties: {
153
+ hostname: {
154
+ type: 'string',
155
+ description: 'The hostname to register, e.g. "go.acme.com". Must be a valid public hostname; the buygolinks.com apex and its subdomains are reserved.',
156
+ },
157
+ },
158
+ required: ['hostname'],
159
+ },
160
+ },
161
+ {
162
+ name: 'verify_domain',
163
+ description:
164
+ 'Force a re-check of a custom domain\'s status against Cloudflare. Use after the user has added the CNAME record to advance the domain from "pending" to "active". Returns the refreshed domain row.',
165
+ inputSchema: {
166
+ type: 'object',
167
+ properties: {
168
+ domain_id: { type: 'number', description: 'The numeric id of the domain (from list_domains / create_domain).' },
169
+ },
170
+ required: ['domain_id'],
171
+ },
172
+ },
173
+ {
174
+ name: 'delete_domain',
175
+ description:
176
+ 'Remove a custom domain. Deletes the Cloudflare hostname binding and the local record. Short links served via this domain stop resolving immediately.',
177
+ inputSchema: {
178
+ type: 'object',
179
+ properties: {
180
+ domain_id: { type: 'number', description: 'The numeric id of the domain to remove.' },
181
+ },
182
+ required: ['domain_id'],
183
+ },
184
+ },
185
+
186
+ // ─── Tracker: affiliate networks ────────────────────────────────────────
187
+ {
188
+ name: 'list_affiliate_networks',
189
+ description:
190
+ 'List the user\'s affiliate networks (ClickBank, Digistore24, MaxWeb, etc). Each network includes a postback_url the user pastes into the network\'s S2S settings so conversions flow back automatically. Networks also configure how the network names the click id and payout in postbacks (conversion_param, payout_param).',
191
+ inputSchema: { type: 'object', properties: {} },
192
+ },
193
+ {
194
+ name: 'create_affiliate_network',
195
+ 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
+ inputSchema: {
198
+ type: 'object',
199
+ properties: {
200
+ name: { type: 'string', description: 'Display name, e.g. "ClickBank".' },
201
+ conversion_param: { type: 'string', description: 'What the network calls the click id in its postback (default "clickid").' },
202
+ payout_param: { type: 'string', description: 'What the network calls the payout amount (default "payout").' },
203
+ currency_param: { type: 'string', description: 'What the network calls the currency code (default "currency").' },
204
+ },
205
+ required: ['name'],
206
+ },
207
+ },
208
+ {
209
+ name: 'update_affiliate_network',
210
+ description: 'Update an affiliate network (rename or change postback param names). Pass archived=true to soft-archive.',
211
+ inputSchema: {
212
+ type: 'object',
213
+ properties: {
214
+ network_id: { type: 'number' },
215
+ name: { type: 'string' },
216
+ conversion_param: { type: 'string' },
217
+ payout_param: { type: 'string' },
218
+ currency_param: { type: 'string' },
219
+ archived: { type: 'boolean' },
220
+ },
221
+ required: ['network_id'],
222
+ },
223
+ },
224
+ {
225
+ name: 'delete_affiliate_network',
226
+ description: 'Delete an affiliate network. Offers that referenced this network keep their URL but have affiliate_network_id set to NULL.',
227
+ inputSchema: {
228
+ type: 'object',
229
+ properties: { network_id: { type: 'number' } },
230
+ required: ['network_id'],
231
+ },
232
+ },
233
+
234
+ // ─── Tracker: offers ────────────────────────────────────────────────────
235
+ {
236
+ name: 'list_offers',
237
+ description: 'List all offers (destination URLs) owned by the user. Use these as the end-point of a campaign.',
238
+ inputSchema: { type: 'object', properties: {} },
239
+ },
240
+ {
241
+ name: 'create_offer',
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=…).',
244
+ inputSchema: {
245
+ type: 'object',
246
+ properties: {
247
+ name: { type: 'string' },
248
+ url: { type: 'string', description: 'Destination URL (http/https).' },
249
+ affiliate_network_id: { type: ['number', 'null'] },
250
+ payout: { type: 'number' },
251
+ payout_currency: { type: 'string', description: 'ISO 4217 currency, default USD.' },
252
+ country: { type: 'string', description: 'ISO2 country code (optional).' },
253
+ },
254
+ required: ['name', 'url'],
255
+ },
256
+ },
257
+ {
258
+ name: 'update_offer',
259
+ description: 'Update an offer\'s name, URL, payout, currency, country, or affiliate network. Pass archived=true to soft-archive.',
260
+ inputSchema: {
261
+ type: 'object',
262
+ properties: {
263
+ offer_id: { type: 'number' },
264
+ name: { type: 'string' },
265
+ url: { type: 'string' },
266
+ affiliate_network_id: { type: ['number', 'null'] },
267
+ payout: { type: 'number' },
268
+ payout_currency: { type: 'string' },
269
+ country: { type: 'string' },
270
+ archived: { type: 'boolean' },
271
+ },
272
+ required: ['offer_id'],
273
+ },
274
+ },
275
+ {
276
+ name: 'delete_offer',
277
+ description: 'Delete an offer. Removes it from any campaigns it was attached to.',
278
+ inputSchema: { type: 'object', properties: { offer_id: { type: 'number' } }, required: ['offer_id'] },
279
+ },
280
+
281
+ // ─── Tracker: landers ───────────────────────────────────────────────────
282
+ {
283
+ name: 'list_landers',
284
+ description: 'List the user\'s landing pages. Landers are optional intermediate pages between the short link and the offer.',
285
+ inputSchema: { type: 'object', properties: {} },
286
+ },
287
+ {
288
+ name: 'create_lander',
289
+ description:
290
+ 'Create a lander (the user\'s own landing page that visitors hit between the short link and the offer). The URL may contain {clickid} tokens which the resolver expands. Install the campaign\'s pixel snippet on the lander to get impressions, time-on-page, scroll depth, and outbound click tracking.',
291
+ inputSchema: {
292
+ type: 'object',
293
+ properties: {
294
+ name: { type: 'string' },
295
+ url: { type: 'string', description: 'Lander URL. May contain {clickid} which the resolver will replace.' },
296
+ country: { type: 'string' },
297
+ },
298
+ required: ['name', 'url'],
299
+ },
300
+ },
301
+ {
302
+ name: 'update_lander',
303
+ description: 'Update a lander\'s name, URL, or country. Pass archived=true to soft-archive.',
304
+ inputSchema: {
305
+ type: 'object',
306
+ properties: {
307
+ lander_id: { type: 'number' },
308
+ name: { type: 'string' },
309
+ url: { type: 'string' },
310
+ country: { type: 'string' },
311
+ archived: { type: 'boolean' },
312
+ },
313
+ required: ['lander_id'],
314
+ },
315
+ },
316
+ {
317
+ name: 'delete_lander',
318
+ description: 'Delete a lander. Removes it from any campaigns it was attached to.',
319
+ inputSchema: { type: 'object', properties: { lander_id: { type: 'number' } }, required: ['lander_id'] },
320
+ },
321
+
322
+ // ─── Tracker: campaigns ─────────────────────────────────────────────────
323
+ {
324
+ name: 'list_campaigns',
325
+ description:
326
+ 'List the user\'s campaigns with quick-look stats (visits / offer_clicks / conversions / revenue). Each campaign has a short_url to paste into traffic sources, a postback_url for the affiliate network, and a pixel_snippet to install on landers.',
327
+ inputSchema: { type: 'object', properties: {} },
328
+ },
329
+ {
330
+ name: 'create_campaign',
331
+ 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.',
333
+ inputSchema: {
334
+ type: 'object',
335
+ properties: {
336
+ name: { type: 'string' },
337
+ traffic_source_label: { type: 'string', description: 'Free text, e.g. "Facebook", "TikTok".' },
338
+ country: { type: 'string', description: 'ISO2 default geo.' },
339
+ cost_model: { type: 'string', enum: ['not_tracked', 'cpc', 'cpm'] },
340
+ cost_value: { type: 'number' },
341
+ transition_mode: { type: 'string', enum: ['302', 'meta_refresh'] },
342
+ 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.' },
343
+ offer_ids: { type: 'array', items: { type: 'number' } },
344
+ offer_weights: { type: 'array', items: { type: 'number' } },
345
+ lander_ids: { type: 'array', items: { type: 'number' } },
346
+ lander_weights: { type: 'array', items: { type: 'number' } },
347
+ },
348
+ required: ['name'],
349
+ },
350
+ },
351
+ {
352
+ name: 'get_campaign',
353
+ description: 'Get a single campaign with its attached offers, landers, postback URL, short URL, and pixel snippet.',
354
+ inputSchema: { type: 'object', properties: { campaign_id: { type: 'number' } }, required: ['campaign_id'] },
355
+ },
356
+ {
357
+ 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.',
359
+ inputSchema: {
360
+ type: 'object',
361
+ properties: {
362
+ campaign_id: { type: 'number' },
363
+ name: { type: 'string' },
364
+ traffic_source_label: { type: 'string' },
365
+ country: { type: 'string' },
366
+ cost_model: { type: 'string', enum: ['not_tracked', 'cpc', 'cpm'] },
367
+ cost_value: { type: 'number' },
368
+ transition_mode: { type: 'string', enum: ['302', 'meta_refresh'] },
369
+ 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.' },
370
+ archived: { type: 'boolean' },
371
+ },
372
+ required: ['campaign_id'],
373
+ },
374
+ },
375
+ {
376
+ name: 'delete_campaign',
377
+ description: 'Delete a campaign. The short link stops resolving immediately. Tracker events for this campaign are also deleted (cascade).',
378
+ inputSchema: { type: 'object', properties: { campaign_id: { type: 'number' } }, required: ['campaign_id'] },
379
+ },
380
+ {
381
+ name: 'set_campaign_offers',
382
+ 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.',
383
+ inputSchema: {
384
+ type: 'object',
385
+ properties: {
386
+ campaign_id: { type: 'number' },
387
+ items: {
388
+ type: 'array',
389
+ items: {
390
+ type: 'object',
391
+ properties: { offer_id: { type: 'number' }, weight: { type: 'number' } },
392
+ required: ['offer_id', 'weight'],
393
+ },
394
+ },
395
+ },
396
+ required: ['campaign_id', 'items'],
397
+ },
398
+ },
399
+ {
400
+ name: 'set_campaign_landers',
401
+ description: 'Replace the campaign\'s lander split. Pass empty items to remove all landers (clicks then go direct to offer). Each item is { lander_id, weight }.',
402
+ inputSchema: {
403
+ type: 'object',
404
+ properties: {
405
+ campaign_id: { type: 'number' },
406
+ items: {
407
+ type: 'array',
408
+ items: {
409
+ type: 'object',
410
+ properties: { lander_id: { type: 'number' }, weight: { type: 'number' } },
411
+ required: ['lander_id', 'weight'],
412
+ },
413
+ },
414
+ },
415
+ required: ['campaign_id', 'items'],
416
+ },
417
+ },
418
+ {
419
+ name: 'get_campaign_stats',
420
+ description:
421
+ 'Get a campaign\'s full report: totals (visits / unique / lander_views / offer_clicks / conversions / revenue / cost / profit / CTR / CR / EPC / ROI), by_day / by_country / by_device / by_browser / by_referrer / by_hour, per-offer and per-lander performance, and the 50 most recent events.',
422
+ inputSchema: {
423
+ type: 'object',
424
+ properties: {
425
+ campaign_id: { type: 'number' },
426
+ days: { type: 'number', description: 'Window size in days (1–90). Defaults to 14.' },
427
+ },
428
+ required: ['campaign_id'],
429
+ },
430
+ },
431
+
432
+ // ─── Tracker: properties (GTM/GA-style site containers) ─────────────────
433
+ {
434
+ name: 'list_properties',
435
+ 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.',
437
+ inputSchema: { type: 'object', properties: {} },
438
+ },
439
+ {
440
+ name: 'create_property',
441
+ description:
442
+ 'Create a property (a GTM/GA-style site container). Returns the property with its public property_key and an install `snippet` — a single <script> tag the user pastes into the <head> of every page of their site. To record conversions, the site sets window.bglConversion = true (optionally window.bglRevenue) on a thank-you page, or calls window.bglFireConversion(revenue).',
443
+ inputSchema: {
444
+ type: 'object',
445
+ properties: {
446
+ name: { type: 'string' },
447
+ site_url: { type: 'string', description: 'Optional primary site URL, informational.' },
448
+ },
449
+ required: ['name'],
450
+ },
451
+ },
452
+ {
453
+ name: 'get_property',
454
+ description: 'Get a single property with its install snippet and the campaigns linked to it.',
455
+ inputSchema: { type: 'object', properties: { property_id: { type: 'number' } }, required: ['property_id'] },
456
+ },
457
+ {
458
+ name: 'update_property',
459
+ description: 'Update a property\'s name, site_url, or archive state.',
460
+ inputSchema: {
461
+ type: 'object',
462
+ properties: {
463
+ property_id: { type: 'number' },
464
+ name: { type: 'string' },
465
+ site_url: { type: ['string', 'null'] },
466
+ archived: { type: 'boolean' },
467
+ },
468
+ required: ['property_id'],
469
+ },
470
+ },
471
+ {
472
+ name: 'delete_property',
473
+ description: 'Delete a property. Its page events are also deleted (cascade). The install snippet stops reporting.',
474
+ inputSchema: { type: 'object', properties: { property_id: { type: 'number' } }, required: ['property_id'] },
475
+ },
476
+ {
477
+ name: 'set_property_campaigns',
478
+ description: 'Replace the set of campaigns linked to a property (many-to-many, for organisation and the source split in reports). Each item is { campaign_id }. Pass empty items to unlink all. Stitching still happens automatically by click regardless of explicit links.',
479
+ inputSchema: {
480
+ type: 'object',
481
+ properties: {
482
+ property_id: { type: 'number' },
483
+ items: {
484
+ type: 'array',
485
+ items: {
486
+ type: 'object',
487
+ properties: { campaign_id: { type: 'number' } },
488
+ required: ['campaign_id'],
489
+ },
490
+ },
491
+ },
492
+ required: ['property_id', 'items'],
493
+ },
494
+ },
495
+ {
496
+ name: 'get_property_stats',
497
+ description:
498
+ '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.',
499
+ inputSchema: {
500
+ type: 'object',
501
+ properties: {
502
+ property_id: { type: 'number' },
503
+ days: { type: 'number', description: 'Window size in days (1–90). Defaults to 14.' },
504
+ },
505
+ required: ['property_id'],
506
+ },
507
+ },
118
508
  ];
119
509
 
120
510
  const server = new Server(
121
- { name: 'buygolinks', version: '0.1.0' },
511
+ { name: 'buygolinks', version: '0.8.0' },
122
512
  { capabilities: { tools: {} } },
123
513
  );
124
514
 
@@ -130,13 +520,33 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
130
520
  let result;
131
521
  switch (name) {
132
522
  case 'create_link': {
523
+ const body = {
524
+ destination_url: args.destination_url,
525
+ title: args.title,
526
+ source_label: args.source_label,
527
+ };
528
+ if (typeof args.domain_id === 'number') body.domain_id = args.domain_id;
133
529
  result = await api('/api/links', {
134
530
  method: 'POST',
135
- body: JSON.stringify({
136
- destination_url: args.destination_url,
137
- title: args.title,
138
- source_label: args.source_label,
139
- }),
531
+ body: JSON.stringify(body),
532
+ });
533
+ break;
534
+ }
535
+ case 'update_link': {
536
+ const body = {};
537
+ if (typeof args.title === 'string') body.title = args.title;
538
+ if (typeof args.source_label === 'string') body.source_label = args.source_label;
539
+ if (typeof args.archived === 'boolean') body.archived = args.archived;
540
+ if (typeof args.password === 'string') body.password = args.password;
541
+ if (args.domain_id === null || typeof args.domain_id === 'number') {
542
+ body.domain_id = args.domain_id;
543
+ }
544
+ if (args.alias === null || typeof args.alias === 'string') {
545
+ body.alias = args.alias;
546
+ }
547
+ result = await api(`/api/links/${Number(args.link_id)}`, {
548
+ method: 'PATCH',
549
+ body: JSON.stringify(body),
140
550
  });
141
551
  break;
142
552
  }
@@ -155,6 +565,204 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
155
565
  result = await api(`/api/stats/overview${days}`);
156
566
  break;
157
567
  }
568
+ case 'list_domains': {
569
+ result = await api('/api/domains');
570
+ break;
571
+ }
572
+ case 'create_domain': {
573
+ result = await api('/api/domains', {
574
+ method: 'POST',
575
+ body: JSON.stringify({ hostname: args.hostname }),
576
+ });
577
+ break;
578
+ }
579
+ case 'verify_domain': {
580
+ result = await api(`/api/domains/${Number(args.domain_id)}/verify`, { method: 'POST' });
581
+ break;
582
+ }
583
+ case 'delete_domain': {
584
+ result = await api(`/api/domains/${Number(args.domain_id)}`, { method: 'DELETE' });
585
+ break;
586
+ }
587
+
588
+ // ── Tracker: networks ──
589
+ case 'list_affiliate_networks': {
590
+ result = await api('/api/affiliate-networks');
591
+ break;
592
+ }
593
+ case 'create_affiliate_network': {
594
+ const body = { name: args.name };
595
+ if (typeof args.conversion_param === 'string') body.conversion_param = args.conversion_param;
596
+ if (typeof args.payout_param === 'string') body.payout_param = args.payout_param;
597
+ if (typeof args.currency_param === 'string') body.currency_param = args.currency_param;
598
+ result = await api('/api/affiliate-networks', { method: 'POST', body: JSON.stringify(body) });
599
+ break;
600
+ }
601
+ case 'update_affiliate_network': {
602
+ const body = {};
603
+ if (typeof args.name === 'string') body.name = args.name;
604
+ if (typeof args.conversion_param === 'string') body.conversion_param = args.conversion_param;
605
+ if (typeof args.payout_param === 'string') body.payout_param = args.payout_param;
606
+ if (typeof args.currency_param === 'string') body.currency_param = args.currency_param;
607
+ if (typeof args.archived === 'boolean') body.archived = args.archived;
608
+ result = await api(`/api/affiliate-networks/${Number(args.network_id)}`, { method: 'PATCH', body: JSON.stringify(body) });
609
+ break;
610
+ }
611
+ case 'delete_affiliate_network': {
612
+ result = await api(`/api/affiliate-networks/${Number(args.network_id)}`, { method: 'DELETE' });
613
+ break;
614
+ }
615
+
616
+ // ── Tracker: offers ──
617
+ case 'list_offers': {
618
+ result = await api('/api/offers');
619
+ break;
620
+ }
621
+ case 'create_offer': {
622
+ const body = { name: args.name, url: args.url };
623
+ if ('affiliate_network_id' in args) body.affiliate_network_id = args.affiliate_network_id;
624
+ if (typeof args.payout === 'number') body.payout = args.payout;
625
+ if (typeof args.payout_currency === 'string') body.payout_currency = args.payout_currency;
626
+ if (typeof args.country === 'string') body.country = args.country;
627
+ result = await api('/api/offers', { method: 'POST', body: JSON.stringify(body) });
628
+ break;
629
+ }
630
+ case 'update_offer': {
631
+ const body = {};
632
+ if (typeof args.name === 'string') body.name = args.name;
633
+ if (typeof args.url === 'string') body.url = args.url;
634
+ if ('affiliate_network_id' in args) body.affiliate_network_id = args.affiliate_network_id;
635
+ if (typeof args.payout === 'number') body.payout = args.payout;
636
+ if (typeof args.payout_currency === 'string') body.payout_currency = args.payout_currency;
637
+ if (typeof args.country === 'string') body.country = args.country;
638
+ if (typeof args.archived === 'boolean') body.archived = args.archived;
639
+ result = await api(`/api/offers/${Number(args.offer_id)}`, { method: 'PATCH', body: JSON.stringify(body) });
640
+ break;
641
+ }
642
+ case 'delete_offer': {
643
+ result = await api(`/api/offers/${Number(args.offer_id)}`, { method: 'DELETE' });
644
+ break;
645
+ }
646
+
647
+ // ── Tracker: landers ──
648
+ case 'list_landers': {
649
+ result = await api('/api/landers');
650
+ break;
651
+ }
652
+ case 'create_lander': {
653
+ const body = { name: args.name, url: args.url };
654
+ if (typeof args.country === 'string') body.country = args.country;
655
+ result = await api('/api/landers', { method: 'POST', body: JSON.stringify(body) });
656
+ break;
657
+ }
658
+ case 'update_lander': {
659
+ const body = {};
660
+ if (typeof args.name === 'string') body.name = args.name;
661
+ if (typeof args.url === 'string') body.url = args.url;
662
+ if (typeof args.country === 'string') body.country = args.country;
663
+ if (typeof args.archived === 'boolean') body.archived = args.archived;
664
+ result = await api(`/api/landers/${Number(args.lander_id)}`, { method: 'PATCH', body: JSON.stringify(body) });
665
+ break;
666
+ }
667
+ case 'delete_lander': {
668
+ result = await api(`/api/landers/${Number(args.lander_id)}`, { method: 'DELETE' });
669
+ break;
670
+ }
671
+
672
+ // ── Tracker: campaigns ──
673
+ case 'list_campaigns': {
674
+ result = await api('/api/campaigns');
675
+ break;
676
+ }
677
+ case 'create_campaign': {
678
+ const body = { name: args.name };
679
+ for (const k of ['traffic_source_label', 'country', 'cost_model', 'transition_mode']) {
680
+ if (typeof args[k] === 'string') body[k] = args[k];
681
+ }
682
+ if (typeof args.cost_value === 'number') body.cost_value = args.cost_value;
683
+ if ('domain_id' in args) body.domain_id = args.domain_id;
684
+ if (Array.isArray(args.offer_ids)) body.offer_ids = args.offer_ids;
685
+ if (Array.isArray(args.offer_weights)) body.offer_weights = args.offer_weights;
686
+ if (Array.isArray(args.lander_ids)) body.lander_ids = args.lander_ids;
687
+ if (Array.isArray(args.lander_weights)) body.lander_weights = args.lander_weights;
688
+ result = await api('/api/campaigns', { method: 'POST', body: JSON.stringify(body) });
689
+ break;
690
+ }
691
+ case 'get_campaign': {
692
+ result = await api(`/api/campaigns/${Number(args.campaign_id)}`);
693
+ break;
694
+ }
695
+ case 'update_campaign': {
696
+ const body = {};
697
+ for (const k of ['name', 'traffic_source_label', 'country', 'cost_model', 'transition_mode']) {
698
+ if (typeof args[k] === 'string') body[k] = args[k];
699
+ }
700
+ if (typeof args.cost_value === 'number') body.cost_value = args.cost_value;
701
+ if ('domain_id' in args) body.domain_id = args.domain_id;
702
+ if (typeof args.archived === 'boolean') body.archived = args.archived;
703
+ result = await api(`/api/campaigns/${Number(args.campaign_id)}`, { method: 'PATCH', body: JSON.stringify(body) });
704
+ break;
705
+ }
706
+ case 'delete_campaign': {
707
+ result = await api(`/api/campaigns/${Number(args.campaign_id)}`, { method: 'DELETE' });
708
+ break;
709
+ }
710
+ case 'set_campaign_offers': {
711
+ result = await api(`/api/campaigns/${Number(args.campaign_id)}/offers`, {
712
+ method: 'PUT', body: JSON.stringify({ items: args.items }),
713
+ });
714
+ break;
715
+ }
716
+ case 'set_campaign_landers': {
717
+ result = await api(`/api/campaigns/${Number(args.campaign_id)}/landers`, {
718
+ method: 'PUT', body: JSON.stringify({ items: args.items }),
719
+ });
720
+ break;
721
+ }
722
+ case 'get_campaign_stats': {
723
+ const days = args?.days ? `?days=${Number(args.days)}` : '';
724
+ result = await api(`/api/campaigns/${Number(args.campaign_id)}/stats${days}`);
725
+ break;
726
+ }
727
+
728
+ case 'list_properties': {
729
+ result = await api('/api/properties');
730
+ break;
731
+ }
732
+ case 'create_property': {
733
+ const body = { name: args.name };
734
+ if (typeof args.site_url === 'string') body.site_url = args.site_url;
735
+ result = await api('/api/properties', { method: 'POST', body: JSON.stringify(body) });
736
+ break;
737
+ }
738
+ case 'get_property': {
739
+ result = await api(`/api/properties/${Number(args.property_id)}`);
740
+ break;
741
+ }
742
+ case 'update_property': {
743
+ const body = {};
744
+ if (typeof args.name === 'string') body.name = args.name;
745
+ if ('site_url' in args) body.site_url = args.site_url;
746
+ if (typeof args.archived === 'boolean') body.archived = args.archived;
747
+ result = await api(`/api/properties/${Number(args.property_id)}`, { method: 'PATCH', body: JSON.stringify(body) });
748
+ break;
749
+ }
750
+ case 'delete_property': {
751
+ result = await api(`/api/properties/${Number(args.property_id)}`, { method: 'DELETE' });
752
+ break;
753
+ }
754
+ case 'set_property_campaigns': {
755
+ result = await api(`/api/properties/${Number(args.property_id)}/campaigns`, {
756
+ method: 'PUT', body: JSON.stringify({ items: args.items }),
757
+ });
758
+ break;
759
+ }
760
+ case 'get_property_stats': {
761
+ const days = args?.days ? `?days=${Number(args.days)}` : '';
762
+ result = await api(`/api/properties/${Number(args.property_id)}/stats${days}`);
763
+ break;
764
+ }
765
+
158
766
  default:
159
767
  throw new Error(`unknown tool: ${name}`);
160
768
  }