atracker-mcp-server 1.0.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 ADDED
@@ -0,0 +1,113 @@
1
+ # ATracker MCP Server
2
+
3
+ [Model Context Protocol](https://modelcontextprotocol.io) server for managing your self-hosted ATracker instance from AI agents like Cursor and Claude Desktop.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g atracker-mcp-server
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ ### Cursor (`~/.cursor/mcp.json`)
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "atracker": {
19
+ "command": "npx",
20
+ "args": ["-y", "atracker-mcp-server"],
21
+ "env": {
22
+ "ATRACKER_URL": "http://your-tracker-ip",
23
+ "ATRACKER_TOKEN": "YOUR_API_TOKEN"
24
+ }
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ ### Claude Desktop (`claude_desktop_config.json`)
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "atracker": {
36
+ "command": "npx",
37
+ "args": ["-y", "atracker-mcp-server"],
38
+ "env": {
39
+ "ATRACKER_URL": "http://your-tracker-ip",
40
+ "ATRACKER_TOKEN": "YOUR_API_TOKEN"
41
+ }
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ ## Getting an API Token
48
+
49
+ 1. Open your tracker in a browser.
50
+ 2. Go to **Settings → API Tokens**.
51
+ 3. Click **Create token**, give it a name, and select **All resources** (*) for full MCP access.
52
+ 4. Copy the token (it is shown only once).
53
+
54
+ ## Available Tools
55
+
56
+ ### Campaigns
57
+ - `list_campaigns` — List all campaigns (optional status filter)
58
+ - `get_campaign` — Get campaign details by ID
59
+ - `create_campaign` — Create a new campaign
60
+ - `update_campaign` — Update an existing campaign
61
+ - `pause_campaign` — Pause a campaign
62
+ - `resume_campaign` — Resume a paused campaign
63
+ - `delete_campaign` — Archive a campaign
64
+
65
+ ### Offers
66
+ - `list_offers` — List all offers
67
+ - `get_offer` — Get offer details by ID
68
+ - `create_offer` — Create a new offer
69
+ - `update_offer` — Update an existing offer
70
+ - `delete_offer` — Archive an offer
71
+
72
+ ### Flows
73
+ - `list_flows` — List flows (optional campaign_id filter)
74
+ - `get_flow` — Get flow details by ID
75
+ - `create_flow` — Create a new flow
76
+ - `update_flow` — Update an existing flow
77
+ - `delete_flow` — Delete a flow
78
+
79
+ ### Landing Pages
80
+ - `list_landings` — List landing pages
81
+ - `get_landing` — Get landing page details by ID
82
+ - `create_landing` — Create a new landing page
83
+ - `update_landing` — Update an existing landing page
84
+ - `delete_landing` — Archive a landing page
85
+
86
+ ### Traffic Sources
87
+ - `list_sources` — List traffic sources
88
+ - `get_source` — Get traffic source details by ID
89
+ - `create_source` — Create a new traffic source
90
+ - `update_source` — Update a traffic source
91
+ - `delete_source` — Delete a traffic source
92
+
93
+ ### Domains
94
+ - `list_domains` — List domains
95
+ - `get_domain` — Get domain details by ID
96
+ - `create_domain` — Create a new domain
97
+ - `delete_domain` — Delete a domain
98
+
99
+ ### Reports & Conversions
100
+ - `get_reports` — Get report data with date range and grouping
101
+ - `get_conversions` — Get conversion data with filters
102
+
103
+ ### System
104
+ - `get_system_status` — Get tracker system status
105
+ - `get_settings` — Get tracker settings
106
+ - `update_settings` — Update tracker settings
107
+
108
+ ## Environment Variables
109
+
110
+ | Variable | Required | Description |
111
+ |----------|----------|-------------|
112
+ | `ATRACKER_URL` | Yes | Tracker URL (e.g. `http://213.176.94.21`) |
113
+ | `ATRACKER_TOKEN` | Yes | API token from Settings → API Tokens |
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "atracker-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP Server for ATracker self-hosted ad tracker",
5
+ "type": "module",
6
+ "bin": {
7
+ "atracker-mcp-server": "./src/index.js"
8
+ },
9
+ "files": ["src"],
10
+ "dependencies": {
11
+ "@modelcontextprotocol/sdk": "^1.12.0"
12
+ },
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "license": "UNLICENSED"
17
+ }
package/src/client.js ADDED
@@ -0,0 +1,48 @@
1
+ class ApiError extends Error {
2
+ constructor(status, body) {
3
+ super(body?.error || `HTTP ${status}`);
4
+ this.status = status;
5
+ this.body = body;
6
+ }
7
+ }
8
+
9
+ export function createClient(url, token) {
10
+ const base = (url || '').replace(/\/+$/, '');
11
+
12
+ async function request(method, path, { body, query } = {}) {
13
+ let fullUrl = `${base}/api/v1${path}`;
14
+ if (query) {
15
+ const params = new URLSearchParams();
16
+ for (const [k, v] of Object.entries(query)) {
17
+ if (v !== undefined && v !== null && v !== '') params.set(k, String(v));
18
+ }
19
+ const qs = params.toString();
20
+ if (qs) fullUrl += `?${qs}`;
21
+ }
22
+
23
+ const opts = {
24
+ method,
25
+ headers: {
26
+ 'Authorization': `Bearer ${token}`,
27
+ 'Content-Type': 'application/json',
28
+ 'Accept': 'application/json',
29
+ },
30
+ };
31
+ if (body && method !== 'GET') opts.body = JSON.stringify(body);
32
+
33
+ const res = await fetch(fullUrl, opts);
34
+ if (res.status === 204) return null;
35
+ let data;
36
+ try { data = await res.json(); } catch { data = null; }
37
+ if (!res.ok) throw new ApiError(res.status, data);
38
+ return data;
39
+ }
40
+
41
+ return {
42
+ get: (path, opts) => request('GET', path, opts),
43
+ post: (path, body, opts) => request('POST', path, { ...opts, body }),
44
+ put: (path, body, opts) => request('PUT', path, { ...opts, body }),
45
+ patch: (path, body, opts) => request('PATCH', path, { ...opts, body }),
46
+ del: (path, opts) => request('DELETE', path, opts),
47
+ };
48
+ }
package/src/index.js ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { createClient } from './client.js';
5
+ import { registerCampaignTools } from './tools/campaigns.js';
6
+ import { registerOfferTools } from './tools/offers.js';
7
+ import { registerFlowTools } from './tools/flows.js';
8
+ import { registerLandingTools } from './tools/landings.js';
9
+ import { registerSourceTools } from './tools/sources.js';
10
+ import { registerDomainTools } from './tools/domains.js';
11
+ import { registerReportTools } from './tools/reports.js';
12
+ import { registerStatusTools } from './tools/status.js';
13
+ import { registerSettingsTools } from './tools/settings.js';
14
+
15
+ const url = process.env.ATRACKER_URL;
16
+ const token = process.env.ATRACKER_TOKEN;
17
+
18
+ if (!url || !token) {
19
+ process.stderr.write('Error: ATRACKER_URL and ATRACKER_TOKEN environment variables are required.\n');
20
+ process.stderr.write('Example:\n');
21
+ process.stderr.write(' ATRACKER_URL=http://your-tracker ATRACKER_TOKEN=atk_... npx atracker-mcp-server\n');
22
+ process.exit(1);
23
+ }
24
+
25
+ const client = createClient(url, token);
26
+
27
+ const server = new McpServer({
28
+ name: 'atracker',
29
+ version: '1.0.0',
30
+ });
31
+
32
+ registerCampaignTools(server, client);
33
+ registerOfferTools(server, client);
34
+ registerFlowTools(server, client);
35
+ registerLandingTools(server, client);
36
+ registerSourceTools(server, client);
37
+ registerDomainTools(server, client);
38
+ registerReportTools(server, client);
39
+ registerStatusTools(server, client);
40
+ registerSettingsTools(server, client);
41
+
42
+ const transport = new StdioServerTransport();
43
+ await server.connect(transport);
@@ -0,0 +1,47 @@
1
+ export function registerCampaignTools(server, client) {
2
+ server.tool('list_campaigns', 'List all campaigns. Optional filter by status.', { status: { type: 'string', description: 'active or paused', enum: ['active', 'paused'] } }, async ({ status }) => {
3
+ const rows = await client.get('/campaigns', { query: { status } });
4
+ return { content: [{ type: 'text', text: JSON.stringify(rows, null, 2) }] };
5
+ });
6
+
7
+ server.tool('get_campaign', 'Get campaign details by ID.', { id: { type: 'string', description: 'Campaign UUID' } }, async ({ id }) => {
8
+ const row = await client.get(`/campaigns/${id}`);
9
+ return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
10
+ });
11
+
12
+ server.tool('create_campaign', 'Create a new campaign.', {
13
+ name: { type: 'string', description: 'Campaign name' },
14
+ alias: { type: 'string', description: 'URL alias (optional)' },
15
+ domain: { type: 'string', description: 'Domain (optional)' },
16
+ traffic_source_id: { type: 'string', description: 'Traffic source UUID (optional)' },
17
+ }, async (args) => {
18
+ const row = await client.post('/campaigns', args);
19
+ return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
20
+ });
21
+
22
+ server.tool('update_campaign', 'Update an existing campaign.', {
23
+ id: { type: 'string', description: 'Campaign UUID' },
24
+ name: { type: 'string', description: 'New name (optional)' },
25
+ alias: { type: 'string', description: 'New alias (optional)' },
26
+ domain: { type: 'string', description: 'New domain (optional)' },
27
+ }, async ({ id, ...rest }) => {
28
+ const body = Object.fromEntries(Object.entries(rest).filter(([, v]) => v !== undefined));
29
+ const row = await client.put(`/campaigns/${id}`, body);
30
+ return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
31
+ });
32
+
33
+ server.tool('pause_campaign', 'Pause a campaign.', { id: { type: 'string', description: 'Campaign UUID' } }, async ({ id }) => {
34
+ const row = await client.patch(`/campaigns/${id}/status`, { status: 'paused' });
35
+ return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
36
+ });
37
+
38
+ server.tool('resume_campaign', 'Resume a paused campaign.', { id: { type: 'string', description: 'Campaign UUID' } }, async ({ id }) => {
39
+ const row = await client.patch(`/campaigns/${id}/status`, { status: 'active' });
40
+ return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
41
+ });
42
+
43
+ server.tool('delete_campaign', 'Archive (soft-delete) a campaign.', { id: { type: 'string', description: 'Campaign UUID' } }, async ({ id }) => {
44
+ await client.del(`/campaigns/${id}`);
45
+ return { content: [{ type: 'text', text: 'Campaign archived.' }] };
46
+ });
47
+ }
@@ -0,0 +1,21 @@
1
+ export function registerDomainTools(server, client) {
2
+ server.tool('list_domains', 'List domains. Optional filter by status.', { status: { type: 'string', description: 'active or paused', enum: ['active', 'paused'] } }, async ({ status }) => {
3
+ const rows = await client.get('/domains', { query: { status } });
4
+ return { content: [{ type: 'text', text: JSON.stringify(rows, null, 2) }] };
5
+ });
6
+
7
+ server.tool('get_domain', 'Get domain details by ID.', { id: { type: 'string', description: 'Domain UUID' } }, async ({ id }) => {
8
+ const row = await client.get(`/domains/${id}`);
9
+ return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
10
+ });
11
+
12
+ server.tool('create_domain', 'Create a new domain.', { domain: { type: 'string', description: 'Domain name (e.g. trk.example.com)' } }, async (args) => {
13
+ const row = await client.post('/domains', args);
14
+ return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
15
+ });
16
+
17
+ server.tool('delete_domain', 'Delete a domain.', { id: { type: 'string', description: 'Domain UUID' } }, async ({ id }) => {
18
+ await client.del(`/domains/${id}`);
19
+ return { content: [{ type: 'text', text: 'Domain deleted.' }] };
20
+ });
21
+ }
@@ -0,0 +1,33 @@
1
+ export function registerFlowTools(server, client) {
2
+ server.tool('list_flows', 'List flows. Optional filter by campaign_id.', { campaign_id: { type: 'string', description: 'Campaign UUID (optional)' } }, async ({ campaign_id }) => {
3
+ const rows = await client.get('/flows', { query: { campaign_id } });
4
+ return { content: [{ type: 'text', text: JSON.stringify(rows, null, 2) }] };
5
+ });
6
+
7
+ server.tool('get_flow', 'Get flow details by ID.', { id: { type: 'string', description: 'Flow UUID' } }, async ({ id }) => {
8
+ const row = await client.get(`/flows/${id}`);
9
+ return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
10
+ });
11
+
12
+ server.tool('create_flow', 'Create a new flow for a campaign.', {
13
+ campaign_id: { type: 'string', description: 'Campaign UUID' },
14
+ name: { type: 'string', description: 'Flow name' },
15
+ }, async (args) => {
16
+ const row = await client.post('/flows', args);
17
+ return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
18
+ });
19
+
20
+ server.tool('update_flow', 'Update an existing flow.', {
21
+ id: { type: 'string', description: 'Flow UUID' },
22
+ name: { type: 'string', description: 'New name (optional)' },
23
+ }, async ({ id, ...rest }) => {
24
+ const body = Object.fromEntries(Object.entries(rest).filter(([, v]) => v !== undefined));
25
+ const row = await client.put(`/flows/${id}`, body);
26
+ return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
27
+ });
28
+
29
+ server.tool('delete_flow', 'Delete a flow.', { id: { type: 'string', description: 'Flow UUID' } }, async ({ id }) => {
30
+ await client.del(`/flows/${id}`);
31
+ return { content: [{ type: 'text', text: 'Flow deleted.' }] };
32
+ });
33
+ }
@@ -0,0 +1,34 @@
1
+ export function registerLandingTools(server, client) {
2
+ server.tool('list_landings', 'List landing pages. Optional filter by status.', { status: { type: 'string', description: 'active or paused', enum: ['active', 'paused'] } }, async ({ status }) => {
3
+ const rows = await client.get('/landings', { query: { status } });
4
+ return { content: [{ type: 'text', text: JSON.stringify(rows, null, 2) }] };
5
+ });
6
+
7
+ server.tool('get_landing', 'Get landing page details by ID.', { id: { type: 'string', description: 'Landing page UUID' } }, async ({ id }) => {
8
+ const row = await client.get(`/landings/${id}`);
9
+ return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
10
+ });
11
+
12
+ server.tool('create_landing', 'Create a new landing page.', {
13
+ name: { type: 'string', description: 'Landing page name' },
14
+ url: { type: 'string', description: 'Landing page URL' },
15
+ }, async (args) => {
16
+ const row = await client.post('/landings', args);
17
+ return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
18
+ });
19
+
20
+ server.tool('update_landing', 'Update an existing landing page.', {
21
+ id: { type: 'string', description: 'Landing page UUID' },
22
+ name: { type: 'string', description: 'New name (optional)' },
23
+ url: { type: 'string', description: 'New URL (optional)' },
24
+ }, async ({ id, ...rest }) => {
25
+ const body = Object.fromEntries(Object.entries(rest).filter(([, v]) => v !== undefined));
26
+ const row = await client.put(`/landings/${id}`, body);
27
+ return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
28
+ });
29
+
30
+ server.tool('delete_landing', 'Archive (soft-delete) a landing page.', { id: { type: 'string', description: 'Landing page UUID' } }, async ({ id }) => {
31
+ await client.del(`/landings/${id}`);
32
+ return { content: [{ type: 'text', text: 'Landing page archived.' }] };
33
+ });
34
+ }
@@ -0,0 +1,37 @@
1
+ export function registerOfferTools(server, client) {
2
+ server.tool('list_offers', 'List all offers. Optional filter by status.', { status: { type: 'string', description: 'active or paused', enum: ['active', 'paused'] } }, async ({ status }) => {
3
+ const rows = await client.get('/offers', { query: { status } });
4
+ return { content: [{ type: 'text', text: JSON.stringify(rows, null, 2) }] };
5
+ });
6
+
7
+ server.tool('get_offer', 'Get offer details by ID.', { id: { type: 'string', description: 'Offer UUID' } }, async ({ id }) => {
8
+ const row = await client.get(`/offers/${id}`);
9
+ return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
10
+ });
11
+
12
+ server.tool('create_offer', 'Create a new offer.', {
13
+ name: { type: 'string', description: 'Offer name' },
14
+ url: { type: 'string', description: 'Offer URL' },
15
+ payout: { type: 'number', description: 'Payout amount (optional)' },
16
+ currency: { type: 'string', description: 'Currency code (optional)' },
17
+ }, async (args) => {
18
+ const row = await client.post('/offers', args);
19
+ return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
20
+ });
21
+
22
+ server.tool('update_offer', 'Update an existing offer.', {
23
+ id: { type: 'string', description: 'Offer UUID' },
24
+ name: { type: 'string', description: 'New name (optional)' },
25
+ url: { type: 'string', description: 'New URL (optional)' },
26
+ payout: { type: 'number', description: 'New payout (optional)' },
27
+ }, async ({ id, ...rest }) => {
28
+ const body = Object.fromEntries(Object.entries(rest).filter(([, v]) => v !== undefined));
29
+ const row = await client.put(`/offers/${id}`, body);
30
+ return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
31
+ });
32
+
33
+ server.tool('delete_offer', 'Archive (soft-delete) an offer.', { id: { type: 'string', description: 'Offer UUID' } }, async ({ id }) => {
34
+ await client.del(`/offers/${id}`);
35
+ return { content: [{ type: 'text', text: 'Offer archived.' }] };
36
+ });
37
+ }
@@ -0,0 +1,22 @@
1
+ export function registerReportTools(server, client) {
2
+ server.tool('get_reports', 'Get tracker report data with date range and grouping.', {
3
+ 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, etc.' },
6
+ campaign_id: { type: 'string', description: 'Filter by campaign UUID (optional)' },
7
+ }, async ({ date_from, date_to, group_by, campaign_id }) => {
8
+ const data = await client.get('/reports', { query: { date_from, date_to, group_by, campaign_id } });
9
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
10
+ });
11
+
12
+ server.tool('get_conversions', 'Get conversion data with date range and filters.', {
13
+ date_from: { type: 'string', description: 'Start date YYYY-MM-DD' },
14
+ date_to: { type: 'string', description: 'End date YYYY-MM-DD' },
15
+ 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 10000)' },
18
+ }, async ({ date_from, date_to, campaign_id, status, limit }) => {
19
+ const data = await client.get('/conversions', { query: { date_from, date_to, campaign_id, status, limit } });
20
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
21
+ });
22
+ }
@@ -0,0 +1,20 @@
1
+ export function registerSettingsTools(server, client) {
2
+ server.tool('get_settings', 'Get all tracker settings. Optional filter by category.', {
3
+ category: { type: 'string', description: 'Settings category (optional)' },
4
+ }, async ({ category }) => {
5
+ const data = await client.get('/settings', { query: { category } });
6
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
7
+ });
8
+
9
+ server.tool('update_settings', 'Update one or more tracker settings.', {
10
+ settings: {
11
+ type: 'string',
12
+ description: 'JSON array of { key, value } objects, e.g. [{"key":"default_currency","value":"EUR"}]',
13
+ },
14
+ }, async ({ settings }) => {
15
+ let items;
16
+ try { items = JSON.parse(settings); } catch { return { content: [{ type: 'text', text: 'Error: settings must be a valid JSON array of {key, value} objects.' }], isError: true }; }
17
+ const data = await client.put('/settings', items);
18
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
19
+ });
20
+ }
@@ -0,0 +1,33 @@
1
+ export function registerSourceTools(server, client) {
2
+ server.tool('list_sources', 'List traffic sources.', {}, async () => {
3
+ const rows = await client.get('/sources');
4
+ return { content: [{ type: 'text', text: JSON.stringify(rows, null, 2) }] };
5
+ });
6
+
7
+ server.tool('get_source', 'Get traffic source details by ID.', { id: { type: 'string', description: 'Traffic source UUID' } }, async ({ id }) => {
8
+ const row = await client.get(`/sources/${id}`);
9
+ return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
10
+ });
11
+
12
+ server.tool('create_source', 'Create a new traffic source.', {
13
+ name: { type: 'string', description: 'Source name' },
14
+ type: { type: 'string', description: 'Source type (optional)' },
15
+ }, async (args) => {
16
+ const row = await client.post('/sources', args);
17
+ return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
18
+ });
19
+
20
+ server.tool('update_source', 'Update a traffic source.', {
21
+ id: { type: 'string', description: 'Traffic source UUID' },
22
+ name: { type: 'string', description: 'New name (optional)' },
23
+ }, async ({ id, ...rest }) => {
24
+ const body = Object.fromEntries(Object.entries(rest).filter(([, v]) => v !== undefined));
25
+ const row = await client.put(`/sources/${id}`, body);
26
+ return { content: [{ type: 'text', text: JSON.stringify(row, null, 2) }] };
27
+ });
28
+
29
+ server.tool('delete_source', 'Delete a traffic source.', { id: { type: 'string', description: 'Traffic source UUID' } }, async ({ id }) => {
30
+ await client.del(`/sources/${id}`);
31
+ return { content: [{ type: 'text', text: 'Traffic source deleted.' }] };
32
+ });
33
+ }
@@ -0,0 +1,6 @@
1
+ export function registerStatusTools(server, client) {
2
+ server.tool('get_system_status', 'Get tracker system status: hostname, CPU, memory, uptime, version.', {}, async () => {
3
+ const data = await client.get('/system-status');
4
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
5
+ });
6
+ }