@buygolinks/mcp 0.2.0 → 0.9.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 -0
  2. package/package.json +1 -1
  3. package/src/index.js +555 -7
package/README.md CHANGED
@@ -63,6 +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. 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,6 +71,15 @@ Most MCP clients use the same shape. Point them at the `npx -y @buygolinks/mcp`
70
71
  | `create_domain` | Register a new branded domain (e.g. `go.acme.com`). Returns the CNAME target. |
71
72
  | `verify_domain` | Force a fresh Cloudflare status check after DNS is added. |
72
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` 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
+ | `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. |
73
83
 
74
84
  ## Example prompts
75
85
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buygolinks/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.9.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:
@@ -160,10 +182,335 @@ const TOOLS = [
160
182
  required: ['domain_id'],
161
183
  },
162
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, 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).',
443
+ inputSchema: {
444
+ type: 'object',
445
+ properties: {
446
+ name: { type: 'string' },
447
+ site_url: { type: 'string', description: 'Optional primary site URL, informational.' },
448
+ domain_id: { type: 'number', description: 'Optional id of one of the user\'s active custom domains (from list_domains) to serve the tag from. Defaults to the first active custom domain.' },
449
+ },
450
+ required: ['name'],
451
+ },
452
+ },
453
+ {
454
+ name: 'get_property',
455
+ description: 'Get a single property with its install snippet and the campaigns linked to it.',
456
+ inputSchema: { type: 'object', properties: { property_id: { type: 'number' } }, required: ['property_id'] },
457
+ },
458
+ {
459
+ name: 'update_property',
460
+ description: 'Update a property\'s name, site_url, tracking domain (domain_id), or archive state. Pass `domain_id` to re-bind the install snippet to a different active custom domain — it must be one of the user\'s active custom domains (a property can never fall back to buygolinks.com, since that breaks iOS tracking). Returns the updated property with its `tracking_domain` and regenerated `snippet`.',
461
+ inputSchema: {
462
+ type: 'object',
463
+ properties: {
464
+ property_id: { type: 'number' },
465
+ name: { type: 'string' },
466
+ site_url: { type: ['string', 'null'] },
467
+ domain_id: { type: 'number', description: 'Id of one of the user\'s active custom domains (from list_domains) to serve the tag from.' },
468
+ archived: { type: 'boolean' },
469
+ },
470
+ required: ['property_id'],
471
+ },
472
+ },
473
+ {
474
+ name: 'delete_property',
475
+ description: 'Delete a property. Its page events are also deleted (cascade). The install snippet stops reporting.',
476
+ inputSchema: { type: 'object', properties: { property_id: { type: 'number' } }, required: ['property_id'] },
477
+ },
478
+ {
479
+ name: 'set_property_campaigns',
480
+ 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.',
481
+ inputSchema: {
482
+ type: 'object',
483
+ properties: {
484
+ property_id: { type: 'number' },
485
+ items: {
486
+ type: 'array',
487
+ items: {
488
+ type: 'object',
489
+ properties: { campaign_id: { type: 'number' } },
490
+ required: ['campaign_id'],
491
+ },
492
+ },
493
+ },
494
+ required: ['property_id', 'items'],
495
+ },
496
+ },
497
+ {
498
+ name: 'get_property_stats',
499
+ 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.',
501
+ inputSchema: {
502
+ type: 'object',
503
+ properties: {
504
+ property_id: { type: 'number' },
505
+ days: { type: 'number', description: 'Window size in days (1–90). Defaults to 14.' },
506
+ },
507
+ required: ['property_id'],
508
+ },
509
+ },
163
510
  ];
164
511
 
165
512
  const server = new Server(
166
- { name: 'buygolinks', version: '0.2.0' },
513
+ { name: 'buygolinks', version: '0.8.0' },
167
514
  { capabilities: { tools: {} } },
168
515
  );
169
516
 
@@ -175,13 +522,33 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
175
522
  let result;
176
523
  switch (name) {
177
524
  case 'create_link': {
525
+ const body = {
526
+ destination_url: args.destination_url,
527
+ title: args.title,
528
+ source_label: args.source_label,
529
+ };
530
+ if (typeof args.domain_id === 'number') body.domain_id = args.domain_id;
178
531
  result = await api('/api/links', {
179
532
  method: 'POST',
180
- body: JSON.stringify({
181
- destination_url: args.destination_url,
182
- title: args.title,
183
- source_label: args.source_label,
184
- }),
533
+ body: JSON.stringify(body),
534
+ });
535
+ break;
536
+ }
537
+ case 'update_link': {
538
+ const body = {};
539
+ if (typeof args.title === 'string') body.title = args.title;
540
+ if (typeof args.source_label === 'string') body.source_label = args.source_label;
541
+ if (typeof args.archived === 'boolean') body.archived = args.archived;
542
+ if (typeof args.password === 'string') body.password = args.password;
543
+ if (args.domain_id === null || typeof args.domain_id === 'number') {
544
+ body.domain_id = args.domain_id;
545
+ }
546
+ if (args.alias === null || typeof args.alias === 'string') {
547
+ body.alias = args.alias;
548
+ }
549
+ result = await api(`/api/links/${Number(args.link_id)}`, {
550
+ method: 'PATCH',
551
+ body: JSON.stringify(body),
185
552
  });
186
553
  break;
187
554
  }
@@ -219,6 +586,187 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
219
586
  result = await api(`/api/domains/${Number(args.domain_id)}`, { method: 'DELETE' });
220
587
  break;
221
588
  }
589
+
590
+ // ── Tracker: networks ──
591
+ case 'list_affiliate_networks': {
592
+ result = await api('/api/affiliate-networks');
593
+ break;
594
+ }
595
+ case 'create_affiliate_network': {
596
+ const body = { name: args.name };
597
+ if (typeof args.conversion_param === 'string') body.conversion_param = args.conversion_param;
598
+ if (typeof args.payout_param === 'string') body.payout_param = args.payout_param;
599
+ if (typeof args.currency_param === 'string') body.currency_param = args.currency_param;
600
+ result = await api('/api/affiliate-networks', { method: 'POST', body: JSON.stringify(body) });
601
+ break;
602
+ }
603
+ case 'update_affiliate_network': {
604
+ const body = {};
605
+ if (typeof args.name === 'string') body.name = args.name;
606
+ if (typeof args.conversion_param === 'string') body.conversion_param = args.conversion_param;
607
+ if (typeof args.payout_param === 'string') body.payout_param = args.payout_param;
608
+ if (typeof args.currency_param === 'string') body.currency_param = args.currency_param;
609
+ if (typeof args.archived === 'boolean') body.archived = args.archived;
610
+ result = await api(`/api/affiliate-networks/${Number(args.network_id)}`, { method: 'PATCH', body: JSON.stringify(body) });
611
+ break;
612
+ }
613
+ case 'delete_affiliate_network': {
614
+ result = await api(`/api/affiliate-networks/${Number(args.network_id)}`, { method: 'DELETE' });
615
+ break;
616
+ }
617
+
618
+ // ── Tracker: offers ──
619
+ case 'list_offers': {
620
+ result = await api('/api/offers');
621
+ break;
622
+ }
623
+ case 'create_offer': {
624
+ const body = { name: args.name, url: args.url };
625
+ if ('affiliate_network_id' in args) body.affiliate_network_id = args.affiliate_network_id;
626
+ if (typeof args.payout === 'number') body.payout = args.payout;
627
+ if (typeof args.payout_currency === 'string') body.payout_currency = args.payout_currency;
628
+ if (typeof args.country === 'string') body.country = args.country;
629
+ result = await api('/api/offers', { method: 'POST', body: JSON.stringify(body) });
630
+ break;
631
+ }
632
+ case 'update_offer': {
633
+ const body = {};
634
+ if (typeof args.name === 'string') body.name = args.name;
635
+ if (typeof args.url === 'string') body.url = args.url;
636
+ if ('affiliate_network_id' in args) body.affiliate_network_id = args.affiliate_network_id;
637
+ if (typeof args.payout === 'number') body.payout = args.payout;
638
+ if (typeof args.payout_currency === 'string') body.payout_currency = args.payout_currency;
639
+ if (typeof args.country === 'string') body.country = args.country;
640
+ if (typeof args.archived === 'boolean') body.archived = args.archived;
641
+ result = await api(`/api/offers/${Number(args.offer_id)}`, { method: 'PATCH', body: JSON.stringify(body) });
642
+ break;
643
+ }
644
+ case 'delete_offer': {
645
+ result = await api(`/api/offers/${Number(args.offer_id)}`, { method: 'DELETE' });
646
+ break;
647
+ }
648
+
649
+ // ── Tracker: landers ──
650
+ case 'list_landers': {
651
+ result = await api('/api/landers');
652
+ break;
653
+ }
654
+ case 'create_lander': {
655
+ const body = { name: args.name, url: args.url };
656
+ if (typeof args.country === 'string') body.country = args.country;
657
+ result = await api('/api/landers', { method: 'POST', body: JSON.stringify(body) });
658
+ break;
659
+ }
660
+ case 'update_lander': {
661
+ const body = {};
662
+ if (typeof args.name === 'string') body.name = args.name;
663
+ if (typeof args.url === 'string') body.url = args.url;
664
+ if (typeof args.country === 'string') body.country = args.country;
665
+ if (typeof args.archived === 'boolean') body.archived = args.archived;
666
+ result = await api(`/api/landers/${Number(args.lander_id)}`, { method: 'PATCH', body: JSON.stringify(body) });
667
+ break;
668
+ }
669
+ case 'delete_lander': {
670
+ result = await api(`/api/landers/${Number(args.lander_id)}`, { method: 'DELETE' });
671
+ break;
672
+ }
673
+
674
+ // ── Tracker: campaigns ──
675
+ case 'list_campaigns': {
676
+ result = await api('/api/campaigns');
677
+ break;
678
+ }
679
+ case 'create_campaign': {
680
+ const body = { name: args.name };
681
+ for (const k of ['traffic_source_label', 'country', 'cost_model', 'transition_mode']) {
682
+ if (typeof args[k] === 'string') body[k] = args[k];
683
+ }
684
+ if (typeof args.cost_value === 'number') body.cost_value = args.cost_value;
685
+ if ('domain_id' in args) body.domain_id = args.domain_id;
686
+ if (Array.isArray(args.offer_ids)) body.offer_ids = args.offer_ids;
687
+ if (Array.isArray(args.offer_weights)) body.offer_weights = args.offer_weights;
688
+ if (Array.isArray(args.lander_ids)) body.lander_ids = args.lander_ids;
689
+ if (Array.isArray(args.lander_weights)) body.lander_weights = args.lander_weights;
690
+ result = await api('/api/campaigns', { method: 'POST', body: JSON.stringify(body) });
691
+ break;
692
+ }
693
+ case 'get_campaign': {
694
+ result = await api(`/api/campaigns/${Number(args.campaign_id)}`);
695
+ break;
696
+ }
697
+ case 'update_campaign': {
698
+ const body = {};
699
+ for (const k of ['name', 'traffic_source_label', 'country', 'cost_model', 'transition_mode']) {
700
+ if (typeof args[k] === 'string') body[k] = args[k];
701
+ }
702
+ if (typeof args.cost_value === 'number') body.cost_value = args.cost_value;
703
+ if ('domain_id' in args) body.domain_id = args.domain_id;
704
+ if (typeof args.archived === 'boolean') body.archived = args.archived;
705
+ result = await api(`/api/campaigns/${Number(args.campaign_id)}`, { method: 'PATCH', body: JSON.stringify(body) });
706
+ break;
707
+ }
708
+ case 'delete_campaign': {
709
+ result = await api(`/api/campaigns/${Number(args.campaign_id)}`, { method: 'DELETE' });
710
+ break;
711
+ }
712
+ case 'set_campaign_offers': {
713
+ result = await api(`/api/campaigns/${Number(args.campaign_id)}/offers`, {
714
+ method: 'PUT', body: JSON.stringify({ items: args.items }),
715
+ });
716
+ break;
717
+ }
718
+ case 'set_campaign_landers': {
719
+ result = await api(`/api/campaigns/${Number(args.campaign_id)}/landers`, {
720
+ method: 'PUT', body: JSON.stringify({ items: args.items }),
721
+ });
722
+ break;
723
+ }
724
+ case 'get_campaign_stats': {
725
+ const days = args?.days ? `?days=${Number(args.days)}` : '';
726
+ result = await api(`/api/campaigns/${Number(args.campaign_id)}/stats${days}`);
727
+ break;
728
+ }
729
+
730
+ case 'list_properties': {
731
+ result = await api('/api/properties');
732
+ break;
733
+ }
734
+ case 'create_property': {
735
+ const body = { name: args.name };
736
+ if (typeof args.site_url === 'string') body.site_url = args.site_url;
737
+ if (typeof args.domain_id === 'number') body.domain_id = args.domain_id;
738
+ result = await api('/api/properties', { method: 'POST', body: JSON.stringify(body) });
739
+ break;
740
+ }
741
+ case 'get_property': {
742
+ result = await api(`/api/properties/${Number(args.property_id)}`);
743
+ break;
744
+ }
745
+ case 'update_property': {
746
+ const body = {};
747
+ if (typeof args.name === 'string') body.name = args.name;
748
+ if ('site_url' in args) body.site_url = args.site_url;
749
+ if (typeof args.domain_id === 'number') body.domain_id = args.domain_id;
750
+ if (typeof args.archived === 'boolean') body.archived = args.archived;
751
+ result = await api(`/api/properties/${Number(args.property_id)}`, { method: 'PATCH', body: JSON.stringify(body) });
752
+ break;
753
+ }
754
+ case 'delete_property': {
755
+ result = await api(`/api/properties/${Number(args.property_id)}`, { method: 'DELETE' });
756
+ break;
757
+ }
758
+ case 'set_property_campaigns': {
759
+ result = await api(`/api/properties/${Number(args.property_id)}/campaigns`, {
760
+ method: 'PUT', body: JSON.stringify({ items: args.items }),
761
+ });
762
+ break;
763
+ }
764
+ case 'get_property_stats': {
765
+ const days = args?.days ? `?days=${Number(args.days)}` : '';
766
+ result = await api(`/api/properties/${Number(args.property_id)}/stats${days}`);
767
+ break;
768
+ }
769
+
222
770
  default:
223
771
  throw new Error(`unknown tool: ${name}`);
224
772
  }