@affset/mcp 0.1.1 → 0.3.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 (79) hide show
  1. package/.env.example +5 -0
  2. package/README.md +191 -56
  3. package/dist/client.d.ts +43 -0
  4. package/dist/client.js +15 -2
  5. package/dist/config.d.ts +7 -0
  6. package/dist/config.js +15 -54
  7. package/dist/core.d.ts +18 -0
  8. package/dist/core.js +18 -0
  9. package/dist/docs.d.ts +39 -0
  10. package/dist/docs.js +105 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/lib/format.d.ts +46 -0
  13. package/dist/lib/format.js +3 -0
  14. package/dist/lib/integrationUrls.d.ts +87 -0
  15. package/dist/lib/integrationUrls.js +84 -4
  16. package/dist/lib/linkArgs.d.ts +32 -0
  17. package/dist/lib/linkArgs.js +14 -3
  18. package/dist/lib/patch.d.ts +10 -0
  19. package/dist/lib/payoutRules.d.ts +38 -0
  20. package/dist/lib/readBody.d.ts +17 -0
  21. package/dist/lib/readBody.js +69 -0
  22. package/dist/lib/targeting.d.ts +51 -0
  23. package/dist/lib/time.d.ts +38 -0
  24. package/dist/lib/toolResult.d.ts +7 -0
  25. package/dist/lib/urls.d.ts +2 -0
  26. package/dist/lib/zones.d.ts +24 -0
  27. package/dist/lib/zones.js +9 -1
  28. package/dist/registerTools.d.ts +40 -0
  29. package/dist/registerTools.js +437 -0
  30. package/dist/runtimeConfig.d.ts +37 -0
  31. package/dist/runtimeConfig.js +93 -0
  32. package/dist/server.d.ts +4 -0
  33. package/dist/server.js +3 -318
  34. package/dist/tools/createCampaign.d.ts +25 -0
  35. package/dist/tools/createCampaign.js +20 -8
  36. package/dist/tools/createTeamMember.d.ts +26 -0
  37. package/dist/tools/createTrafficSource.d.ts +25 -0
  38. package/dist/tools/createTrafficSource.js +191 -0
  39. package/dist/tools/createZone.d.ts +24 -0
  40. package/dist/tools/createZone.js +13 -1
  41. package/dist/tools/cutZones.d.ts +29 -0
  42. package/dist/tools/deletePayoutRule.d.ts +16 -0
  43. package/dist/tools/getCampaign.d.ts +12 -0
  44. package/dist/tools/getCampaign.js +150 -0
  45. package/dist/tools/getStats.d.ts +42 -0
  46. package/dist/tools/getStats.js +54 -3
  47. package/dist/tools/getTrackingLink.d.ts +23 -0
  48. package/dist/tools/getTrackingLink.js +26 -9
  49. package/dist/tools/getZoneUrl.d.ts +21 -0
  50. package/dist/tools/getZoneUrl.js +30 -13
  51. package/dist/tools/listCampaigns.d.ts +23 -0
  52. package/dist/tools/listConversions.d.ts +31 -0
  53. package/dist/tools/listConversions.js +29 -10
  54. package/dist/tools/listPayoutRules.d.ts +12 -0
  55. package/dist/tools/listSubLabels.d.ts +5 -0
  56. package/dist/tools/listTargetingRules.d.ts +12 -0
  57. package/dist/tools/listTargetingTypes.d.ts +5 -0
  58. package/dist/tools/listTeam.d.ts +14 -0
  59. package/dist/tools/listTrafficSources.d.ts +17 -0
  60. package/dist/tools/listTrafficSources.js +63 -0
  61. package/dist/tools/listZones.d.ts +23 -0
  62. package/dist/tools/listZones.js +10 -5
  63. package/dist/tools/removeTargetingRule.d.ts +21 -0
  64. package/dist/tools/setCampaignStatus.d.ts +18 -0
  65. package/dist/tools/setPayoutGoal.d.ts +16 -0
  66. package/dist/tools/setPayoutRule.d.ts +18 -0
  67. package/dist/tools/setSubLabels.d.ts +22 -0
  68. package/dist/tools/setTargetingRule.d.ts +21 -0
  69. package/dist/tools/updateCampaign.d.ts +35 -0
  70. package/dist/tools/updateTrafficSource.d.ts +25 -0
  71. package/dist/tools/updateTrafficSource.js +171 -0
  72. package/dist/tools/updateZone.d.ts +27 -0
  73. package/dist/tools/updateZone.js +13 -3
  74. package/dist/tools/whoami.d.ts +6 -0
  75. package/dist/types.d.ts +257 -0
  76. package/dist/types.js +2 -0
  77. package/dist/version.d.ts +7 -0
  78. package/dist/version.js +8 -0
  79. package/package.json +15 -2
@@ -0,0 +1,93 @@
1
+ export const MIN_REQUEST_TIMEOUT_MS = 1_000;
2
+ export const MAX_REQUEST_TIMEOUT_MS = 300_000;
3
+ const NAMESPACE_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
4
+ function isLoopbackHost(hostname) {
5
+ return (hostname === "localhost" ||
6
+ hostname === "127.0.0.1" ||
7
+ hostname === "::1" ||
8
+ hostname.endsWith(".localhost"));
9
+ }
10
+ /** Parse and normalize an HTTP(S) origin, refusing cleartext remote hosts. */
11
+ export function parseOriginUrl(raw, name, carriesApiKey = false) {
12
+ const normalized = raw.replace(/\/+$/, "");
13
+ let parsed;
14
+ try {
15
+ parsed = new URL(normalized);
16
+ }
17
+ catch {
18
+ throw new Error(`${name} is not a valid URL: ${raw}`);
19
+ }
20
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
21
+ throw new Error(`${name} must be http(s), got ${parsed.protocol}`);
22
+ }
23
+ if (parsed.username || parsed.password) {
24
+ throw new Error(`${name} must not contain embedded credentials.`);
25
+ }
26
+ if ((parsed.pathname !== "/" && parsed.pathname !== "") || parsed.search || parsed.hash) {
27
+ throw new Error(`${name} must be an origin only (scheme, host, and optional port; no path, query, or fragment).`);
28
+ }
29
+ if (parsed.protocol === "http:" && !isLoopbackHost(parsed.hostname)) {
30
+ const cleartextHint = carriesApiKey ? " — plain http would send the API key in cleartext" : "";
31
+ throw new Error(`${name} must be https for a non-local host (got http://${parsed.hostname})${cleartextHint}.`);
32
+ }
33
+ return parsed;
34
+ }
35
+ /** Validate a namespace using the same rules as tenant signup. */
36
+ export function validateNamespace(namespace, name) {
37
+ if (namespace.length < 3 || namespace.length > 63) {
38
+ throw new Error(`${name} must be 3–63 characters.`);
39
+ }
40
+ if (!NAMESPACE_RE.test(namespace) || namespace.includes("--")) {
41
+ throw new Error(`${name} must be lowercase letters, numbers, and hyphens only ` +
42
+ "(no leading/trailing or consecutive hyphens).");
43
+ }
44
+ }
45
+ function requiredString(value, name) {
46
+ if (typeof value !== "string" || value.trim() === "") {
47
+ throw new Error(`${name} must be a non-empty string.`);
48
+ }
49
+ return value.trim();
50
+ }
51
+ /** Reject empty keys and CR/LF so a value cannot split HTTP headers. */
52
+ export function validateApiKey(value, name) {
53
+ const apiKey = requiredString(value, name);
54
+ if (/\r|\n/.test(apiKey)) {
55
+ throw new Error(`${name} must not contain line breaks.`);
56
+ }
57
+ return apiKey;
58
+ }
59
+ /**
60
+ * Validate and normalize config supplied through the public library API.
61
+ *
62
+ * TypeScript types are not a runtime security boundary. Hosted transports can
63
+ * source these values from deployment config or grant records, so fail before
64
+ * registering tools if a malformed value could leak a bearer key or silently
65
+ * disable the read-only boundary.
66
+ */
67
+ export function normalizeRuntimeConfig(config) {
68
+ if (config === null || typeof config !== "object") {
69
+ throw new Error("config must be an object.");
70
+ }
71
+ const baseUrl = parseOriginUrl(requiredString(config.baseUrl, "config.baseUrl"), "config.baseUrl", true).origin;
72
+ const docsBaseUrl = parseOriginUrl(requiredString(config.docsBaseUrl, "config.docsBaseUrl"), "config.docsBaseUrl").origin;
73
+ const apiKey = validateApiKey(config.apiKey, "config.apiKey");
74
+ const namespace = requiredString(config.namespace, "config.namespace");
75
+ validateNamespace(namespace, "config.namespace");
76
+ if (!Number.isSafeInteger(config.requestTimeoutMs) ||
77
+ config.requestTimeoutMs < MIN_REQUEST_TIMEOUT_MS ||
78
+ config.requestTimeoutMs > MAX_REQUEST_TIMEOUT_MS) {
79
+ throw new Error(`config.requestTimeoutMs must be an integer between ${MIN_REQUEST_TIMEOUT_MS} and ${MAX_REQUEST_TIMEOUT_MS}.`);
80
+ }
81
+ if (typeof config.readOnly !== "boolean") {
82
+ throw new Error("config.readOnly must be a boolean.");
83
+ }
84
+ return {
85
+ baseUrl,
86
+ docsBaseUrl,
87
+ apiKey,
88
+ namespace,
89
+ requestTimeoutMs: config.requestTimeoutMs,
90
+ readOnly: config.readOnly,
91
+ };
92
+ }
93
+ //# sourceMappingURL=runtimeConfig.js.map
@@ -0,0 +1,4 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { Config } from "./runtimeConfig.js";
3
+ /** Build the MCP server with the affset tools registered against `config`. */
4
+ export declare function createServer(config: Config): McpServer;
package/dist/server.js CHANGED
@@ -1,328 +1,13 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { AffsetClient } from "./client.js";
3
- import { getStats, getStatsInputSchema, GET_STATS_DESCRIPTION } from "./tools/getStats.js";
4
- import { cutZones, cutZonesInputSchema, CUT_ZONES_DESCRIPTION } from "./tools/cutZones.js";
5
- import { createCampaign, createCampaignInputSchema, CREATE_CAMPAIGN_DESCRIPTION, } from "./tools/createCampaign.js";
6
- import { listCampaigns, listCampaignsInputSchema, LIST_CAMPAIGNS_DESCRIPTION, } from "./tools/listCampaigns.js";
7
- import { listZones, listZonesInputSchema, LIST_ZONES_DESCRIPTION } from "./tools/listZones.js";
8
- import { listTeam, listTeamInputSchema, LIST_TEAM_DESCRIPTION } from "./tools/listTeam.js";
9
- import { createTeamMember, createTeamMemberInputSchema, CREATE_TEAM_MEMBER_DESCRIPTION, } from "./tools/createTeamMember.js";
10
- import { createZone, createZoneInputSchema, CREATE_ZONE_DESCRIPTION } from "./tools/createZone.js";
11
- import { getZoneUrl, getZoneUrlInputSchema, GET_ZONE_URL_DESCRIPTION } from "./tools/getZoneUrl.js";
12
- import { getTrackingLink, getTrackingLinkInputSchema, GET_TRACKING_LINK_DESCRIPTION, } from "./tools/getTrackingLink.js";
13
- import { updateZone, updateZoneInputSchema, UPDATE_ZONE_DESCRIPTION } from "./tools/updateZone.js";
14
- import { updateCampaign, updateCampaignInputSchema, UPDATE_CAMPAIGN_DESCRIPTION, } from "./tools/updateCampaign.js";
15
- import { setCampaignStatus, setCampaignStatusInputSchema, SET_CAMPAIGN_STATUS_DESCRIPTION, } from "./tools/setCampaignStatus.js";
16
- import { listPayoutRules, listPayoutRulesInputSchema, LIST_PAYOUT_RULES_DESCRIPTION, } from "./tools/listPayoutRules.js";
17
- import { setPayoutRule, setPayoutRuleInputSchema, SET_PAYOUT_RULE_DESCRIPTION, } from "./tools/setPayoutRule.js";
18
- import { deletePayoutRule, deletePayoutRuleInputSchema, DELETE_PAYOUT_RULE_DESCRIPTION, } from "./tools/deletePayoutRule.js";
19
- import { setPayoutGoal, setPayoutGoalInputSchema, SET_PAYOUT_GOAL_DESCRIPTION, } from "./tools/setPayoutGoal.js";
20
- import { listTargetingTypes, listTargetingTypesInputSchema, LIST_TARGETING_TYPES_DESCRIPTION, } from "./tools/listTargetingTypes.js";
21
- import { listTargetingRules, listTargetingRulesInputSchema, LIST_TARGETING_RULES_DESCRIPTION, } from "./tools/listTargetingRules.js";
22
- import { setTargetingRule, setTargetingRuleInputSchema, SET_TARGETING_RULE_DESCRIPTION, } from "./tools/setTargetingRule.js";
23
- import { removeTargetingRule, removeTargetingRuleInputSchema, REMOVE_TARGETING_RULE_DESCRIPTION, } from "./tools/removeTargetingRule.js";
24
- import { listSubLabels, listSubLabelsInputSchema, LIST_SUB_LABELS_DESCRIPTION, } from "./tools/listSubLabels.js";
25
- import { setSubLabels, setSubLabelsInputSchema, SET_SUB_LABELS_DESCRIPTION, } from "./tools/setSubLabels.js";
26
- import { listConversions, listConversionsInputSchema, LIST_CONVERSIONS_DESCRIPTION, } from "./tools/listConversions.js";
27
- import { whoami, whoamiInputSchema, WHOAMI_DESCRIPTION } from "./tools/whoami.js";
28
- import { createRequire } from "node:module";
29
- const require = createRequire(import.meta.url);
30
- const { version: VERSION } = require("../package.json");
2
+ import { registerAffsetTools } from "./registerTools.js";
3
+ import { VERSION } from "./version.js";
31
4
  /** Build the MCP server with the affset tools registered against `config`. */
32
5
  export function createServer(config) {
33
- const client = new AffsetClient(config);
34
6
  const server = new McpServer({
35
7
  name: "affset-mcp",
36
8
  version: VERSION,
37
9
  });
38
- // Registers every tool as normal, then immediately removes anything that isn't
39
- // readOnlyHint:true when AFFSET_READ_ONLY is set — a mutation tool never has a
40
- // chance to appear in tools/list or be called. See AFFSET_READ_ONLY in the README
41
- // for why this exists (untrusted third-party data can reach model context via
42
- // list_conversions/get_stats, so mutation tools are an injection blast-radius
43
- // control, not just a UI nicety).
44
- const registerTool = (name, toolConfig, cb) => {
45
- const tool = server.registerTool(name, toolConfig, cb);
46
- if (config.readOnly && toolConfig.annotations?.readOnlyHint !== true) {
47
- tool.remove();
48
- }
49
- return tool;
50
- };
51
- registerTool("whoami", {
52
- title: "Show the tenant this MCP is bound to",
53
- description: WHOAMI_DESCRIPTION,
54
- inputSchema: whoamiInputSchema,
55
- annotations: {
56
- readOnlyHint: true,
57
- destructiveHint: false,
58
- idempotentHint: true,
59
- openWorldHint: true,
60
- },
61
- }, () => whoami(client, config));
62
- registerTool("get_stats", {
63
- title: "Get affset stats",
64
- description: GET_STATS_DESCRIPTION,
65
- inputSchema: getStatsInputSchema,
66
- annotations: {
67
- readOnlyHint: true,
68
- destructiveHint: false,
69
- idempotentHint: true,
70
- openWorldHint: true,
71
- },
72
- }, (args) => getStats(client, args));
73
- registerTool("list_campaigns", {
74
- title: "List campaigns",
75
- description: LIST_CAMPAIGNS_DESCRIPTION,
76
- inputSchema: listCampaignsInputSchema,
77
- annotations: {
78
- readOnlyHint: true,
79
- destructiveHint: false,
80
- idempotentHint: true,
81
- openWorldHint: true,
82
- },
83
- }, (args) => listCampaigns(client, args));
84
- registerTool("list_zones", {
85
- title: "List zones",
86
- description: LIST_ZONES_DESCRIPTION,
87
- inputSchema: listZonesInputSchema,
88
- annotations: {
89
- readOnlyHint: true,
90
- destructiveHint: false,
91
- idempotentHint: true,
92
- openWorldHint: true,
93
- },
94
- }, (args) => listZones(client, args));
95
- registerTool("list_team", {
96
- title: "List team members",
97
- description: LIST_TEAM_DESCRIPTION,
98
- inputSchema: listTeamInputSchema,
99
- annotations: {
100
- readOnlyHint: true,
101
- destructiveHint: false,
102
- idempotentHint: true,
103
- openWorldHint: true,
104
- },
105
- }, (args) => listTeam(client, args));
106
- registerTool("create_team_member", {
107
- title: "Invite a team member",
108
- description: CREATE_TEAM_MEMBER_DESCRIPTION,
109
- inputSchema: createTeamMemberInputSchema,
110
- annotations: {
111
- readOnlyHint: false,
112
- destructiveHint: false,
113
- idempotentHint: false,
114
- openWorldHint: true,
115
- },
116
- }, (args) => createTeamMember(client, args));
117
- registerTool("get_zone_url", {
118
- title: "Get the zone URL for a traffic source",
119
- description: GET_ZONE_URL_DESCRIPTION,
120
- inputSchema: getZoneUrlInputSchema,
121
- annotations: {
122
- readOnlyHint: true,
123
- destructiveHint: false,
124
- idempotentHint: true,
125
- openWorldHint: true,
126
- },
127
- }, (args) => getZoneUrl(client, config, args));
128
- registerTool("get_tracking_link", {
129
- title: "Get a campaign's tracking link",
130
- description: GET_TRACKING_LINK_DESCRIPTION,
131
- inputSchema: getTrackingLinkInputSchema,
132
- annotations: {
133
- readOnlyHint: true,
134
- destructiveHint: false,
135
- idempotentHint: true,
136
- openWorldHint: true,
137
- },
138
- }, (args) => getTrackingLink(client, config, args));
139
- registerTool("create_campaign", {
140
- title: "Create a campaign",
141
- description: CREATE_CAMPAIGN_DESCRIPTION,
142
- inputSchema: createCampaignInputSchema,
143
- annotations: {
144
- readOnlyHint: false,
145
- destructiveHint: false,
146
- idempotentHint: false,
147
- openWorldHint: true,
148
- },
149
- }, (args) => createCampaign(client, config, args));
150
- registerTool("update_campaign", {
151
- title: "Update a campaign",
152
- description: UPDATE_CAMPAIGN_DESCRIPTION,
153
- inputSchema: updateCampaignInputSchema,
154
- annotations: {
155
- readOnlyHint: false,
156
- destructiveHint: true,
157
- idempotentHint: false,
158
- openWorldHint: true,
159
- },
160
- }, (args) => updateCampaign(client, args));
161
- registerTool("set_campaign_status", {
162
- title: "Run or pause a campaign",
163
- description: SET_CAMPAIGN_STATUS_DESCRIPTION,
164
- inputSchema: setCampaignStatusInputSchema,
165
- annotations: {
166
- readOnlyHint: false,
167
- destructiveHint: true,
168
- idempotentHint: false,
169
- openWorldHint: true,
170
- },
171
- }, (args) => setCampaignStatus(client, args));
172
- registerTool("create_zone", {
173
- title: "Create a zone",
174
- description: CREATE_ZONE_DESCRIPTION,
175
- inputSchema: createZoneInputSchema,
176
- annotations: {
177
- readOnlyHint: false,
178
- destructiveHint: false,
179
- idempotentHint: false,
180
- openWorldHint: true,
181
- },
182
- }, (args) => createZone(client, args));
183
- registerTool("update_zone", {
184
- title: "Update a zone",
185
- description: UPDATE_ZONE_DESCRIPTION,
186
- inputSchema: updateZoneInputSchema,
187
- annotations: {
188
- readOnlyHint: false,
189
- destructiveHint: true,
190
- idempotentHint: false,
191
- openWorldHint: true,
192
- },
193
- }, (args) => updateZone(client, args));
194
- registerTool("cut_zones", {
195
- title: "Cut underperforming zones",
196
- description: CUT_ZONES_DESCRIPTION,
197
- inputSchema: cutZonesInputSchema,
198
- annotations: {
199
- readOnlyHint: false,
200
- destructiveHint: true,
201
- idempotentHint: false,
202
- openWorldHint: true,
203
- },
204
- }, (args) => cutZones(client, args));
205
- registerTool("list_payout_rules", {
206
- title: "List payout rules",
207
- description: LIST_PAYOUT_RULES_DESCRIPTION,
208
- inputSchema: listPayoutRulesInputSchema,
209
- annotations: {
210
- readOnlyHint: true,
211
- destructiveHint: false,
212
- idempotentHint: true,
213
- openWorldHint: true,
214
- },
215
- }, (args) => listPayoutRules(client, args));
216
- registerTool("set_payout_rule", {
217
- title: "Set a payout rule",
218
- description: SET_PAYOUT_RULE_DESCRIPTION,
219
- inputSchema: setPayoutRuleInputSchema,
220
- annotations: {
221
- readOnlyHint: false,
222
- destructiveHint: true,
223
- idempotentHint: false,
224
- openWorldHint: true,
225
- },
226
- }, (args) => setPayoutRule(client, args));
227
- registerTool("delete_payout_rule", {
228
- title: "Delete a payout rule",
229
- description: DELETE_PAYOUT_RULE_DESCRIPTION,
230
- inputSchema: deletePayoutRuleInputSchema,
231
- annotations: {
232
- readOnlyHint: false,
233
- destructiveHint: true,
234
- idempotentHint: false,
235
- openWorldHint: true,
236
- },
237
- }, (args) => deletePayoutRule(client, args));
238
- registerTool("set_payout_goal", {
239
- title: "Set payout goal type",
240
- description: SET_PAYOUT_GOAL_DESCRIPTION,
241
- inputSchema: setPayoutGoalInputSchema,
242
- annotations: {
243
- readOnlyHint: false,
244
- destructiveHint: true,
245
- idempotentHint: false,
246
- openWorldHint: true,
247
- },
248
- }, (args) => setPayoutGoal(client, args));
249
- registerTool("list_targeting_types", {
250
- title: "List targeting rule types",
251
- description: LIST_TARGETING_TYPES_DESCRIPTION,
252
- inputSchema: listTargetingTypesInputSchema,
253
- annotations: {
254
- readOnlyHint: true,
255
- destructiveHint: false,
256
- idempotentHint: true,
257
- openWorldHint: true,
258
- },
259
- }, () => listTargetingTypes(client));
260
- registerTool("list_targeting_rules", {
261
- title: "List campaign targeting rules",
262
- description: LIST_TARGETING_RULES_DESCRIPTION,
263
- inputSchema: listTargetingRulesInputSchema,
264
- annotations: {
265
- readOnlyHint: true,
266
- destructiveHint: false,
267
- idempotentHint: true,
268
- openWorldHint: true,
269
- },
270
- }, (args) => listTargetingRules(client, args));
271
- registerTool("set_targeting_rule", {
272
- title: "Set a targeting rule",
273
- description: SET_TARGETING_RULE_DESCRIPTION,
274
- inputSchema: setTargetingRuleInputSchema,
275
- annotations: {
276
- readOnlyHint: false,
277
- destructiveHint: true,
278
- idempotentHint: false,
279
- openWorldHint: true,
280
- },
281
- }, (args) => setTargetingRule(client, args));
282
- registerTool("remove_targeting_rule", {
283
- title: "Remove a targeting rule",
284
- description: REMOVE_TARGETING_RULE_DESCRIPTION,
285
- inputSchema: removeTargetingRuleInputSchema,
286
- annotations: {
287
- readOnlyHint: false,
288
- destructiveHint: true,
289
- idempotentHint: false,
290
- openWorldHint: true,
291
- },
292
- }, (args) => removeTargetingRule(client, args));
293
- registerTool("list_sub_labels", {
294
- title: "List sub labels",
295
- description: LIST_SUB_LABELS_DESCRIPTION,
296
- inputSchema: listSubLabelsInputSchema,
297
- annotations: {
298
- readOnlyHint: true,
299
- destructiveHint: false,
300
- idempotentHint: true,
301
- openWorldHint: true,
302
- },
303
- }, () => listSubLabels(client));
304
- registerTool("set_sub_labels", {
305
- title: "Set sub labels",
306
- description: SET_SUB_LABELS_DESCRIPTION,
307
- inputSchema: setSubLabelsInputSchema,
308
- annotations: {
309
- readOnlyHint: false,
310
- destructiveHint: true,
311
- idempotentHint: false,
312
- openWorldHint: true,
313
- },
314
- }, (args) => setSubLabels(client, args));
315
- registerTool("list_conversions", {
316
- title: "List conversions",
317
- description: LIST_CONVERSIONS_DESCRIPTION,
318
- inputSchema: listConversionsInputSchema,
319
- annotations: {
320
- readOnlyHint: true,
321
- destructiveHint: false,
322
- idempotentHint: true,
323
- openWorldHint: true,
324
- },
325
- }, (args) => listConversions(client, args));
10
+ registerAffsetTools(server, config);
326
11
  return server;
327
12
  }
328
13
  //# sourceMappingURL=server.js.map
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
+ import { type AffsetClient } from "../client.js";
4
+ import type { Config } from "../runtimeConfig.js";
5
+ export declare const CREATE_CAMPAIGN_DESCRIPTION: string;
6
+ export declare const createCampaignInputSchema: {
7
+ user_email: z.ZodString;
8
+ offer_url: z.ZodString;
9
+ name: z.ZodOptional<z.ZodString>;
10
+ geo: z.ZodOptional<z.ZodArray<z.ZodString, "atleastone">>;
11
+ payout: z.ZodOptional<z.ZodNumber>;
12
+ zone_id: z.ZodOptional<z.ZodString>;
13
+ confirm: z.ZodDefault<z.ZodBoolean>;
14
+ };
15
+ type CreateCampaignArgs = {
16
+ user_email: string;
17
+ offer_url: string;
18
+ name?: string;
19
+ geo?: string[];
20
+ payout?: number;
21
+ zone_id?: string;
22
+ confirm: boolean;
23
+ };
24
+ export declare function createCampaign(client: AffsetClient, config: Config, args: CreateCampaignArgs): Promise<CallToolResult>;
25
+ export {};
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { AffsetApiError } from "../client.js";
3
3
  import { mdCell, moneyPrecise } from "../lib/format.js";
4
- import { buildTrackingLink, fetchTenantIntegration } from "../lib/integrationUrls.js";
4
+ import { buildTrackingLink, fetchTenantIntegration, resolveLinkedSource, } from "../lib/integrationUrls.js";
5
5
  import { errorResult, textError, textResult } from "../lib/toolResult.js";
6
6
  import { httpUrlError } from "../lib/urls.js";
7
7
  import { resolveZone, zonePostbackNote } from "../lib/zones.js";
@@ -17,8 +17,9 @@ export const CREATE_CAMPAIGN_DESCRIPTION = "Create a campaign (offer) in the cur
17
17
  "create one first with create_team_member if needed. " +
18
18
  "Everything else gets media-buying defaults: CPA model, " +
19
19
  "rate 0 (no internal advertiser billing), created paused, a global payout rule when " +
20
- "payout is given, and a ready-to-use tracking link prefilled with the sub convention " +
21
- "(source_click_id + sub1..sub5, named by the tenant's sub labels). The campaign is " +
20
+ "payout is given, and a ready-to-use tracking link. A zone linked to a traffic source " +
21
+ "uses that source's tracking template; otherwise the link is prefilled with the sub " +
22
+ "convention (source_click_id + sub1..sub5, named by the tenant's sub labels). The campaign is " +
22
23
  "created paused, so activate it before sending traffic through either URL. " +
23
24
  "Geo whitelist applies to /serve rotation only; the tracking link itself is not geo-gated. " +
24
25
  "DRY-RUN by default; pass confirm=true to apply. No money is spent by this call; the " +
@@ -87,6 +88,7 @@ export async function createCampaign(client, config, args) {
87
88
  return textError(zoneResult.error);
88
89
  }
89
90
  const { zone, inactiveWarning } = zoneResult;
91
+ const linked = await resolveLinkedSource(client, zone, false);
90
92
  const name = args.name?.trim() || defaultName(offerUrl, geoCodes, args.payout);
91
93
  const geoNote = geoCodes.length
92
94
  ? `${geoCodes.join(", ")} (whitelist; enforced on /serve, not on the tracking link)`
@@ -105,6 +107,7 @@ export async function createCampaign(client, config, args) {
105
107
  `| Model | CPA, rate 0 (defaults — no internal advertiser billing) |`,
106
108
  `| Status | paused (activate before sending traffic through either URL) |`,
107
109
  `| Zone | ${mdCell(zone.name)} (\`${zone.id}\`) — ${zonePostbackNote(zone)} |`,
110
+ ...(linked.sourceLabel ? [`| Traffic source | ${linked.sourceLabel} |`] : []),
108
111
  ].join("\n");
109
112
  if (!args.confirm) {
110
113
  return textResult([
@@ -112,6 +115,7 @@ export async function createCampaign(client, config, args) {
112
115
  "",
113
116
  summaryTable,
114
117
  ...(inactiveWarning ? ["", inactiveWarning] : []),
118
+ ...linked.notes.flatMap((note) => ["", note]),
115
119
  "",
116
120
  "Call again with `confirm: true` to create it. The tracking link is returned after create.",
117
121
  ].join("\n"));
@@ -149,9 +153,11 @@ export async function createCampaign(client, config, args) {
149
153
  payoutNote = `⚠️ campaign created, but setting the payout rule failed: ${message}`;
150
154
  }
151
155
  }
152
- // 5. Ready-to-paste tracking link with the sub convention prefilled.
156
+ // 5. Ready-to-paste tracking link: linked source template when present,
157
+ // otherwise the generic sub convention.
153
158
  const trackingLink = buildTrackingLink(integration.baseUrl, created.id, zone.id, {
154
159
  subLabels: integration.subLabels,
160
+ template: linked.template,
155
161
  });
156
162
  return textResult([
157
163
  `✅ Campaign **${mdCell(created.name)}** created (id \`${created.id}\`).`,
@@ -165,16 +171,22 @@ export async function createCampaign(client, config, args) {
165
171
  `| Model | CPA, rate 0 (defaults — no internal advertiser billing) |`,
166
172
  `| Status | ${created.status || "paused"} (activate before sending traffic through either URL) |`,
167
173
  `| Zone | ${mdCell(zone.name)} (\`${zone.id}\`) — ${zonePostbackNote(zone)} |`,
174
+ ...(linked.sourceLabel ? [`| Traffic source | ${linked.sourceLabel} |`] : []),
168
175
  ...(inactiveWarning ? ["", inactiveWarning] : []),
176
+ ...linked.notes.flatMap((note) => ["", note]),
169
177
  "",
170
178
  "**Tracking link** (give this to the traffic source):",
171
179
  "```",
172
180
  trackingLink,
173
181
  "```",
174
- "Replace each `{…}` placeholder with the source's macro; `{clickid}` is already " +
175
- "correct for RichAds. Drop sub slots you don't need. Append `&cost={cost}` " +
176
- "(the network's cost macro) to import media cost for ROI. This link returns " +
177
- "404 while the campaign is paused; run it with `set_campaign_status` before use.",
182
+ linked.template !== undefined
183
+ ? "Query parameters come from the linked source's tracking template. Edit them " +
184
+ "with `update_traffic_source`. This link returns 404 while the campaign is " +
185
+ "paused; run it with `set_campaign_status` before use."
186
+ : "Replace each `{…}` placeholder with the source's macro and drop sub slots you " +
187
+ "don't need. Append `&cost=<network cost macro>` to import media cost for ROI. " +
188
+ "This link returns 404 while the campaign is paused; run it with " +
189
+ "`set_campaign_status` before use.",
178
190
  "",
179
191
  "_Only defaults were set — use `set_targeting_rule`, `set_payout_rule`, and budget fields on `update_campaign` for the rest._",
180
192
  "_Need this link again later, or the rotating /serve URL instead? `get_tracking_link` / `get_zone_url`._",
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
+ import type { AffsetClient } from "../client.js";
4
+ /** Matches lite-adserver's TENANT_ROLES. */
5
+ declare const TENANT_ROLES: readonly ["owner", "manager", "advertiser_manager", "publisher_manager", "advertiser", "publisher"];
6
+ type TenantRole = (typeof TENANT_ROLES)[number];
7
+ declare const PERMISSION_VALUES: readonly ["read", "write"];
8
+ export declare const CREATE_TEAM_MEMBER_DESCRIPTION: string;
9
+ export declare const createTeamMemberInputSchema: {
10
+ email: z.ZodString;
11
+ role: z.ZodEnum<["owner", "manager", "advertiser_manager", "publisher_manager", "advertiser", "publisher"]>;
12
+ manager_email: z.ZodOptional<z.ZodString>;
13
+ permissions: z.ZodOptional<z.ZodArray<z.ZodEnum<["read", "write"]>, "atleastone">>;
14
+ expires_at: z.ZodOptional<z.ZodNumber>;
15
+ confirm: z.ZodDefault<z.ZodBoolean>;
16
+ };
17
+ type CreateTeamMemberArgs = {
18
+ email: string;
19
+ role: TenantRole;
20
+ manager_email?: string;
21
+ permissions?: (typeof PERMISSION_VALUES)[number][];
22
+ expires_at?: number;
23
+ confirm: boolean;
24
+ };
25
+ export declare function createTeamMember(client: AffsetClient, args: CreateTeamMemberArgs): Promise<CallToolResult>;
26
+ export {};
@@ -0,0 +1,25 @@
1
+ import { z } from "zod";
2
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
+ import type { AffsetClient } from "../client.js";
4
+ import { TRAFFIC_SOURCE_STATUSES } from "../types.js";
5
+ export declare const CREATE_TRAFFIC_SOURCE_DESCRIPTION: string;
6
+ export declare const createTrafficSourceInputSchema: {
7
+ name: z.ZodString;
8
+ preset: z.ZodOptional<z.ZodString>;
9
+ tracking_template: z.ZodOptional<z.ZodString>;
10
+ postback_template: z.ZodOptional<z.ZodString>;
11
+ api_token: z.ZodOptional<z.ZodString>;
12
+ status: z.ZodOptional<z.ZodEnum<["active", "archived"]>>;
13
+ confirm: z.ZodDefault<z.ZodBoolean>;
14
+ };
15
+ type CreateTrafficSourceArgs = {
16
+ name: string;
17
+ preset?: string;
18
+ tracking_template?: string;
19
+ postback_template?: string;
20
+ api_token?: string;
21
+ status?: (typeof TRAFFIC_SOURCE_STATUSES)[number];
22
+ confirm: boolean;
23
+ };
24
+ export declare function createTrafficSource(client: AffsetClient, args: CreateTrafficSourceArgs): Promise<CallToolResult>;
25
+ export {};