@affset/mcp 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -54
- package/dist/lib/bids.d.ts +21 -0
- package/dist/lib/bids.js +84 -0
- package/dist/lib/integrationUrls.d.ts +34 -4
- package/dist/lib/integrationUrls.js +84 -4
- package/dist/lib/linkArgs.d.ts +5 -0
- package/dist/lib/linkArgs.js +14 -3
- package/dist/lib/zones.js +9 -1
- package/dist/registerTools.js +60 -0
- package/dist/tools/createCampaign.js +20 -8
- package/dist/tools/createTrafficSource.d.ts +25 -0
- package/dist/tools/createTrafficSource.js +191 -0
- package/dist/tools/createZone.d.ts +2 -0
- package/dist/tools/createZone.js +13 -1
- package/dist/tools/getStats.d.ts +2 -0
- package/dist/tools/getStats.js +19 -3
- package/dist/tools/getTrackingLink.js +26 -9
- package/dist/tools/getZoneUrl.js +30 -13
- package/dist/tools/listConversions.d.ts +2 -0
- package/dist/tools/listConversions.js +29 -10
- package/dist/tools/listSourceBids.d.ts +12 -0
- package/dist/tools/listSourceBids.js +51 -0
- package/dist/tools/listTrafficSources.d.ts +17 -0
- package/dist/tools/listTrafficSources.js +63 -0
- package/dist/tools/listZones.js +10 -5
- package/dist/tools/setSourceBid.d.ts +22 -0
- package/dist/tools/setSourceBid.js +195 -0
- package/dist/tools/updateTrafficSource.d.ts +25 -0
- package/dist/tools/updateTrafficSource.js +171 -0
- package/dist/tools/updateZone.d.ts +2 -0
- package/dist/tools/updateZone.js +13 -3
- package/dist/types.d.ts +99 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AffsetApiError } from "../client.js";
|
|
3
|
+
import { SOURCE_CLICK_ID_PARAM, templateHasParam } from "../lib/integrationUrls.js";
|
|
4
|
+
import { displayValue, renderDiff } from "../lib/patch.js";
|
|
5
|
+
import { errorResult, textError, textResult } from "../lib/toolResult.js";
|
|
6
|
+
import { httpUrlError } from "../lib/urls.js";
|
|
7
|
+
import { TRAFFIC_SOURCE_STATUSES, } from "../types.js";
|
|
8
|
+
const NAME_MAX = 200;
|
|
9
|
+
const TEMPLATE_MAX = 2000;
|
|
10
|
+
const TOKEN_MAX = 500;
|
|
11
|
+
/** Zod: replacement string, or null to clear. */
|
|
12
|
+
const clearableTemplate = z.union([z.string().max(TEMPLATE_MAX), z.null()]);
|
|
13
|
+
export const UPDATE_TRAFFIC_SOURCE_DESCRIPTION = "Update a traffic source (name, tracking_template, postback_template, api_token, " +
|
|
14
|
+
'status). Partial update — only provided fields change; pass null (or "") to clear a ' +
|
|
15
|
+
"template or the stored api_token. Zones linked to the source pick the new tracking " +
|
|
16
|
+
"template up immediately in get_zone_url/get_tracking_link. " +
|
|
17
|
+
"DRY-RUN by default; pass confirm=true to apply.";
|
|
18
|
+
export const updateTrafficSourceInputSchema = {
|
|
19
|
+
traffic_source_id: z.string().trim().min(1).describe("Traffic source id to update."),
|
|
20
|
+
name: z
|
|
21
|
+
.string()
|
|
22
|
+
.trim()
|
|
23
|
+
.min(1)
|
|
24
|
+
.max(NAME_MAX)
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("New display name (unique per tenant)."),
|
|
27
|
+
tracking_template: clearableTemplate
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("New tracking template (query string appended to the zone URL; must not start " +
|
|
30
|
+
"with ? or &), or null to clear it."),
|
|
31
|
+
postback_template: clearableTemplate
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("New postback template (http(s) URL with our macros), or null to clear it."),
|
|
34
|
+
api_token: z
|
|
35
|
+
.union([z.string().max(TOKEN_MAX), z.null()])
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('Replacement network API credential (stored write-only), or null/"" to clear the ' +
|
|
38
|
+
"stored one. Omit to leave it unchanged."),
|
|
39
|
+
status: z
|
|
40
|
+
.enum(TRAFFIC_SOURCE_STATUSES)
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("active or archived. Archiving keeps the source linked to its zones — deletion is " +
|
|
43
|
+
"only possible once no zone references it."),
|
|
44
|
+
confirm: z
|
|
45
|
+
.boolean()
|
|
46
|
+
.default(false)
|
|
47
|
+
.describe("false = dry-run preview (default). true = apply the update."),
|
|
48
|
+
};
|
|
49
|
+
export async function updateTrafficSource(client, args) {
|
|
50
|
+
try {
|
|
51
|
+
const patch = buildPatch(args);
|
|
52
|
+
if ("error" in patch)
|
|
53
|
+
return textError(patch.error);
|
|
54
|
+
if (Object.keys(patch.body).length === 0) {
|
|
55
|
+
return textError("Provide at least one field to update: name, tracking_template, " +
|
|
56
|
+
"postback_template, api_token, status.");
|
|
57
|
+
}
|
|
58
|
+
let existing;
|
|
59
|
+
try {
|
|
60
|
+
existing = await client.get(`/api/traffic-sources/${encodeURIComponent(args.traffic_source_id)}`);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (err instanceof AffsetApiError && err.status === 404) {
|
|
64
|
+
return textError(`Traffic source \`${args.traffic_source_id}\` not found in this namespace.`);
|
|
65
|
+
}
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
const changes = diffSource(existing, patch.body);
|
|
69
|
+
if (changes.length === 0) {
|
|
70
|
+
return textResult(`Nothing to change on traffic source \`${existing.id}\` (${existing.name}) — ` +
|
|
71
|
+
"provided fields already match current values.");
|
|
72
|
+
}
|
|
73
|
+
const warnings = [];
|
|
74
|
+
const linkedZones = existing.linked_zones ?? 0;
|
|
75
|
+
if ("tracking_template" in patch.body && linkedZones > 0) {
|
|
76
|
+
warnings.push(`The new tracking template applies to the ${linkedZones} linked ` +
|
|
77
|
+
`zone${linkedZones === 1 ? "" : "s"} the next time a URL is rendered — links ` +
|
|
78
|
+
"already pasted into network campaigns keep their old parameters until replaced.");
|
|
79
|
+
}
|
|
80
|
+
if (typeof patch.body.tracking_template === "string" &&
|
|
81
|
+
patch.body.tracking_template !== "" &&
|
|
82
|
+
!templateHasParam(patch.body.tracking_template, SOURCE_CLICK_ID_PARAM)) {
|
|
83
|
+
warnings.push("⚠️ The new tracking template has no `source_click_id=` parameter — the " +
|
|
84
|
+
"network's click token will not be captured on linked zones' URLs.");
|
|
85
|
+
}
|
|
86
|
+
if (patch.body.status === "archived" && existing.status !== "archived") {
|
|
87
|
+
warnings.push("Archived sources stay linked to their zones and keep rendering their template; " +
|
|
88
|
+
"archiving only hides the source from active pickers.");
|
|
89
|
+
}
|
|
90
|
+
if (!args.confirm) {
|
|
91
|
+
return textResult([
|
|
92
|
+
`**Dry run** — would update traffic source \`${existing.id}\` (${existing.name}).`,
|
|
93
|
+
"",
|
|
94
|
+
renderDiff(changes),
|
|
95
|
+
...(warnings.length ? ["", ...warnings] : []),
|
|
96
|
+
"",
|
|
97
|
+
"Call again with `confirm: true` to apply.",
|
|
98
|
+
].join("\n"));
|
|
99
|
+
}
|
|
100
|
+
const updated = await client.put(`/api/traffic-sources/${encodeURIComponent(args.traffic_source_id)}`, patch.body);
|
|
101
|
+
return textResult([
|
|
102
|
+
`✅ Traffic source \`${updated.id}\` updated.`,
|
|
103
|
+
"",
|
|
104
|
+
renderDiff(changes),
|
|
105
|
+
...(warnings.length ? ["", ...warnings] : []),
|
|
106
|
+
].join("\n"));
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
return errorResult(err);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function buildPatch(args) {
|
|
113
|
+
const body = {};
|
|
114
|
+
if (args.name !== undefined)
|
|
115
|
+
body.name = args.name;
|
|
116
|
+
if (args.status !== undefined)
|
|
117
|
+
body.status = args.status;
|
|
118
|
+
// Templates clear with "" — the server rejects null for them (api_token is the
|
|
119
|
+
// one field where null also clears).
|
|
120
|
+
if (args.tracking_template !== undefined) {
|
|
121
|
+
const value = args.tracking_template === null ? "" : args.tracking_template.trim();
|
|
122
|
+
if (value && /^[?&]/.test(value)) {
|
|
123
|
+
return {
|
|
124
|
+
error: "tracking_template must not start with ? or & — it is appended after ? automatically.",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
body.tracking_template = value;
|
|
128
|
+
}
|
|
129
|
+
if (args.postback_template !== undefined) {
|
|
130
|
+
const value = args.postback_template === null ? "" : args.postback_template.trim();
|
|
131
|
+
if (value) {
|
|
132
|
+
const err = httpUrlError(value, "postback_template");
|
|
133
|
+
if (err)
|
|
134
|
+
return { error: err };
|
|
135
|
+
}
|
|
136
|
+
body.postback_template = value;
|
|
137
|
+
}
|
|
138
|
+
if (args.api_token !== undefined) {
|
|
139
|
+
body.api_token = args.api_token === null ? null : args.api_token.trim();
|
|
140
|
+
}
|
|
141
|
+
return { body };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* api_token never round-trips: the existing row only carries has_api_token, and
|
|
145
|
+
* the diff must not echo the new value either.
|
|
146
|
+
*/
|
|
147
|
+
function diffSource(existing, body) {
|
|
148
|
+
const changes = [];
|
|
149
|
+
for (const [field, to] of Object.entries(body)) {
|
|
150
|
+
if (field === "api_token") {
|
|
151
|
+
const from = existing.has_api_token ? "(set)" : "(none)";
|
|
152
|
+
const toDisplay = to === null || to === "" ? "(cleared)" : "(set — write-only)";
|
|
153
|
+
if (existing.has_api_token && (to === null || to === "")) {
|
|
154
|
+
changes.push({ field, from, to: toDisplay });
|
|
155
|
+
}
|
|
156
|
+
else if (to !== null && to !== "") {
|
|
157
|
+
changes.push({ field, from, to: toDisplay });
|
|
158
|
+
}
|
|
159
|
+
else if (!existing.has_api_token && (to === null || to === "")) {
|
|
160
|
+
// Clearing an absent token is a no-op; skip so "nothing to change" stays honest.
|
|
161
|
+
}
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const from = existing[field];
|
|
165
|
+
if (String(from ?? "") === String(to ?? ""))
|
|
166
|
+
continue;
|
|
167
|
+
changes.push({ field, from: displayValue(from ?? null), to: displayValue(to) });
|
|
168
|
+
}
|
|
169
|
+
return changes;
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=updateTrafficSource.js.map
|
|
@@ -10,6 +10,7 @@ export declare const updateZoneInputSchema: {
|
|
|
10
10
|
postback_url: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNull]>>;
|
|
11
11
|
site_url: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNull]>>;
|
|
12
12
|
traffic_back_url: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNull]>>;
|
|
13
|
+
traffic_source_id: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodNull]>>;
|
|
13
14
|
confirm: z.ZodDefault<z.ZodBoolean>;
|
|
14
15
|
};
|
|
15
16
|
type UpdateZoneArgs = {
|
|
@@ -19,6 +20,7 @@ type UpdateZoneArgs = {
|
|
|
19
20
|
postback_url?: string | null;
|
|
20
21
|
site_url?: string | null;
|
|
21
22
|
traffic_back_url?: string | null;
|
|
23
|
+
traffic_source_id?: string | null;
|
|
22
24
|
confirm: boolean;
|
|
23
25
|
};
|
|
24
26
|
export declare function updateZone(client: AffsetClient, args: UpdateZoneArgs): Promise<CallToolResult>;
|
package/dist/tools/updateZone.js
CHANGED
|
@@ -6,8 +6,10 @@ import { httpUrlError } from "../lib/urls.js";
|
|
|
6
6
|
import { ZONE_STATUSES } from "../types.js";
|
|
7
7
|
/** Zod: optional http(s) URL, or null to clear. */
|
|
8
8
|
const clearableUrl = z.union([z.string().min(1), z.null()]);
|
|
9
|
-
export const UPDATE_ZONE_DESCRIPTION = "Update a traffic-source zone (name, status, postback_url, site_url, traffic_back_url
|
|
10
|
-
"Partial update — only provided fields change. Pass null for a URL
|
|
9
|
+
export const UPDATE_ZONE_DESCRIPTION = "Update a traffic-source zone (name, status, postback_url, site_url, traffic_back_url, " +
|
|
10
|
+
"traffic_source_id). Partial update — only provided fields change. Pass null for a URL " +
|
|
11
|
+
"field or traffic_source_id to clear it. Linking a traffic source makes " +
|
|
12
|
+
"get_zone_url/get_tracking_link render its tracking template. " +
|
|
11
13
|
"DRY-RUN by default; pass confirm=true to apply.";
|
|
12
14
|
export const updateZoneInputSchema = {
|
|
13
15
|
zone_id: z.string().min(1).describe("Zone id to update."),
|
|
@@ -18,6 +20,11 @@ export const updateZoneInputSchema = {
|
|
|
18
20
|
.describe("S2S postback URL, or null to clear. Prefer including {source_click_id}."),
|
|
19
21
|
site_url: clearableUrl.optional().describe("Site URL, or null to clear."),
|
|
20
22
|
traffic_back_url: clearableUrl.optional().describe("Traffic-back URL, or null to clear."),
|
|
23
|
+
traffic_source_id: z
|
|
24
|
+
.union([z.string().trim().min(1), z.null()])
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Traffic source to link (see list_traffic_sources; must be in this namespace), " +
|
|
27
|
+
"or null to unlink."),
|
|
21
28
|
confirm: z
|
|
22
29
|
.boolean()
|
|
23
30
|
.default(false)
|
|
@@ -29,7 +36,8 @@ export async function updateZone(client, args) {
|
|
|
29
36
|
if ("error" in patch)
|
|
30
37
|
return textError(patch.error);
|
|
31
38
|
if (Object.keys(patch.body).length === 0) {
|
|
32
|
-
return textError("Provide at least one field to update: name, status, postback_url, site_url,
|
|
39
|
+
return textError("Provide at least one field to update: name, status, postback_url, site_url, " +
|
|
40
|
+
"traffic_back_url, traffic_source_id.");
|
|
33
41
|
}
|
|
34
42
|
let existing;
|
|
35
43
|
try {
|
|
@@ -103,6 +111,8 @@ function buildPatch(args) {
|
|
|
103
111
|
return { error: err };
|
|
104
112
|
body[key] = args[key];
|
|
105
113
|
}
|
|
114
|
+
if (args.traffic_source_id !== undefined)
|
|
115
|
+
body.traffic_source_id = args.traffic_source_id;
|
|
106
116
|
return { body };
|
|
107
117
|
}
|
|
108
118
|
function diffZone(existing, body) {
|
package/dist/types.d.ts
CHANGED
|
@@ -111,6 +111,9 @@ export interface Zone {
|
|
|
111
111
|
postback_url?: string | null;
|
|
112
112
|
user_email?: string | null;
|
|
113
113
|
manager_email?: string | null;
|
|
114
|
+
/** Linked traffic source; its tracking template drives the URL tools. */
|
|
115
|
+
traffic_source_id?: string | null;
|
|
116
|
+
traffic_source_name?: string | null;
|
|
114
117
|
created_at?: number;
|
|
115
118
|
updated_at?: number;
|
|
116
119
|
}
|
|
@@ -131,6 +134,101 @@ export interface UpdateCampaignResponse {
|
|
|
131
134
|
id: number;
|
|
132
135
|
updated_at: number;
|
|
133
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* One row from GET /api/traffic-sources. The stored network API credential is
|
|
139
|
+
* write-only — reads carry `has_api_token`, never the token itself.
|
|
140
|
+
*/
|
|
141
|
+
export interface TrafficSource {
|
|
142
|
+
id: string;
|
|
143
|
+
name: string;
|
|
144
|
+
preset: string | null;
|
|
145
|
+
tracking_template: string;
|
|
146
|
+
postback_template: string;
|
|
147
|
+
has_api_token: boolean;
|
|
148
|
+
status: string;
|
|
149
|
+
created_at?: number;
|
|
150
|
+
updated_at?: number;
|
|
151
|
+
/** Present on GET /api/traffic-sources/{id} only. */
|
|
152
|
+
linked_zones?: number;
|
|
153
|
+
}
|
|
154
|
+
export interface TrafficSourcesResponse {
|
|
155
|
+
traffic_sources: TrafficSource[];
|
|
156
|
+
pagination: Pagination;
|
|
157
|
+
}
|
|
158
|
+
export interface CreateTrafficSourceResponse {
|
|
159
|
+
id: string;
|
|
160
|
+
status: string;
|
|
161
|
+
created_at: number;
|
|
162
|
+
}
|
|
163
|
+
export interface UpdateTrafficSourceResponse {
|
|
164
|
+
id: string;
|
|
165
|
+
updated_at: number;
|
|
166
|
+
}
|
|
167
|
+
/** One entry from GET /api/traffic-source-presets. */
|
|
168
|
+
export interface TrafficSourcePreset {
|
|
169
|
+
id: string;
|
|
170
|
+
name: string;
|
|
171
|
+
doc_url: string;
|
|
172
|
+
tracking_template: string;
|
|
173
|
+
postback_template: string;
|
|
174
|
+
sub_meanings: Partial<Record<SubKey, string>>;
|
|
175
|
+
notes: string;
|
|
176
|
+
}
|
|
177
|
+
export interface TrafficSourcePresetsResponse {
|
|
178
|
+
presets: TrafficSourcePreset[];
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* One write that entered Affset's mutation stage
|
|
182
|
+
* (GET /api/traffic-sources/{id}/bid-changes and `last_change` on the bids list).
|
|
183
|
+
* `changed_by` is the caller's email.
|
|
184
|
+
*/
|
|
185
|
+
export interface SourceBidChange {
|
|
186
|
+
id: string;
|
|
187
|
+
source_id: string;
|
|
188
|
+
network_campaign_id: string;
|
|
189
|
+
previous_bid: number | null;
|
|
190
|
+
bid: number;
|
|
191
|
+
currency: string;
|
|
192
|
+
pricing_model: string;
|
|
193
|
+
status: "pending" | "applied" | "failed";
|
|
194
|
+
error: string | null;
|
|
195
|
+
changed_by: string;
|
|
196
|
+
created_at: number;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* One network campaign from GET /api/traffic-sources/{id}/bids — read live from
|
|
200
|
+
* the network account. `name` and `status_label` are the network's own text.
|
|
201
|
+
*/
|
|
202
|
+
export interface SourceBidCampaign {
|
|
203
|
+
network_campaign_id: string;
|
|
204
|
+
name: string;
|
|
205
|
+
status: "active" | "paused" | "other";
|
|
206
|
+
status_label: string;
|
|
207
|
+
bid: number | null;
|
|
208
|
+
currency: string;
|
|
209
|
+
pricing_model: string;
|
|
210
|
+
extra: Record<string, number>;
|
|
211
|
+
last_change: SourceBidChange | null;
|
|
212
|
+
}
|
|
213
|
+
export interface SourceBidsResponse {
|
|
214
|
+
campaigns: SourceBidCampaign[];
|
|
215
|
+
fetched_at: number;
|
|
216
|
+
}
|
|
217
|
+
/** Response of POST /api/traffic-sources/{id}/bids. */
|
|
218
|
+
export interface SetSourceBidResponse {
|
|
219
|
+
change: SourceBidChange;
|
|
220
|
+
campaign: {
|
|
221
|
+
network_campaign_id: string;
|
|
222
|
+
name: string;
|
|
223
|
+
status: string;
|
|
224
|
+
bid: number | null;
|
|
225
|
+
previous_bid: number | null;
|
|
226
|
+
currency: string;
|
|
227
|
+
pricing_model: string;
|
|
228
|
+
};
|
|
229
|
+
audit_finalized: boolean;
|
|
230
|
+
warning?: string;
|
|
231
|
+
}
|
|
134
232
|
/** Subset of GET /api/tenant this server reads. */
|
|
135
233
|
export interface TenantSettingsResponse {
|
|
136
234
|
company?: string;
|
|
@@ -206,5 +304,6 @@ export declare const GROUP_BY_VALUES: readonly ["date", "campaign_id", "zone_id"
|
|
|
206
304
|
export type GroupBy = (typeof GROUP_BY_VALUES)[number];
|
|
207
305
|
export declare const CAMPAIGN_STATUSES: readonly ["active", "paused", "archived"];
|
|
208
306
|
export declare const ZONE_STATUSES: readonly ["active", "inactive"];
|
|
307
|
+
export declare const TRAFFIC_SOURCE_STATUSES: readonly ["active", "archived"];
|
|
209
308
|
export declare const PAYMENT_MODELS: readonly ["cpa", "cpm"];
|
|
210
309
|
export declare const PACING_VALUES: readonly ["asap", "even"];
|
package/dist/types.js
CHANGED
|
@@ -14,6 +14,7 @@ export const GROUP_BY_VALUES = [
|
|
|
14
14
|
];
|
|
15
15
|
export const CAMPAIGN_STATUSES = ["active", "paused", "archived"];
|
|
16
16
|
export const ZONE_STATUSES = ["active", "inactive"];
|
|
17
|
+
export const TRAFFIC_SOURCE_STATUSES = ["active", "archived"];
|
|
17
18
|
export const PAYMENT_MODELS = ["cpa", "cpm"];
|
|
18
19
|
export const PACING_VALUES = ["asap", "even"];
|
|
19
20
|
//# sourceMappingURL=types.js.map
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@affset/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"mcpName": "io.github.affset/mcp",
|
|
5
5
|
"description": "MCP server for the affset ad platform — stats, campaigns, zones, payouts, targeting, sub labels, and team from your chat client.",
|
|
6
6
|
"type": "module",
|