atracker-mcp-server 1.1.0 → 1.1.2
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/package.json +8 -3
- package/src/client.js +3 -2
- package/src/index.js +1 -1
- package/src/tools/campaigns.js +38 -20
- package/src/tools/domains.js +20 -8
- package/src/tools/flows.js +25 -14
- package/src/tools/geo.js +33 -36
- package/src/tools/landings.js +25 -14
- package/src/tools/offers.js +26 -14
- package/src/tools/reports.js +30 -11
- package/src/tools/settings.js +24 -5
- package/src/tools/sources.js +21 -13
- package/src/tools/status.js +4 -2
- package/src/util.js +48 -0
- package/src/util.test.js +27 -0
package/package.json
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atracker-mcp-server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "MCP Server for ATracker self-hosted ad tracker",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"atracker-mcp-server": "./src/index.js"
|
|
8
8
|
},
|
|
9
|
-
"files": [
|
|
9
|
+
"files": [
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
10
12
|
"dependencies": {
|
|
11
|
-
"@modelcontextprotocol/sdk": "
|
|
13
|
+
"@modelcontextprotocol/sdk": "1.12.0"
|
|
12
14
|
},
|
|
13
15
|
"engines": {
|
|
14
16
|
"node": ">=20"
|
|
15
17
|
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "node --test src/util.test.js"
|
|
20
|
+
},
|
|
16
21
|
"license": "UNLICENSED"
|
|
17
22
|
}
|
package/src/client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
class ApiError extends Error {
|
|
1
|
+
export class ApiError extends Error {
|
|
2
2
|
constructor(status, body) {
|
|
3
|
-
|
|
3
|
+
const detail = body?.message || body?.error || `HTTP ${status}`;
|
|
4
|
+
super(detail);
|
|
4
5
|
this.status = status;
|
|
5
6
|
this.body = body;
|
|
6
7
|
}
|
package/src/index.js
CHANGED
package/src/tools/campaigns.js
CHANGED
|
@@ -1,46 +1,64 @@
|
|
|
1
|
+
import { defineTool, jsonText, stripUndefined, toolErrorText } from '../util.js';
|
|
2
|
+
|
|
1
3
|
export function registerCampaignTools(server, client) {
|
|
2
|
-
server
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
defineTool(server, 'list_campaigns', 'List all campaigns. Optional filter by status or group_name.', {
|
|
5
|
+
status: { type: 'string', description: 'active or paused', enum: ['active', 'paused'] },
|
|
6
|
+
group_name: { type: 'string', description: 'Filter by campaign group name (optional)' },
|
|
7
|
+
}, async ({ status, group_name }) => {
|
|
8
|
+
const rows = await client.get('/campaigns', { query: { status, group_name } });
|
|
9
|
+
return jsonText(rows);
|
|
5
10
|
});
|
|
6
11
|
|
|
7
|
-
server
|
|
12
|
+
defineTool(server, 'get_campaign', 'Get campaign details by ID.', {
|
|
13
|
+
id: { type: 'string', description: 'Campaign UUID' },
|
|
14
|
+
}, async ({ id }) => {
|
|
8
15
|
const row = await client.get(`/campaigns/${id}`);
|
|
9
|
-
return
|
|
16
|
+
return jsonText(row);
|
|
10
17
|
});
|
|
11
18
|
|
|
12
|
-
server
|
|
13
|
-
name: { type: 'string', description: 'Campaign name' },
|
|
19
|
+
defineTool(server, 'create_campaign', 'Create a new campaign. Requires name.', {
|
|
20
|
+
name: { type: 'string', description: 'Campaign name (required)' },
|
|
14
21
|
alias: { type: 'string', description: 'URL alias (optional)' },
|
|
15
|
-
|
|
22
|
+
domain_id: { type: 'string', description: 'Domain UUID (optional)' },
|
|
23
|
+
domain: { type: 'string', description: 'Domain hostname — resolved to domain_id if set (optional)' },
|
|
24
|
+
group_name: { type: 'string', description: 'Campaign group name (optional)' },
|
|
16
25
|
traffic_source_id: { type: 'string', description: 'Traffic source UUID (optional)' },
|
|
17
26
|
}, async (args) => {
|
|
18
|
-
const
|
|
19
|
-
|
|
27
|
+
const body = stripUndefined(args);
|
|
28
|
+
if (!body.name) return toolErrorText('Validation error: name is required');
|
|
29
|
+
const row = await client.post('/campaigns', body);
|
|
30
|
+
return jsonText(row);
|
|
20
31
|
});
|
|
21
32
|
|
|
22
|
-
server
|
|
33
|
+
defineTool(server, 'update_campaign', 'Update an existing campaign.', {
|
|
23
34
|
id: { type: 'string', description: 'Campaign UUID' },
|
|
24
35
|
name: { type: 'string', description: 'New name (optional)' },
|
|
25
36
|
alias: { type: 'string', description: 'New alias (optional)' },
|
|
26
|
-
|
|
37
|
+
domain_id: { type: 'string', description: 'Domain UUID (optional)' },
|
|
38
|
+
domain: { type: 'string', description: 'Domain hostname — resolved to domain_id if set (optional)' },
|
|
39
|
+
group_name: { type: 'string', description: 'Campaign group name (optional)' },
|
|
27
40
|
}, async ({ id, ...rest }) => {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
|
|
41
|
+
const row = await client.put(`/campaigns/${id}`, stripUndefined(rest));
|
|
42
|
+
return jsonText(row);
|
|
31
43
|
});
|
|
32
44
|
|
|
33
|
-
server
|
|
45
|
+
defineTool(server, 'pause_campaign', 'Pause a campaign.', {
|
|
46
|
+
id: { type: 'string', description: 'Campaign UUID' },
|
|
47
|
+
}, async ({ id }) => {
|
|
34
48
|
const row = await client.patch(`/campaigns/${id}/status`, { status: 'paused' });
|
|
35
|
-
return
|
|
49
|
+
return jsonText(row);
|
|
36
50
|
});
|
|
37
51
|
|
|
38
|
-
server
|
|
52
|
+
defineTool(server, 'resume_campaign', 'Resume a paused campaign.', {
|
|
53
|
+
id: { type: 'string', description: 'Campaign UUID' },
|
|
54
|
+
}, async ({ id }) => {
|
|
39
55
|
const row = await client.patch(`/campaigns/${id}/status`, { status: 'active' });
|
|
40
|
-
return
|
|
56
|
+
return jsonText(row);
|
|
41
57
|
});
|
|
42
58
|
|
|
43
|
-
server
|
|
59
|
+
defineTool(server, 'delete_campaign', 'Archive (soft-delete) a campaign.', {
|
|
60
|
+
id: { type: 'string', description: 'Campaign UUID' },
|
|
61
|
+
}, async ({ id }) => {
|
|
44
62
|
await client.del(`/campaigns/${id}`);
|
|
45
63
|
return { content: [{ type: 'text', text: 'Campaign archived.' }] };
|
|
46
64
|
});
|
package/src/tools/domains.js
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
|
+
import { defineTool, jsonText, stripUndefined, toolErrorText } from '../util.js';
|
|
2
|
+
|
|
1
3
|
export function registerDomainTools(server, client) {
|
|
2
|
-
server
|
|
4
|
+
defineTool(server, 'list_domains', 'List domains. Optional filter by status.', {
|
|
5
|
+
status: { type: 'string', description: 'active or paused', enum: ['active', 'paused'] },
|
|
6
|
+
}, async ({ status }) => {
|
|
3
7
|
const rows = await client.get('/domains', { query: { status } });
|
|
4
|
-
return
|
|
8
|
+
return jsonText(rows);
|
|
5
9
|
});
|
|
6
10
|
|
|
7
|
-
server
|
|
11
|
+
defineTool(server, 'get_domain', 'Get domain details by ID.', {
|
|
12
|
+
id: { type: 'string', description: 'Domain UUID' },
|
|
13
|
+
}, async ({ id }) => {
|
|
8
14
|
const row = await client.get(`/domains/${id}`);
|
|
9
|
-
return
|
|
15
|
+
return jsonText(row);
|
|
10
16
|
});
|
|
11
17
|
|
|
12
|
-
server
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
defineTool(server, 'create_domain', 'Create a new domain. Requires domain hostname.', {
|
|
19
|
+
domain: { type: 'string', description: 'Domain name e.g. trk.example.com (required)' },
|
|
20
|
+
}, async (args) => {
|
|
21
|
+
const body = stripUndefined(args);
|
|
22
|
+
if (!body.domain) return toolErrorText('Validation error: domain is required');
|
|
23
|
+
const row = await client.post('/domains', body);
|
|
24
|
+
return jsonText(row);
|
|
15
25
|
});
|
|
16
26
|
|
|
17
|
-
server
|
|
27
|
+
defineTool(server, 'delete_domain', 'Delete a domain.', {
|
|
28
|
+
id: { type: 'string', description: 'Domain UUID' },
|
|
29
|
+
}, async ({ id }) => {
|
|
18
30
|
await client.del(`/domains/${id}`);
|
|
19
31
|
return { content: [{ type: 'text', text: 'Domain deleted.' }] };
|
|
20
32
|
});
|
package/src/tools/flows.js
CHANGED
|
@@ -1,32 +1,43 @@
|
|
|
1
|
+
import { defineTool, jsonText, stripUndefined, toolErrorText } from '../util.js';
|
|
2
|
+
|
|
1
3
|
export function registerFlowTools(server, client) {
|
|
2
|
-
server
|
|
4
|
+
defineTool(server, 'list_flows', 'List flows. Optional filter by campaign_id.', {
|
|
5
|
+
campaign_id: { type: 'string', description: 'Campaign UUID (optional)' },
|
|
6
|
+
}, async ({ campaign_id }) => {
|
|
3
7
|
const rows = await client.get('/flows', { query: { campaign_id } });
|
|
4
|
-
return
|
|
8
|
+
return jsonText(rows);
|
|
5
9
|
});
|
|
6
10
|
|
|
7
|
-
server
|
|
11
|
+
defineTool(server, 'get_flow', 'Get flow details by ID.', {
|
|
12
|
+
id: { type: 'string', description: 'Flow UUID' },
|
|
13
|
+
}, async ({ id }) => {
|
|
8
14
|
const row = await client.get(`/flows/${id}`);
|
|
9
|
-
return
|
|
15
|
+
return jsonText(row);
|
|
10
16
|
});
|
|
11
17
|
|
|
12
|
-
server
|
|
13
|
-
campaign_id: { type: 'string', description: 'Campaign UUID' },
|
|
14
|
-
name: { type: 'string', description: 'Flow name' },
|
|
18
|
+
defineTool(server, 'create_flow', 'Create a new flow for a campaign.', {
|
|
19
|
+
campaign_id: { type: 'string', description: 'Campaign UUID (required)' },
|
|
20
|
+
name: { type: 'string', description: 'Flow name (required)' },
|
|
15
21
|
}, async (args) => {
|
|
16
|
-
const
|
|
17
|
-
|
|
22
|
+
const body = stripUndefined(args);
|
|
23
|
+
if (!body.campaign_id) return toolErrorText('Validation error: campaign_id is required');
|
|
24
|
+
if (!body.name) return toolErrorText('Validation error: name is required');
|
|
25
|
+
const row = await client.post('/flows', body);
|
|
26
|
+
return jsonText(row);
|
|
18
27
|
});
|
|
19
28
|
|
|
20
|
-
server
|
|
29
|
+
defineTool(server, 'update_flow', 'Update an existing flow.', {
|
|
21
30
|
id: { type: 'string', description: 'Flow UUID' },
|
|
22
31
|
name: { type: 'string', description: 'New name (optional)' },
|
|
32
|
+
campaign_id: { type: 'string', description: 'Move flow to another campaign UUID (optional)' },
|
|
23
33
|
}, async ({ id, ...rest }) => {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
|
|
34
|
+
const row = await client.put(`/flows/${id}`, stripUndefined(rest));
|
|
35
|
+
return jsonText(row);
|
|
27
36
|
});
|
|
28
37
|
|
|
29
|
-
server
|
|
38
|
+
defineTool(server, 'delete_flow', 'Delete a flow.', {
|
|
39
|
+
id: { type: 'string', description: 'Flow UUID' },
|
|
40
|
+
}, async ({ id }) => {
|
|
30
41
|
await client.del(`/flows/${id}`);
|
|
31
42
|
return { content: [{ type: 'text', text: 'Flow deleted.' }] };
|
|
32
43
|
});
|
package/src/tools/geo.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { defineTool, jsonText, stripUndefined } from '../util.js';
|
|
2
|
+
|
|
1
3
|
const CATEGORIES = ['geo', 'asn', 'threat'];
|
|
2
4
|
const POLL_INTERVAL_MS = 1000;
|
|
3
5
|
const POLL_TIMEOUT_MS = 30 * 60 * 1000;
|
|
@@ -8,12 +10,14 @@ async function waitForDownload(client, categories) {
|
|
|
8
10
|
// eslint-disable-next-line no-constant-condition
|
|
9
11
|
while (true) {
|
|
10
12
|
if (Date.now() - startedAt > POLL_TIMEOUT_MS) {
|
|
11
|
-
throw new Error('Timed out waiting for geo bundle download');
|
|
13
|
+
throw new Error('Timed out waiting for geo bundle download (30 min)');
|
|
12
14
|
}
|
|
13
15
|
const state = await client.get('/geo2ip/download-status');
|
|
14
16
|
const summaries = targets.map((c) => ({ category: c, ...(state[c] || {}) }));
|
|
15
17
|
const errored = summaries.find((s) => s.status === 'error');
|
|
16
|
-
if (errored)
|
|
18
|
+
if (errored) {
|
|
19
|
+
throw new Error(`Download failed for ${errored.category}: ${errored.error || 'unknown'}`);
|
|
20
|
+
}
|
|
17
21
|
const active = summaries.find((s) => !['done', 'idle'].includes(s.status));
|
|
18
22
|
if (!active) return summaries;
|
|
19
23
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
@@ -21,58 +25,51 @@ async function waitForDownload(client, categories) {
|
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export function registerGeoTools(server, client) {
|
|
24
|
-
|
|
28
|
+
defineTool(
|
|
29
|
+
server,
|
|
25
30
|
'geo_status',
|
|
26
|
-
'Get
|
|
31
|
+
'Get geo2ip bundle versions, sizes, and download state (admin token).',
|
|
27
32
|
{},
|
|
28
|
-
async () =>
|
|
29
|
-
const data = await client.get('/geo2ip/bundle-status');
|
|
30
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
31
|
-
}
|
|
33
|
+
async () => jsonText(await client.get('/geo2ip/bundle-status')),
|
|
32
34
|
);
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
defineTool(
|
|
37
|
+
server,
|
|
35
38
|
'geo_check_update',
|
|
36
|
-
'Check
|
|
39
|
+
'Check platform for newer geo bundle versions (admin token).',
|
|
37
40
|
{},
|
|
38
|
-
async () => {
|
|
39
|
-
const data = await client.post('/geo2ip/check-updates', {});
|
|
40
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
41
|
-
}
|
|
41
|
+
async () => jsonText(await client.post('/geo2ip/check-updates', {})),
|
|
42
42
|
);
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
defineTool(
|
|
45
|
+
server,
|
|
45
46
|
'geo_update',
|
|
46
|
-
'Download and apply
|
|
47
|
+
'Download and apply latest geo bundles (admin token). Polls until complete.',
|
|
47
48
|
{
|
|
48
|
-
categories: {
|
|
49
|
+
categories: {
|
|
50
|
+
type: 'array',
|
|
51
|
+
items: { type: 'string', enum: CATEGORIES },
|
|
52
|
+
description: 'Optional subset: geo, asn, threat',
|
|
53
|
+
},
|
|
49
54
|
force: { type: 'boolean', description: 'Re-download even if bundles are up to date' },
|
|
50
55
|
},
|
|
51
56
|
async (args = {}) => {
|
|
52
|
-
const body = {
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
const body = stripUndefined({
|
|
58
|
+
categories: Array.isArray(args.categories) && args.categories.length ? args.categories : undefined,
|
|
59
|
+
force: args.force ? true : undefined,
|
|
60
|
+
});
|
|
55
61
|
await client.post('/geo2ip/download-bundle', body);
|
|
56
62
|
const summaries = await waitForDownload(client, body.categories);
|
|
57
63
|
const finalStatus = await client.get('/geo2ip/bundle-status');
|
|
58
|
-
return {
|
|
59
|
-
|
|
60
|
-
type: 'text',
|
|
61
|
-
text: JSON.stringify({ summaries, status: finalStatus }, null, 2),
|
|
62
|
-
}],
|
|
63
|
-
};
|
|
64
|
-
}
|
|
64
|
+
return jsonText({ summaries, status: finalStatus });
|
|
65
|
+
},
|
|
65
66
|
);
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
defineTool(
|
|
69
|
+
server,
|
|
68
70
|
'geo_lookup',
|
|
69
|
-
'Look up geo, ASN, and threat info for an
|
|
70
|
-
{
|
|
71
|
-
|
|
72
|
-
},
|
|
73
|
-
async ({ ip }) => {
|
|
74
|
-
const data = await client.post('/geo2ip/test-lookup', { ip });
|
|
75
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
76
|
-
}
|
|
71
|
+
'Look up geo, ASN, and threat info for an IP (admin token).',
|
|
72
|
+
{ ip: { type: 'string', description: 'IPv4 or IPv6 address' } },
|
|
73
|
+
async ({ ip }) => jsonText(await client.post('/geo2ip/test-lookup', { ip })),
|
|
77
74
|
);
|
|
78
75
|
}
|
package/src/tools/landings.js
CHANGED
|
@@ -1,33 +1,44 @@
|
|
|
1
|
+
import { defineTool, jsonText, stripUndefined, toolErrorText } from '../util.js';
|
|
2
|
+
|
|
1
3
|
export function registerLandingTools(server, client) {
|
|
2
|
-
server
|
|
4
|
+
defineTool(server, 'list_landings', 'List landing pages. Optional filter by status.', {
|
|
5
|
+
status: { type: 'string', description: 'active or paused', enum: ['active', 'paused'] },
|
|
6
|
+
}, async ({ status }) => {
|
|
3
7
|
const rows = await client.get('/landings', { query: { status } });
|
|
4
|
-
return
|
|
8
|
+
return jsonText(rows);
|
|
5
9
|
});
|
|
6
10
|
|
|
7
|
-
server
|
|
11
|
+
defineTool(server, 'get_landing', 'Get landing page details by ID.', {
|
|
12
|
+
id: { type: 'string', description: 'Landing page UUID' },
|
|
13
|
+
}, async ({ id }) => {
|
|
8
14
|
const row = await client.get(`/landings/${id}`);
|
|
9
|
-
return
|
|
15
|
+
return jsonText(row);
|
|
10
16
|
});
|
|
11
17
|
|
|
12
|
-
server
|
|
13
|
-
name: { type: 'string', description: 'Landing page name' },
|
|
14
|
-
url: { type: 'string', description: 'Landing page URL' },
|
|
18
|
+
defineTool(server, 'create_landing', 'Create a new landing page. Requires name.', {
|
|
19
|
+
name: { type: 'string', description: 'Landing page name (required)' },
|
|
20
|
+
url: { type: 'string', description: 'Landing page URL (optional)' },
|
|
21
|
+
group_name: { type: 'string', description: 'Landing group name (optional)' },
|
|
15
22
|
}, async (args) => {
|
|
16
|
-
const
|
|
17
|
-
|
|
23
|
+
const body = stripUndefined(args);
|
|
24
|
+
if (!body.name) return toolErrorText('Validation error: name is required');
|
|
25
|
+
const row = await client.post('/landings', body);
|
|
26
|
+
return jsonText(row);
|
|
18
27
|
});
|
|
19
28
|
|
|
20
|
-
server
|
|
29
|
+
defineTool(server, 'update_landing', 'Update an existing landing page.', {
|
|
21
30
|
id: { type: 'string', description: 'Landing page UUID' },
|
|
22
31
|
name: { type: 'string', description: 'New name (optional)' },
|
|
23
32
|
url: { type: 'string', description: 'New URL (optional)' },
|
|
33
|
+
group_name: { type: 'string', description: 'Landing group name (optional)' },
|
|
24
34
|
}, async ({ id, ...rest }) => {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
|
|
35
|
+
const row = await client.put(`/landings/${id}`, stripUndefined(rest));
|
|
36
|
+
return jsonText(row);
|
|
28
37
|
});
|
|
29
38
|
|
|
30
|
-
server
|
|
39
|
+
defineTool(server, 'delete_landing', 'Archive (soft-delete) a landing page.', {
|
|
40
|
+
id: { type: 'string', description: 'Landing page UUID' },
|
|
41
|
+
}, async ({ id }) => {
|
|
31
42
|
await client.del(`/landings/${id}`);
|
|
32
43
|
return { content: [{ type: 'text', text: 'Landing page archived.' }] };
|
|
33
44
|
});
|
package/src/tools/offers.js
CHANGED
|
@@ -1,36 +1,48 @@
|
|
|
1
|
+
import { defineTool, jsonText, stripUndefined, toolErrorText } from '../util.js';
|
|
2
|
+
|
|
1
3
|
export function registerOfferTools(server, client) {
|
|
2
|
-
server
|
|
4
|
+
defineTool(server, 'list_offers', 'List all offers. Optional filter by status.', {
|
|
5
|
+
status: { type: 'string', description: 'active or paused', enum: ['active', 'paused'] },
|
|
6
|
+
}, async ({ status }) => {
|
|
3
7
|
const rows = await client.get('/offers', { query: { status } });
|
|
4
|
-
return
|
|
8
|
+
return jsonText(rows);
|
|
5
9
|
});
|
|
6
10
|
|
|
7
|
-
server
|
|
11
|
+
defineTool(server, 'get_offer', 'Get offer details by ID.', {
|
|
12
|
+
id: { type: 'string', description: 'Offer UUID' },
|
|
13
|
+
}, async ({ id }) => {
|
|
8
14
|
const row = await client.get(`/offers/${id}`);
|
|
9
|
-
return
|
|
15
|
+
return jsonText(row);
|
|
10
16
|
});
|
|
11
17
|
|
|
12
|
-
server
|
|
13
|
-
name: { type: 'string', description: 'Offer name' },
|
|
14
|
-
url: { type: 'string', description: 'Offer URL' },
|
|
18
|
+
defineTool(server, 'create_offer', 'Create a new offer. Requires name.', {
|
|
19
|
+
name: { type: 'string', description: 'Offer name (required)' },
|
|
20
|
+
url: { type: 'string', description: 'Offer URL (optional)' },
|
|
15
21
|
payout: { type: 'number', description: 'Payout amount (optional)' },
|
|
16
22
|
currency: { type: 'string', description: 'Currency code (optional)' },
|
|
23
|
+
group_name: { type: 'string', description: 'Offer group name (optional)' },
|
|
17
24
|
}, async (args) => {
|
|
18
|
-
const
|
|
19
|
-
|
|
25
|
+
const body = stripUndefined(args);
|
|
26
|
+
if (!body.name) return toolErrorText('Validation error: name is required');
|
|
27
|
+
const row = await client.post('/offers', body);
|
|
28
|
+
return jsonText(row);
|
|
20
29
|
});
|
|
21
30
|
|
|
22
|
-
server
|
|
31
|
+
defineTool(server, 'update_offer', 'Update an existing offer.', {
|
|
23
32
|
id: { type: 'string', description: 'Offer UUID' },
|
|
24
33
|
name: { type: 'string', description: 'New name (optional)' },
|
|
25
34
|
url: { type: 'string', description: 'New URL (optional)' },
|
|
26
35
|
payout: { type: 'number', description: 'New payout (optional)' },
|
|
36
|
+
currency: { type: 'string', description: 'Currency code (optional)' },
|
|
37
|
+
group_name: { type: 'string', description: 'Offer group name (optional)' },
|
|
27
38
|
}, async ({ id, ...rest }) => {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
|
|
39
|
+
const row = await client.put(`/offers/${id}`, stripUndefined(rest));
|
|
40
|
+
return jsonText(row);
|
|
31
41
|
});
|
|
32
42
|
|
|
33
|
-
server
|
|
43
|
+
defineTool(server, 'delete_offer', 'Archive (soft-delete) an offer.', {
|
|
44
|
+
id: { type: 'string', description: 'Offer UUID' },
|
|
45
|
+
}, async ({ id }) => {
|
|
34
46
|
await client.del(`/offers/${id}`);
|
|
35
47
|
return { content: [{ type: 'text', text: 'Offer archived.' }] };
|
|
36
48
|
});
|
package/src/tools/reports.js
CHANGED
|
@@ -1,22 +1,41 @@
|
|
|
1
|
+
import { defineTool, jsonText } from '../util.js';
|
|
2
|
+
|
|
3
|
+
function normalizeLimit(limit) {
|
|
4
|
+
if (limit === undefined || limit === null || limit === '') return undefined;
|
|
5
|
+
const n = Number(limit);
|
|
6
|
+
if (!Number.isFinite(n) || n < 1) return 100;
|
|
7
|
+
return Math.min(Math.floor(n), 1000);
|
|
8
|
+
}
|
|
9
|
+
|
|
1
10
|
export function registerReportTools(server, client) {
|
|
2
|
-
server
|
|
11
|
+
defineTool(server, 'get_reports', 'Get tracker report data with date range and grouping.', {
|
|
3
12
|
date_from: { type: 'string', description: 'Start date YYYY-MM-DD (default: 7 days ago)' },
|
|
4
|
-
date_to: { type: 'string', description: 'End date YYYY-MM-DD (default: today)' },
|
|
5
|
-
group_by: { type: 'string', description: 'Group by field: campaign, offer, country,
|
|
13
|
+
date_to: { type: 'string', description: 'End date YYYY-MM-DD inclusive (default: today)' },
|
|
14
|
+
group_by: { type: 'string', description: 'Group by field: campaign, offer, country, category, city, hour' },
|
|
6
15
|
campaign_id: { type: 'string', description: 'Filter by campaign UUID (optional)' },
|
|
7
16
|
}, async ({ date_from, date_to, group_by, campaign_id }) => {
|
|
8
|
-
const data = await client.get('/reports', {
|
|
9
|
-
|
|
17
|
+
const data = await client.get('/reports', {
|
|
18
|
+
query: { date_from, date_to, group_by, campaign_id },
|
|
19
|
+
});
|
|
20
|
+
return jsonText(data);
|
|
10
21
|
});
|
|
11
22
|
|
|
12
|
-
server
|
|
23
|
+
defineTool(server, 'get_conversions', 'Get conversion log with date range and filters.', {
|
|
13
24
|
date_from: { type: 'string', description: 'Start date YYYY-MM-DD' },
|
|
14
|
-
date_to: { type: 'string', description: 'End date YYYY-MM-DD' },
|
|
25
|
+
date_to: { type: 'string', description: 'End date YYYY-MM-DD inclusive' },
|
|
15
26
|
campaign_id: { type: 'string', description: 'Filter by campaign UUID (optional)' },
|
|
16
|
-
status: { type: 'string', description: 'Conversion status (optional)' },
|
|
17
|
-
limit: { type: 'number', description: 'Max rows (default 100, max
|
|
27
|
+
status: { type: 'string', description: 'Conversion status: lead, sale, rejected, registration, deposit, rebill (optional)' },
|
|
28
|
+
limit: { type: 'number', description: 'Max rows (default 100, max 1000)' },
|
|
18
29
|
}, async ({ date_from, date_to, campaign_id, status, limit }) => {
|
|
19
|
-
const data = await client.get('/conversions', {
|
|
20
|
-
|
|
30
|
+
const data = await client.get('/conversions', {
|
|
31
|
+
query: {
|
|
32
|
+
date_from,
|
|
33
|
+
date_to,
|
|
34
|
+
campaign_id,
|
|
35
|
+
status,
|
|
36
|
+
limit: normalizeLimit(limit),
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
return jsonText(data);
|
|
21
40
|
});
|
|
22
41
|
}
|
package/src/tools/settings.js
CHANGED
|
@@ -1,20 +1,39 @@
|
|
|
1
|
+
import { defineTool, jsonText, toolErrorText } from '../util.js';
|
|
2
|
+
|
|
1
3
|
export function registerSettingsTools(server, client) {
|
|
2
|
-
server
|
|
4
|
+
defineTool(server, 'get_settings', 'Get tracker settings (admin token). Optional filter by category. Secrets are redacted.', {
|
|
3
5
|
category: { type: 'string', description: 'Settings category (optional)' },
|
|
4
6
|
}, async ({ category }) => {
|
|
5
7
|
const data = await client.get('/settings', { query: { category } });
|
|
6
|
-
return
|
|
8
|
+
return jsonText(data);
|
|
7
9
|
});
|
|
8
10
|
|
|
9
|
-
server
|
|
11
|
+
defineTool(server, 'update_settings', 'Update one or more tracker settings (admin token). Body is JSON array of {key, value}.', {
|
|
10
12
|
settings: {
|
|
11
13
|
type: 'string',
|
|
12
14
|
description: 'JSON array of { key, value } objects, e.g. [{"key":"default_currency","value":"EUR"}]',
|
|
13
15
|
},
|
|
14
16
|
}, async ({ settings }) => {
|
|
15
17
|
let items;
|
|
16
|
-
try {
|
|
18
|
+
try {
|
|
19
|
+
items = JSON.parse(settings);
|
|
20
|
+
} catch {
|
|
21
|
+
return toolErrorText('Error: settings must be a valid JSON array of {key, value} objects.');
|
|
22
|
+
}
|
|
23
|
+
if (!Array.isArray(items)) {
|
|
24
|
+
return toolErrorText('Error: settings must be a JSON array.');
|
|
25
|
+
}
|
|
17
26
|
const data = await client.put('/settings', items);
|
|
18
|
-
return
|
|
27
|
+
return jsonText(data);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
defineTool(server, 'get_global_postback', 'Get global postback URL and installation token (resolves campaign from subid).', {}, async () => {
|
|
31
|
+
const data = await client.get('/global-postback');
|
|
32
|
+
return jsonText(data);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
defineTool(server, 'regenerate_global_postback_token', 'Regenerate global postback token. The old global postback URL stops working.', {}, async () => {
|
|
36
|
+
const data = await client.post('/global-postback/regenerate');
|
|
37
|
+
return jsonText(data);
|
|
19
38
|
});
|
|
20
39
|
}
|
package/src/tools/sources.js
CHANGED
|
@@ -1,32 +1,40 @@
|
|
|
1
|
+
import { defineTool, jsonText, stripUndefined, toolErrorText } from '../util.js';
|
|
2
|
+
|
|
1
3
|
export function registerSourceTools(server, client) {
|
|
2
|
-
server
|
|
4
|
+
defineTool(server, 'list_sources', 'List traffic sources.', {}, async () => {
|
|
3
5
|
const rows = await client.get('/sources');
|
|
4
|
-
return
|
|
6
|
+
return jsonText(rows);
|
|
5
7
|
});
|
|
6
8
|
|
|
7
|
-
server
|
|
9
|
+
defineTool(server, 'get_source', 'Get traffic source details by ID.', {
|
|
10
|
+
id: { type: 'string', description: 'Traffic source UUID' },
|
|
11
|
+
}, async ({ id }) => {
|
|
8
12
|
const row = await client.get(`/sources/${id}`);
|
|
9
|
-
return
|
|
13
|
+
return jsonText(row);
|
|
10
14
|
});
|
|
11
15
|
|
|
12
|
-
server
|
|
13
|
-
name: { type: 'string', description: 'Source name' },
|
|
16
|
+
defineTool(server, 'create_source', 'Create a new traffic source. Requires name.', {
|
|
17
|
+
name: { type: 'string', description: 'Source name (required)' },
|
|
14
18
|
type: { type: 'string', description: 'Source type (optional)' },
|
|
15
19
|
}, async (args) => {
|
|
16
|
-
const
|
|
17
|
-
|
|
20
|
+
const body = stripUndefined(args);
|
|
21
|
+
if (!body.name) return toolErrorText('Validation error: name is required');
|
|
22
|
+
const row = await client.post('/sources', body);
|
|
23
|
+
return jsonText(row);
|
|
18
24
|
});
|
|
19
25
|
|
|
20
|
-
server
|
|
26
|
+
defineTool(server, 'update_source', 'Update a traffic source.', {
|
|
21
27
|
id: { type: 'string', description: 'Traffic source UUID' },
|
|
22
28
|
name: { type: 'string', description: 'New name (optional)' },
|
|
29
|
+
type: { type: 'string', description: 'Source type (optional)' },
|
|
23
30
|
}, async ({ id, ...rest }) => {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
|
|
31
|
+
const row = await client.put(`/sources/${id}`, stripUndefined(rest));
|
|
32
|
+
return jsonText(row);
|
|
27
33
|
});
|
|
28
34
|
|
|
29
|
-
server
|
|
35
|
+
defineTool(server, 'delete_source', 'Delete a traffic source.', {
|
|
36
|
+
id: { type: 'string', description: 'Traffic source UUID' },
|
|
37
|
+
}, async ({ id }) => {
|
|
30
38
|
await client.del(`/sources/${id}`);
|
|
31
39
|
return { content: [{ type: 'text', text: 'Traffic source deleted.' }] };
|
|
32
40
|
});
|
package/src/tools/status.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { defineTool, jsonText } from '../util.js';
|
|
2
|
+
|
|
1
3
|
export function registerStatusTools(server, client) {
|
|
2
|
-
server
|
|
4
|
+
defineTool(server, 'get_system_status', 'Get tracker system status: hostname, CPU, memory, uptime, version.', {}, async () => {
|
|
3
5
|
const data = await client.get('/system-status');
|
|
4
|
-
return
|
|
6
|
+
return jsonText(data);
|
|
5
7
|
});
|
|
6
8
|
}
|
package/src/util.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ApiError } from './client.js';
|
|
2
|
+
|
|
3
|
+
export function stripUndefined(obj) {
|
|
4
|
+
if (!obj || typeof obj !== 'object') return {};
|
|
5
|
+
return Object.fromEntries(
|
|
6
|
+
Object.entries(obj).filter(([, v]) => v !== undefined && v !== null),
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function formatApiErrorMessage(err) {
|
|
11
|
+
if (err instanceof ApiError) {
|
|
12
|
+
const body = err.body;
|
|
13
|
+
const detail =
|
|
14
|
+
(body && (body.message || body.error)) ||
|
|
15
|
+
err.message ||
|
|
16
|
+
`HTTP ${err.status}`;
|
|
17
|
+
return `API error ${err.status}: ${detail}`;
|
|
18
|
+
}
|
|
19
|
+
return err?.message || String(err);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function jsonText(data) {
|
|
23
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function toolError(err) {
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: 'text', text: formatApiErrorMessage(err) }],
|
|
29
|
+
isError: true,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function toolErrorText(text) {
|
|
34
|
+
return { content: [{ type: 'text', text }], isError: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Registers an MCP tool with consistent API error handling (isError + message).
|
|
39
|
+
*/
|
|
40
|
+
export function defineTool(server, name, description, schema, handler) {
|
|
41
|
+
server.tool(name, description, schema, async (args) => {
|
|
42
|
+
try {
|
|
43
|
+
return await handler(args ?? {});
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return toolError(err);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
package/src/util.test.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { ApiError } from './client.js';
|
|
4
|
+
import { stripUndefined, formatApiErrorMessage } from './util.js';
|
|
5
|
+
|
|
6
|
+
describe('stripUndefined', () => {
|
|
7
|
+
it('removes null and undefined keys', () => {
|
|
8
|
+
assert.deepEqual(stripUndefined({ a: 1, b: undefined, c: null, d: '' }), { a: 1, d: '' });
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('formatApiErrorMessage', () => {
|
|
13
|
+
it('prefers Fastify message over generic error', () => {
|
|
14
|
+
const err = new ApiError(500, {
|
|
15
|
+
statusCode: 500,
|
|
16
|
+
error: 'Internal Server Error',
|
|
17
|
+
message: 'reportService.getCampaignReport is not a function',
|
|
18
|
+
});
|
|
19
|
+
const msg = formatApiErrorMessage(err);
|
|
20
|
+
assert.match(msg, /getCampaignReport/);
|
|
21
|
+
assert.notEqual(msg, 'API error 500: Internal Server Error');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('formats plain errors', () => {
|
|
25
|
+
assert.equal(formatApiErrorMessage(new Error('network')), 'network');
|
|
26
|
+
});
|
|
27
|
+
});
|