@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.
- package/.env.example +5 -0
- package/README.md +191 -56
- package/dist/client.d.ts +43 -0
- package/dist/client.js +15 -2
- package/dist/config.d.ts +7 -0
- package/dist/config.js +15 -54
- package/dist/core.d.ts +18 -0
- package/dist/core.js +18 -0
- package/dist/docs.d.ts +39 -0
- package/dist/docs.js +105 -0
- package/dist/index.d.ts +2 -0
- package/dist/lib/format.d.ts +46 -0
- package/dist/lib/format.js +3 -0
- package/dist/lib/integrationUrls.d.ts +87 -0
- package/dist/lib/integrationUrls.js +84 -4
- package/dist/lib/linkArgs.d.ts +32 -0
- package/dist/lib/linkArgs.js +14 -3
- package/dist/lib/patch.d.ts +10 -0
- package/dist/lib/payoutRules.d.ts +38 -0
- package/dist/lib/readBody.d.ts +17 -0
- package/dist/lib/readBody.js +69 -0
- package/dist/lib/targeting.d.ts +51 -0
- package/dist/lib/time.d.ts +38 -0
- package/dist/lib/toolResult.d.ts +7 -0
- package/dist/lib/urls.d.ts +2 -0
- package/dist/lib/zones.d.ts +24 -0
- package/dist/lib/zones.js +9 -1
- package/dist/registerTools.d.ts +40 -0
- package/dist/registerTools.js +437 -0
- package/dist/runtimeConfig.d.ts +37 -0
- package/dist/runtimeConfig.js +93 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +3 -318
- package/dist/tools/createCampaign.d.ts +25 -0
- package/dist/tools/createCampaign.js +20 -8
- package/dist/tools/createTeamMember.d.ts +26 -0
- package/dist/tools/createTrafficSource.d.ts +25 -0
- package/dist/tools/createTrafficSource.js +191 -0
- package/dist/tools/createZone.d.ts +24 -0
- package/dist/tools/createZone.js +13 -1
- package/dist/tools/cutZones.d.ts +29 -0
- package/dist/tools/deletePayoutRule.d.ts +16 -0
- package/dist/tools/getCampaign.d.ts +12 -0
- package/dist/tools/getCampaign.js +150 -0
- package/dist/tools/getStats.d.ts +42 -0
- package/dist/tools/getStats.js +54 -3
- package/dist/tools/getTrackingLink.d.ts +23 -0
- package/dist/tools/getTrackingLink.js +26 -9
- package/dist/tools/getZoneUrl.d.ts +21 -0
- package/dist/tools/getZoneUrl.js +30 -13
- package/dist/tools/listCampaigns.d.ts +23 -0
- package/dist/tools/listConversions.d.ts +31 -0
- package/dist/tools/listConversions.js +29 -10
- package/dist/tools/listPayoutRules.d.ts +12 -0
- package/dist/tools/listSubLabels.d.ts +5 -0
- package/dist/tools/listTargetingRules.d.ts +12 -0
- package/dist/tools/listTargetingTypes.d.ts +5 -0
- package/dist/tools/listTeam.d.ts +14 -0
- package/dist/tools/listTrafficSources.d.ts +17 -0
- package/dist/tools/listTrafficSources.js +63 -0
- package/dist/tools/listZones.d.ts +23 -0
- package/dist/tools/listZones.js +10 -5
- package/dist/tools/removeTargetingRule.d.ts +21 -0
- package/dist/tools/setCampaignStatus.d.ts +18 -0
- package/dist/tools/setPayoutGoal.d.ts +16 -0
- package/dist/tools/setPayoutRule.d.ts +18 -0
- package/dist/tools/setSubLabels.d.ts +22 -0
- package/dist/tools/setTargetingRule.d.ts +21 -0
- package/dist/tools/updateCampaign.d.ts +35 -0
- package/dist/tools/updateTrafficSource.d.ts +25 -0
- package/dist/tools/updateTrafficSource.js +171 -0
- package/dist/tools/updateZone.d.ts +27 -0
- package/dist/tools/updateZone.js +13 -3
- package/dist/tools/whoami.d.ts +6 -0
- package/dist/types.d.ts +257 -0
- package/dist/types.js +2 -0
- package/dist/version.d.ts +7 -0
- package/dist/version.js +8 -0
- package/package.json +15 -2
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { mdCell } from "../lib/format.js";
|
|
3
|
+
import { SOURCE_CLICK_ID_PARAM, templateHasParam } from "../lib/integrationUrls.js";
|
|
4
|
+
import { errorResult, textError, textResult } from "../lib/toolResult.js";
|
|
5
|
+
import { httpUrlError } from "../lib/urls.js";
|
|
6
|
+
import { TRAFFIC_SOURCE_STATUSES, } from "../types.js";
|
|
7
|
+
const NAME_MAX = 200;
|
|
8
|
+
const TEMPLATE_MAX = 2000;
|
|
9
|
+
const TOKEN_MAX = 500;
|
|
10
|
+
export const CREATE_TRAFFIC_SOURCE_DESCRIPTION = "Create a traffic source — an ad network account bought from. Creating from a preset " +
|
|
11
|
+
"(exoclick, trafficstars, propellerads, adsterra, richads) copies the network's " +
|
|
12
|
+
"verified tracking + postback templates into an editable row; [BRACKETED] pieces in a " +
|
|
13
|
+
"postback template are account-specific values to fill in. Link zones to the source " +
|
|
14
|
+
"via create_zone/update_zone traffic_source_id, and get_zone_url renders its template. " +
|
|
15
|
+
"The api_token is stored write-only for upcoming cost sync. " +
|
|
16
|
+
"DRY-RUN by default; pass confirm=true to apply.";
|
|
17
|
+
export const createTrafficSourceInputSchema = {
|
|
18
|
+
name: z
|
|
19
|
+
.string()
|
|
20
|
+
.trim()
|
|
21
|
+
.min(1)
|
|
22
|
+
.max(NAME_MAX)
|
|
23
|
+
.describe('Display name, unique per tenant — e.g. "ExoClick — main".'),
|
|
24
|
+
preset: z
|
|
25
|
+
.string()
|
|
26
|
+
.trim()
|
|
27
|
+
.min(1)
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Network preset id to copy templates from (e.g. exoclick, trafficstars, " +
|
|
30
|
+
"propellerads, adsterra, richads). The row stays fully editable and remembers " +
|
|
31
|
+
"its preset. Omit for a custom source."),
|
|
32
|
+
tracking_template: z
|
|
33
|
+
.string()
|
|
34
|
+
.max(TEMPLATE_MAX)
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Query string appended to the zone URL — our param names on the left, the " +
|
|
37
|
+
"network's macros on the right. Overrides the preset's template. Must not start " +
|
|
38
|
+
"with ? or &."),
|
|
39
|
+
postback_template: z
|
|
40
|
+
.string()
|
|
41
|
+
.max(TEMPLATE_MAX)
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("The network's S2S conversion endpoint using our postback macros " +
|
|
44
|
+
"({source_click_id}, {payout}). Overrides the preset's template. Prefills the " +
|
|
45
|
+
"linked zones' postback_url suggestion — never applied automatically."),
|
|
46
|
+
api_token: z
|
|
47
|
+
.string()
|
|
48
|
+
.min(1)
|
|
49
|
+
.max(TOKEN_MAX)
|
|
50
|
+
.optional()
|
|
51
|
+
.describe("Network API credential for upcoming cost sync. Stored write-only — reads only " +
|
|
52
|
+
"ever return has_api_token."),
|
|
53
|
+
status: z.enum(TRAFFIC_SOURCE_STATUSES).optional().describe("active (default) or archived."),
|
|
54
|
+
confirm: z
|
|
55
|
+
.boolean()
|
|
56
|
+
.default(false)
|
|
57
|
+
.describe("false = dry-run preview (default). true = create the traffic source."),
|
|
58
|
+
};
|
|
59
|
+
export async function createTrafficSource(client, args) {
|
|
60
|
+
try {
|
|
61
|
+
const trackingTemplate = args.tracking_template?.trim();
|
|
62
|
+
if (trackingTemplate && /^[?&]/.test(trackingTemplate)) {
|
|
63
|
+
return textError("tracking_template must not start with ? or & — it is appended after ? automatically.");
|
|
64
|
+
}
|
|
65
|
+
if (args.postback_template?.trim()) {
|
|
66
|
+
const err = httpUrlError(args.postback_template.trim(), "postback_template");
|
|
67
|
+
if (err)
|
|
68
|
+
return textError(err);
|
|
69
|
+
}
|
|
70
|
+
const apiToken = args.api_token?.trim();
|
|
71
|
+
if (args.api_token !== undefined && !apiToken) {
|
|
72
|
+
return textError("api_token must be a non-empty string.");
|
|
73
|
+
}
|
|
74
|
+
// The server copies preset templates on create; the catalog is fetched only
|
|
75
|
+
// to preview them in the dry run. An unreadable catalog is not an error —
|
|
76
|
+
// the server stays the arbiter of preset validity on the actual create.
|
|
77
|
+
const lookup = args.preset
|
|
78
|
+
? await fetchPreset(client, args.preset)
|
|
79
|
+
: { state: "none" };
|
|
80
|
+
if (lookup.state === "unknown") {
|
|
81
|
+
const listed = lookup.ids.length > 0
|
|
82
|
+
? `Valid ids: ${lookup.ids.map((id) => `\`${mdCell(id)}\``).join(", ")}.`
|
|
83
|
+
: "The preset catalog was empty.";
|
|
84
|
+
return textError(`Unknown preset \`${mdCell(args.preset)}\`. ${listed} Omit preset for a custom source.`);
|
|
85
|
+
}
|
|
86
|
+
const preset = lookup.state === "found" ? lookup.preset : undefined;
|
|
87
|
+
// Catalog ids are lowercase; send the canonical id when we have it so
|
|
88
|
+
// "ExoClick" still creates, including when the catalog itself is down.
|
|
89
|
+
const presetId = lookup.state === "found"
|
|
90
|
+
? lookup.preset.id
|
|
91
|
+
: args.preset
|
|
92
|
+
? args.preset.toLowerCase()
|
|
93
|
+
: undefined;
|
|
94
|
+
const presetFallbackCell = lookup.state === "unavailable" ? "_(copied from preset — catalog could not be read)_" : "—";
|
|
95
|
+
const effectiveTracking = trackingTemplate !== undefined ? trackingTemplate : (preset?.tracking_template ?? "");
|
|
96
|
+
const warnings = [];
|
|
97
|
+
if (effectiveTracking && !templateHasParam(effectiveTracking, SOURCE_CLICK_ID_PARAM)) {
|
|
98
|
+
warnings.push("⚠️ The tracking template has no `source_click_id=` parameter — the " +
|
|
99
|
+
"network's click token will not be captured on linked zones' URLs.");
|
|
100
|
+
}
|
|
101
|
+
const summaryTable = [
|
|
102
|
+
"| Field | Value |",
|
|
103
|
+
"|---|---|",
|
|
104
|
+
`| Name | ${mdCell(args.name)} |`,
|
|
105
|
+
`| Preset | ${presetId ? mdCell(presetId) : "— (custom)"} |`,
|
|
106
|
+
`| Tracking template | ${templateCell(trackingTemplate, preset?.tracking_template, presetFallbackCell)} |`,
|
|
107
|
+
`| Postback template | ${templateCell(args.postback_template?.trim(), preset?.postback_template, presetFallbackCell)} |`,
|
|
108
|
+
`| API token | ${apiToken ? "(set — stored write-only)" : "—"} |`,
|
|
109
|
+
`| Status | ${args.status ?? "active"} |`,
|
|
110
|
+
...(preset?.notes ? [`| Preset notes | ${mdCell(preset.notes)} |`] : []),
|
|
111
|
+
].join("\n");
|
|
112
|
+
if (!args.confirm) {
|
|
113
|
+
return textResult([
|
|
114
|
+
"**Dry run** — would create a traffic source with:",
|
|
115
|
+
"",
|
|
116
|
+
summaryTable,
|
|
117
|
+
...(warnings.length ? ["", ...warnings] : []),
|
|
118
|
+
"",
|
|
119
|
+
"Call again with `confirm: true` to create it.",
|
|
120
|
+
].join("\n"));
|
|
121
|
+
}
|
|
122
|
+
const body = { name: args.name };
|
|
123
|
+
if (presetId !== undefined)
|
|
124
|
+
body.preset = presetId;
|
|
125
|
+
if (args.tracking_template !== undefined)
|
|
126
|
+
body.tracking_template = trackingTemplate ?? "";
|
|
127
|
+
if (args.postback_template !== undefined) {
|
|
128
|
+
body.postback_template = args.postback_template.trim();
|
|
129
|
+
}
|
|
130
|
+
if (apiToken !== undefined)
|
|
131
|
+
body.api_token = apiToken;
|
|
132
|
+
if (args.status !== undefined)
|
|
133
|
+
body.status = args.status;
|
|
134
|
+
const created = await client.post("/api/traffic-sources", body);
|
|
135
|
+
// Echo the row the server actually stored (preset templates copied in).
|
|
136
|
+
// Non-fatal; fall back to the create summary.
|
|
137
|
+
let stored = null;
|
|
138
|
+
try {
|
|
139
|
+
stored = await client.get(`/api/traffic-sources/${encodeURIComponent(created.id)}`);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
stored = null;
|
|
143
|
+
}
|
|
144
|
+
return textResult([
|
|
145
|
+
`✅ Traffic source **${mdCell(args.name)}** created (id \`${created.id}\`).`,
|
|
146
|
+
"",
|
|
147
|
+
stored
|
|
148
|
+
? [
|
|
149
|
+
"| Field | Value |",
|
|
150
|
+
"|---|---|",
|
|
151
|
+
`| Preset | ${stored.preset ? mdCell(stored.preset) : "— (custom)"} |`,
|
|
152
|
+
`| Tracking template | ${stored.tracking_template ? mdCell(stored.tracking_template) : "—"} |`,
|
|
153
|
+
`| Postback template | ${stored.postback_template ? mdCell(stored.postback_template) : "—"} |`,
|
|
154
|
+
`| API token | ${stored.has_api_token ? "set (write-only)" : "—"} |`,
|
|
155
|
+
`| Status | ${mdCell(stored.status)} |`,
|
|
156
|
+
].join("\n")
|
|
157
|
+
: summaryTable,
|
|
158
|
+
...(warnings.length ? ["", ...warnings] : []),
|
|
159
|
+
"",
|
|
160
|
+
"Link zones to it with `create_zone`/`update_zone` `traffic_source_id`; " +
|
|
161
|
+
"`get_zone_url` then renders its tracking template. Replace any `[BRACKETED]` " +
|
|
162
|
+
"values in the postback template with your account's, then use it for the " +
|
|
163
|
+
"zone's postback_url.",
|
|
164
|
+
].join("\n"));
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
return errorResult(err);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function templateCell(explicit, presetValue, fallback) {
|
|
171
|
+
if (explicit !== undefined) {
|
|
172
|
+
return explicit ? mdCell(explicit) : "— _(explicitly empty)_";
|
|
173
|
+
}
|
|
174
|
+
if (presetValue)
|
|
175
|
+
return `${mdCell(presetValue)} _(copied from preset)_`;
|
|
176
|
+
return fallback;
|
|
177
|
+
}
|
|
178
|
+
async function fetchPreset(client, presetId) {
|
|
179
|
+
try {
|
|
180
|
+
const res = await client.get("/api/traffic-source-presets");
|
|
181
|
+
const presets = res.presets ?? [];
|
|
182
|
+
const preset = presets.find((p) => p.id.toLowerCase() === presetId.toLowerCase());
|
|
183
|
+
return preset
|
|
184
|
+
? { state: "found", preset }
|
|
185
|
+
: { state: "unknown", ids: presets.map((p) => p.id) };
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return { state: "unavailable" };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=createTrafficSource.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import type { AffsetClient } from "../client.js";
|
|
4
|
+
export declare const CREATE_ZONE_DESCRIPTION: string;
|
|
5
|
+
export declare const createZoneInputSchema: {
|
|
6
|
+
name: z.ZodString;
|
|
7
|
+
postback_url: z.ZodOptional<z.ZodString>;
|
|
8
|
+
site_url: z.ZodOptional<z.ZodString>;
|
|
9
|
+
traffic_back_url: z.ZodOptional<z.ZodString>;
|
|
10
|
+
user_email: z.ZodOptional<z.ZodString>;
|
|
11
|
+
traffic_source_id: z.ZodOptional<z.ZodString>;
|
|
12
|
+
confirm: z.ZodDefault<z.ZodBoolean>;
|
|
13
|
+
};
|
|
14
|
+
type CreateZoneArgs = {
|
|
15
|
+
name: string;
|
|
16
|
+
postback_url?: string;
|
|
17
|
+
site_url?: string;
|
|
18
|
+
traffic_back_url?: string;
|
|
19
|
+
user_email?: string;
|
|
20
|
+
traffic_source_id?: string;
|
|
21
|
+
confirm: boolean;
|
|
22
|
+
};
|
|
23
|
+
export declare function createZone(client: AffsetClient, args: CreateZoneArgs): Promise<CallToolResult>;
|
|
24
|
+
export {};
|
package/dist/tools/createZone.js
CHANGED
|
@@ -4,7 +4,8 @@ import { errorResult, textError, textResult } from "../lib/toolResult.js";
|
|
|
4
4
|
import { httpUrlError } from "../lib/urls.js";
|
|
5
5
|
export const CREATE_ZONE_DESCRIPTION = "Create a traffic-source zone in the current namespace. Requires a name; optional " +
|
|
6
6
|
"postback_url (where conversions are reported back — include {source_click_id}), " +
|
|
7
|
-
"site_url, traffic_back_url,
|
|
7
|
+
"site_url, traffic_back_url, user_email (publisher owner), and traffic_source_id " +
|
|
8
|
+
"(get_zone_url then renders that source's tracking template). Status is always " +
|
|
8
9
|
"active on create. Counts against the plan's zone limit (402 if exceeded). " +
|
|
9
10
|
"DRY-RUN by default; pass confirm=true to apply.";
|
|
10
11
|
export const createZoneInputSchema = {
|
|
@@ -26,6 +27,13 @@ export const createZoneInputSchema = {
|
|
|
26
27
|
.email()
|
|
27
28
|
.optional()
|
|
28
29
|
.describe("Optional publisher email to own this zone (owner/manager only)."),
|
|
30
|
+
traffic_source_id: z
|
|
31
|
+
.string()
|
|
32
|
+
.trim()
|
|
33
|
+
.min(1)
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Traffic source to link (see list_traffic_sources). Must be a source in this " +
|
|
36
|
+
"namespace. get_zone_url/get_tracking_link then render its tracking template."),
|
|
29
37
|
confirm: z
|
|
30
38
|
.boolean()
|
|
31
39
|
.default(false)
|
|
@@ -62,6 +70,7 @@ export async function createZone(client, args) {
|
|
|
62
70
|
`| Site | ${mdCell(args.site_url ?? "—")} |`,
|
|
63
71
|
`| Traffic back | ${mdCell(args.traffic_back_url ?? "—")} |`,
|
|
64
72
|
`| Publisher | ${mdCell(args.user_email ?? "—")} |`,
|
|
73
|
+
`| Traffic source | ${args.traffic_source_id ? `\`${mdCell(args.traffic_source_id)}\`` : "—"} |`,
|
|
65
74
|
].join("\n");
|
|
66
75
|
if (!args.confirm) {
|
|
67
76
|
return textResult([
|
|
@@ -81,6 +90,8 @@ export async function createZone(client, args) {
|
|
|
81
90
|
body.traffic_back_url = args.traffic_back_url;
|
|
82
91
|
if (args.user_email !== undefined)
|
|
83
92
|
body.user_email = args.user_email;
|
|
93
|
+
if (args.traffic_source_id !== undefined)
|
|
94
|
+
body.traffic_source_id = args.traffic_source_id;
|
|
84
95
|
const created = await client.post("/api/zones", body);
|
|
85
96
|
// Create response is sparse — fetch full row for the echo.
|
|
86
97
|
let zone = null;
|
|
@@ -106,6 +117,7 @@ export async function createZone(client, args) {
|
|
|
106
117
|
`| Site | ${mdCell(zone?.site_url ?? args.site_url ?? "—")} |`,
|
|
107
118
|
`| Traffic back | ${mdCell(zone?.traffic_back_url ?? args.traffic_back_url ?? "—")} |`,
|
|
108
119
|
`| Publisher | ${mdCell(zone?.user_email ?? args.user_email ?? "—")} |`,
|
|
120
|
+
`| Traffic source | ${zone?.traffic_source_name ? mdCell(zone.traffic_source_name) : args.traffic_source_id ? `\`${mdCell(args.traffic_source_id)}\`` : "—"} |`,
|
|
109
121
|
"",
|
|
110
122
|
"Use this zone id with `create_campaign` (or pass it as zone_id) for tracking links.",
|
|
111
123
|
].join("\n"));
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import type { AffsetClient } from "../client.js";
|
|
4
|
+
import { RANGE_PRESETS } from "../lib/time.js";
|
|
5
|
+
export declare const CUT_ZONES_DESCRIPTION: string;
|
|
6
|
+
export declare const cutZonesInputSchema: {
|
|
7
|
+
campaign_id: z.ZodUnion<[z.ZodString, z.ZodNumber]>;
|
|
8
|
+
cr_max: z.ZodOptional<z.ZodNumber>;
|
|
9
|
+
spend_min: z.ZodOptional<z.ZodNumber>;
|
|
10
|
+
roi_max: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
min_clicks: z.ZodDefault<z.ZodNumber>;
|
|
12
|
+
range: z.ZodOptional<z.ZodEnum<["today", "yesterday", "last_7_days", "last_30_days", "this_month"]>>;
|
|
13
|
+
from: z.ZodOptional<z.ZodString>;
|
|
14
|
+
to: z.ZodOptional<z.ZodString>;
|
|
15
|
+
confirm: z.ZodDefault<z.ZodBoolean>;
|
|
16
|
+
};
|
|
17
|
+
type CutZonesArgs = {
|
|
18
|
+
campaign_id: string | number;
|
|
19
|
+
cr_max?: number;
|
|
20
|
+
spend_min?: number;
|
|
21
|
+
roi_max?: number;
|
|
22
|
+
min_clicks: number;
|
|
23
|
+
range?: (typeof RANGE_PRESETS)[number];
|
|
24
|
+
from?: string;
|
|
25
|
+
to?: string;
|
|
26
|
+
confirm: boolean;
|
|
27
|
+
};
|
|
28
|
+
export declare function cutZones(client: AffsetClient, args: CutZonesArgs): Promise<CallToolResult>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { type AffsetClient } from "../client.js";
|
|
4
|
+
export declare const DELETE_PAYOUT_RULE_DESCRIPTION: string;
|
|
5
|
+
export declare const deletePayoutRuleInputSchema: {
|
|
6
|
+
campaign_id: z.ZodUnion<[z.ZodString, z.ZodNumber]>;
|
|
7
|
+
zone_id: z.ZodOptional<z.ZodString>;
|
|
8
|
+
confirm: z.ZodDefault<z.ZodBoolean>;
|
|
9
|
+
};
|
|
10
|
+
type DeletePayoutRuleArgs = {
|
|
11
|
+
campaign_id: string | number;
|
|
12
|
+
zone_id?: string;
|
|
13
|
+
confirm: boolean;
|
|
14
|
+
};
|
|
15
|
+
export declare function deletePayoutRule(client: AffsetClient, args: DeletePayoutRuleArgs): Promise<CallToolResult>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { type AffsetClient } from "../client.js";
|
|
4
|
+
export declare const GET_CAMPAIGN_DESCRIPTION: string;
|
|
5
|
+
export declare const getCampaignInputSchema: {
|
|
6
|
+
campaign_id: z.ZodUnion<[z.ZodEffects<z.ZodString, string, string>, z.ZodNumber]>;
|
|
7
|
+
};
|
|
8
|
+
type GetCampaignArgs = {
|
|
9
|
+
campaign_id: string | number;
|
|
10
|
+
};
|
|
11
|
+
export declare function getCampaign(client: AffsetClient, args: GetCampaignArgs): Promise<CallToolResult>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AffsetApiError } from "../client.js";
|
|
3
|
+
import { formatInstant } from "../lib/time.js";
|
|
4
|
+
import { mdCell, money, moneyPrecise } from "../lib/format.js";
|
|
5
|
+
import { fetchPayoutRules } from "../lib/payoutRules.js";
|
|
6
|
+
import { fetchTargetingRules, fetchTargetingTypes, UNENFORCED_TYPES } from "../lib/targeting.js";
|
|
7
|
+
import { errorResult, textError, textResult } from "../lib/toolResult.js";
|
|
8
|
+
export const GET_CAMPAIGN_DESCRIPTION = "Get one campaign's full record: every field (including the untruncated offer URL, " +
|
|
9
|
+
"exact schedule, budgets/pacing, silent-conversions flag, payout goal type) plus its " +
|
|
10
|
+
"targeting rules and payout rules in one call. `list_campaigns` renders a summary table " +
|
|
11
|
+
"(offer URL truncated, no dates/pacing/silent) meant for scanning many campaigns; use " +
|
|
12
|
+
"this when you need one campaign's complete data — e.g. before recreating it as a new " +
|
|
13
|
+
"campaign, or before editing it with `update_campaign`.";
|
|
14
|
+
export const getCampaignInputSchema = {
|
|
15
|
+
campaign_id: z
|
|
16
|
+
.union([
|
|
17
|
+
z
|
|
18
|
+
.string()
|
|
19
|
+
.trim()
|
|
20
|
+
.regex(/^[1-9]\d*$/, "campaign_id must be a positive integer")
|
|
21
|
+
.refine((value) => Number.isSafeInteger(Number(value)), "campaign_id is too large"),
|
|
22
|
+
z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
23
|
+
])
|
|
24
|
+
.describe("Campaign id to fetch."),
|
|
25
|
+
};
|
|
26
|
+
export async function getCampaign(client, args) {
|
|
27
|
+
try {
|
|
28
|
+
const campaignId = String(args.campaign_id).trim();
|
|
29
|
+
if (!campaignId)
|
|
30
|
+
return textError("campaign_id is required.");
|
|
31
|
+
let campaign;
|
|
32
|
+
try {
|
|
33
|
+
campaign = await client.get(`/api/campaigns/${encodeURIComponent(campaignId)}`);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (err instanceof AffsetApiError && err.status === 404) {
|
|
37
|
+
return textError(`Campaign \`${campaignId}\` not found in this namespace.`);
|
|
38
|
+
}
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
const [targetingRules, payoutRules, targetingTypes, timeZone] = await Promise.all([
|
|
42
|
+
fetchTargetingRules(client, campaignId),
|
|
43
|
+
fetchPayoutRules(client, campaignId),
|
|
44
|
+
// Names are a nicety; the rule table still works with bare type ids.
|
|
45
|
+
fetchTargetingTypes(client).catch(() => []),
|
|
46
|
+
client.getTenantTimezone(),
|
|
47
|
+
]);
|
|
48
|
+
const typeById = new Map(targetingTypes.map((t) => [t.id, t]));
|
|
49
|
+
const fmt = (ms) => ms == null ? "—" : `${formatInstant(ms, timeZone)} ${mdCell(timeZone)} (${ms} epoch ms)`;
|
|
50
|
+
const deadTargetingRules = targetingRules.filter((rule) => {
|
|
51
|
+
const name = typeById.get(rule.targeting_rule_type_id)?.name.toLowerCase();
|
|
52
|
+
return name !== undefined && UNENFORCED_TYPES[name] !== undefined;
|
|
53
|
+
});
|
|
54
|
+
const payoutGoal = campaign.payout_goal_type?.trim();
|
|
55
|
+
const fields = [
|
|
56
|
+
"| Field | Value |",
|
|
57
|
+
"|---|---|",
|
|
58
|
+
`| Id | ${campaign.id} |`,
|
|
59
|
+
`| Name | ${mdCell(campaign.name)} |`,
|
|
60
|
+
`| Status | ${mdCell(campaign.status)} |`,
|
|
61
|
+
`| Advertiser | ${mdCell(campaign.user_email ?? "—")} |`,
|
|
62
|
+
`| Offer URL | ${mdCell(campaign.redirect_url ?? "—")} |`,
|
|
63
|
+
`| Model | ${mdCell(campaign.payment_model ?? "—")} |`,
|
|
64
|
+
`| Rate | ${campaign.rate != null ? moneyPrecise(campaign.rate) : "—"} |`,
|
|
65
|
+
`| Start date | ${fmt(campaign.start_date)} |`,
|
|
66
|
+
`| End date | ${fmt(campaign.end_date)} |`,
|
|
67
|
+
`| Daily budget | ${money(campaign.daily_budget)} |`,
|
|
68
|
+
`| Total budget | ${money(campaign.total_budget)} |`,
|
|
69
|
+
`| Pacing | ${mdCell(campaign.pacing ?? "—")} |`,
|
|
70
|
+
`| Budget paused | ${formatBudgetPaused(campaign)} |`,
|
|
71
|
+
`| Silent conversions | ${formatFlag(campaign.silent)} |`,
|
|
72
|
+
`| Payout goal type | ${payoutGoal ? mdCell(payoutGoal) : "_none — every conversion type gets the resolved payout_"} |`,
|
|
73
|
+
`| Created | ${fmt(campaign.created_at)} |`,
|
|
74
|
+
`| Updated | ${fmt(campaign.updated_at)} |`,
|
|
75
|
+
];
|
|
76
|
+
return textResult([
|
|
77
|
+
`**Campaign** \`${campaign.id}\` — ${mdCell(campaign.name)}`,
|
|
78
|
+
"",
|
|
79
|
+
fields.join("\n"),
|
|
80
|
+
"",
|
|
81
|
+
`**Targeting rules** (${targetingRules.length})`,
|
|
82
|
+
"",
|
|
83
|
+
renderTargetingRules(targetingRules, typeById),
|
|
84
|
+
...(deadTargetingRules.length
|
|
85
|
+
? ["", ...renderDeadTargetingWarnings(deadTargetingRules, typeById)]
|
|
86
|
+
: []),
|
|
87
|
+
"",
|
|
88
|
+
`**Payout rules** (${payoutRules.length})`,
|
|
89
|
+
"",
|
|
90
|
+
renderPayoutRules(payoutRules),
|
|
91
|
+
"",
|
|
92
|
+
"_Recreate elsewhere with `create_campaign` + `update_campaign`, then " +
|
|
93
|
+
"`set_targeting_rule` / `set_payout_rule` for each rule above._",
|
|
94
|
+
"_Manage this campaign with `update_campaign`, `set_campaign_status`, " +
|
|
95
|
+
"`set_targeting_rule`, `set_payout_rule`, `set_payout_goal`._",
|
|
96
|
+
].join("\n"));
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
return errorResult(err);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function formatFlag(value) {
|
|
103
|
+
if (value == null)
|
|
104
|
+
return "—";
|
|
105
|
+
return value ? "yes" : "no";
|
|
106
|
+
}
|
|
107
|
+
function formatBudgetPaused(campaign) {
|
|
108
|
+
if (campaign.budget_paused == null)
|
|
109
|
+
return "—";
|
|
110
|
+
if (!campaign.budget_paused)
|
|
111
|
+
return "no";
|
|
112
|
+
const reason = campaign.budget_pause_reason?.trim();
|
|
113
|
+
return reason ? `yes (${mdCell(reason)})` : "yes (reason unavailable)";
|
|
114
|
+
}
|
|
115
|
+
function renderTargetingRules(rules, typeById) {
|
|
116
|
+
if (rules.length === 0) {
|
|
117
|
+
return "_No targeting rules — campaign is unrestricted on /serve._";
|
|
118
|
+
}
|
|
119
|
+
const lines = ["| Id | Type | Method | Rule |", "|---|---|---|---|"];
|
|
120
|
+
for (const r of rules) {
|
|
121
|
+
const t = typeById.get(r.targeting_rule_type_id);
|
|
122
|
+
const typeLabel = t
|
|
123
|
+
? `${mdCell(t.name)} (${r.targeting_rule_type_id})`
|
|
124
|
+
: String(r.targeting_rule_type_id);
|
|
125
|
+
const inert = t && UNENFORCED_TYPES[t.name.toLowerCase()] ? " ⚠️" : "";
|
|
126
|
+
lines.push(`| ${r.id ?? "—"} | ${typeLabel}${inert} | ${mdCell(r.targeting_method)} | ${mdCell(r.rule)} |`);
|
|
127
|
+
}
|
|
128
|
+
return lines.join("\n");
|
|
129
|
+
}
|
|
130
|
+
function renderDeadTargetingWarnings(rules, typeById) {
|
|
131
|
+
const names = [...new Set(rules.map((rule) => typeById.get(rule.targeting_rule_type_id).name))];
|
|
132
|
+
return names.map((name) => `⚠️ \`${mdCell(name)}\` — ${UNENFORCED_TYPES[name.toLowerCase()]}. This rule has no effect.`);
|
|
133
|
+
}
|
|
134
|
+
function renderPayoutRules(rules) {
|
|
135
|
+
if (rules.length === 0) {
|
|
136
|
+
return "_No payout rules — conversions resolve to $0 until you set one._";
|
|
137
|
+
}
|
|
138
|
+
const lines = ["| Scope | Zone | Payout | Rule id |", "|---|---|--:|---|"];
|
|
139
|
+
for (const r of rules) {
|
|
140
|
+
const scope = r.zone_id == null ? "global" : "zone";
|
|
141
|
+
const zone = r.zone_id == null ? "—" : `\`${r.zone_id}\``;
|
|
142
|
+
lines.push(`| ${scope} | ${zone} | ${moneyPrecise(r.payout)} | \`${r.id}\` |`);
|
|
143
|
+
}
|
|
144
|
+
if (!rules.some((rule) => rule.zone_id == null)) {
|
|
145
|
+
lines.push("");
|
|
146
|
+
lines.push("_⚠️ No global rule — conversions from any zone without an override resolve to **$0**._");
|
|
147
|
+
}
|
|
148
|
+
return lines.join("\n");
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=getCampaign.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import type { AffsetClient } from "../client.js";
|
|
4
|
+
import { RANGE_PRESETS } from "../lib/time.js";
|
|
5
|
+
import { type GroupBy } from "../types.js";
|
|
6
|
+
export declare const GET_STATS_DESCRIPTION: string;
|
|
7
|
+
export declare const getStatsInputSchema: {
|
|
8
|
+
group_by: z.ZodDefault<z.ZodEnum<["date", "campaign_id", "zone_id", "country", "conversion_type", "publisher_email", "advertiser_email", "sub1", "sub2", "sub3", "sub4", "sub5"]>>;
|
|
9
|
+
range: z.ZodOptional<z.ZodEnum<["today", "yesterday", "last_7_days", "last_30_days", "this_month"]>>;
|
|
10
|
+
from: z.ZodOptional<z.ZodString>;
|
|
11
|
+
to: z.ZodOptional<z.ZodString>;
|
|
12
|
+
campaign_ids: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
13
|
+
zone_ids: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
14
|
+
sub1: z.ZodOptional<z.ZodString>;
|
|
15
|
+
sub2: z.ZodOptional<z.ZodString>;
|
|
16
|
+
sub3: z.ZodOptional<z.ZodString>;
|
|
17
|
+
sub4: z.ZodOptional<z.ZodString>;
|
|
18
|
+
sub5: z.ZodOptional<z.ZodString>;
|
|
19
|
+
conversion_type: z.ZodOptional<z.ZodString>;
|
|
20
|
+
advertiser_email: z.ZodOptional<z.ZodString>;
|
|
21
|
+
publisher_email: z.ZodOptional<z.ZodString>;
|
|
22
|
+
paid_only: z.ZodDefault<z.ZodBoolean>;
|
|
23
|
+
};
|
|
24
|
+
type GetStatsArgs = {
|
|
25
|
+
group_by: GroupBy;
|
|
26
|
+
range?: (typeof RANGE_PRESETS)[number];
|
|
27
|
+
from?: string;
|
|
28
|
+
to?: string;
|
|
29
|
+
campaign_ids?: string[];
|
|
30
|
+
zone_ids?: string[];
|
|
31
|
+
sub1?: string;
|
|
32
|
+
sub2?: string;
|
|
33
|
+
sub3?: string;
|
|
34
|
+
sub4?: string;
|
|
35
|
+
sub5?: string;
|
|
36
|
+
conversion_type?: string;
|
|
37
|
+
advertiser_email?: string;
|
|
38
|
+
publisher_email?: string;
|
|
39
|
+
paid_only?: boolean;
|
|
40
|
+
};
|
|
41
|
+
export declare function getStats(client: AffsetClient, args: GetStatsArgs): Promise<CallToolResult>;
|
|
42
|
+
export {};
|
package/dist/tools/getStats.js
CHANGED
|
@@ -6,10 +6,21 @@ import { GROUP_BY_VALUES, SUB_KEYS } from "../types.js";
|
|
|
6
6
|
export const GET_STATS_DESCRIPTION = "Pull affset traffic stats grouped by a single dimension. Returns impressions, clicks, " +
|
|
7
7
|
"conversions, CR, payout, media cost and ROI as a table. Drill down by calling " +
|
|
8
8
|
"repeatedly: first group_by=date or campaign_id, then narrow with filters " +
|
|
9
|
-
"(campaign_ids, zone_ids, sub1..sub5
|
|
9
|
+
"(campaign_ids, zone_ids, sub1..sub5, conversion_type, advertiser_email, publisher_email, " +
|
|
10
|
+
"paid_only) and change group_by (zone_id, sub1, ...). " +
|
|
10
11
|
'Sub columns are titled with the tenant\'s configured labels (e.g. "Zone (sub1)") ' +
|
|
11
12
|
"when set; group_by/filters always take the raw subN key. " +
|
|
12
|
-
"
|
|
13
|
+
"conversion_type only matches conversion rows, so filtering by it zeroes impressions, clicks " +
|
|
14
|
+
"and media cost — it narrows to conversions of that type, not clicks that led to one. " +
|
|
15
|
+
"paid_only defaults to true (same as the dashboard): conversions and CR exclude informative " +
|
|
16
|
+
"pings whose pixel type missed payout_goal_type, so CR is not inflated above 100%. Set " +
|
|
17
|
+
"false for the raw unfiltered count. " +
|
|
18
|
+
"ROI is blank until traffic cost has been imported for the slice. " +
|
|
19
|
+
"group_by=advertiser_email / publisher_email break down by team member; the API limits them " +
|
|
20
|
+
"to owner/manager plus the matching side's manager role (403 otherwise). " +
|
|
21
|
+
"advertiser_email / publisher_email are also standalone filters — narrow every row to one " +
|
|
22
|
+
"advertiser's campaigns or one publisher's zones regardless of group_by, instead of breaking " +
|
|
23
|
+
"every user out into its own row. Same role limits as the matching group_by value.";
|
|
13
24
|
export const getStatsInputSchema = {
|
|
14
25
|
group_by: z
|
|
15
26
|
.enum(GROUP_BY_VALUES)
|
|
@@ -34,15 +45,46 @@ export const getStatsInputSchema = {
|
|
|
34
45
|
sub3: z.string().optional().describe("Filter by sub3 value(s)."),
|
|
35
46
|
sub4: z.string().optional().describe("Filter by sub4 value(s)."),
|
|
36
47
|
sub5: z.string().optional().describe("Filter by sub5 value(s)."),
|
|
48
|
+
conversion_type: z
|
|
49
|
+
.string()
|
|
50
|
+
.optional()
|
|
51
|
+
.describe("Filter by conversion type value(s) (e.g. deposit, register), comma-separated for multiple. " +
|
|
52
|
+
'Pass "" to select conversions recorded without a type.'),
|
|
53
|
+
advertiser_email: z
|
|
54
|
+
.string()
|
|
55
|
+
.trim()
|
|
56
|
+
.email()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe("Narrow to one advertiser's campaigns, independent of group_by. Owner/manager: any advertiser. " +
|
|
59
|
+
"advertiser_manager: only one of their own assigned advertisers (else 403). Other roles: 403."),
|
|
60
|
+
publisher_email: z
|
|
61
|
+
.string()
|
|
62
|
+
.trim()
|
|
63
|
+
.email()
|
|
64
|
+
.optional()
|
|
65
|
+
.describe("Narrow to one publisher's zones, independent of group_by. Owner/manager: any publisher. " +
|
|
66
|
+
"publisher_manager: only one of their own assigned publishers (else 403). Other roles: 403."),
|
|
67
|
+
paid_only: z
|
|
68
|
+
.boolean()
|
|
69
|
+
.default(true)
|
|
70
|
+
.describe("Drop informative conversions (postback_skipped=non_goal_type, e.g. lp_view pings that " +
|
|
71
|
+
"missed payout_goal_type) from the conversions count and CR. Silent conversions still " +
|
|
72
|
+
"count — not a payout>0 filter. Default true (matches the dashboard) so CR is not " +
|
|
73
|
+
"inflated above 100%. Set false for the raw count."),
|
|
37
74
|
};
|
|
38
75
|
export async function getStats(client, args) {
|
|
39
76
|
try {
|
|
40
77
|
const timeZone = await client.getTenantTimezone();
|
|
41
78
|
const { from, to, label } = resolveRange(args.range, args.from, args.to, timeZone);
|
|
79
|
+
// The API accepts only the literal strings "true"/"false"; omitted = false.
|
|
80
|
+
// MCP (and the dashboard) default to true so CR isn't inflated by lp_view
|
|
81
|
+
// pings, so we always send the resolved value rather than omitting it.
|
|
82
|
+
const paidOnly = args.paid_only ?? true;
|
|
42
83
|
const query = {
|
|
43
84
|
from,
|
|
44
85
|
to,
|
|
45
86
|
group_by: args.group_by,
|
|
87
|
+
paid_only: String(paidOnly),
|
|
46
88
|
};
|
|
47
89
|
if (args.campaign_ids?.length)
|
|
48
90
|
query.campaign_ids = args.campaign_ids.join(",");
|
|
@@ -53,14 +95,23 @@ export async function getStats(client, args) {
|
|
|
53
95
|
if (value !== undefined)
|
|
54
96
|
query[key] = value;
|
|
55
97
|
}
|
|
98
|
+
if (args.conversion_type !== undefined)
|
|
99
|
+
query.conversion_type = args.conversion_type;
|
|
100
|
+
if (args.advertiser_email !== undefined) {
|
|
101
|
+
query.advertiser_email = args.advertiser_email.trim();
|
|
102
|
+
}
|
|
103
|
+
if (args.publisher_email !== undefined) {
|
|
104
|
+
query.publisher_email = args.publisher_email.trim();
|
|
105
|
+
}
|
|
56
106
|
const data = await client.get("/api/stats", query);
|
|
57
107
|
const table = formatStatsTable(data.stats ?? [], args.group_by, data.sub_labels);
|
|
58
108
|
const heading = groupHeader(args.group_by, data.sub_labels);
|
|
109
|
+
const paidNote = paidOnly ? "" : " (including informative conversions)";
|
|
59
110
|
return {
|
|
60
111
|
content: [
|
|
61
112
|
{
|
|
62
113
|
type: "text",
|
|
63
|
-
text: `**Stats — ${label}, by ${heading}
|
|
114
|
+
text: `**Stats — ${label}, by ${heading}**${paidNote}\n\n${table}`,
|
|
64
115
|
},
|
|
65
116
|
],
|
|
66
117
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
import { type LinkArgs } from "../lib/linkArgs.js";
|
|
6
|
+
export declare const GET_TRACKING_LINK_DESCRIPTION: string;
|
|
7
|
+
export declare const getTrackingLinkInputSchema: {
|
|
8
|
+
source_click_id: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
9
|
+
cost: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
10
|
+
sub1: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
11
|
+
sub2: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
12
|
+
sub3: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
13
|
+
sub4: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
14
|
+
sub5: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
15
|
+
campaign_id: z.ZodUnion<[z.ZodEffects<z.ZodString, string, string>, z.ZodNumber]>;
|
|
16
|
+
zone_id: z.ZodOptional<z.ZodString>;
|
|
17
|
+
};
|
|
18
|
+
type GetTrackingLinkArgs = LinkArgs & {
|
|
19
|
+
campaign_id: string | number;
|
|
20
|
+
zone_id?: string;
|
|
21
|
+
};
|
|
22
|
+
export declare function getTrackingLink(client: AffsetClient, config: Config, args: GetTrackingLinkArgs): Promise<CallToolResult>;
|
|
23
|
+
export {};
|