@channel47/tiktok-ads-mcp 0.1.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.
@@ -0,0 +1,168 @@
1
+ import { validateArray } from './validation.js';
2
+
3
+ /**
4
+ * Supported mutate entity identifiers.
5
+ */
6
+ export const SUPPORTED_MUTATE_ENTITIES = ['campaign', 'adgroup', 'ad'];
7
+ /**
8
+ * Supported mutate action identifiers.
9
+ */
10
+ export const SUPPORTED_MUTATE_ACTIONS = ['create', 'update', 'pause', 'enable', 'delete'];
11
+
12
+ // TikTok status update endpoints take plural id arrays.
13
+ const STATUS_ID_FIELDS = {
14
+ campaign: 'campaign_ids',
15
+ adgroup: 'adgroup_ids',
16
+ ad: 'ad_ids'
17
+ };
18
+
19
+ // Update endpoints take a singular id field (ads identify targets via
20
+ // creatives[].ad_id inside params instead — see /ad/update/).
21
+ const UPDATE_ID_FIELDS = {
22
+ campaign: 'campaign_id',
23
+ adgroup: 'adgroup_id'
24
+ };
25
+
26
+ const STATUS_BY_ACTION = {
27
+ pause: 'DISABLE',
28
+ enable: 'ENABLE',
29
+ delete: 'DELETE'
30
+ };
31
+
32
+ // /campaign/create/ and /adgroup/create/ default operation_status to ENABLE
33
+ // server-side; this server defaults them to DISABLE (paused) for safety.
34
+ // /ad/create/ has no top-level operation_status field.
35
+ const PAUSED_CREATE_ENTITIES = new Set(['campaign', 'adgroup']);
36
+
37
+ function validateOperationShape(op, index) {
38
+ if (!op || typeof op !== 'object' || Array.isArray(op)) {
39
+ return { index, message: 'Operation must be an object' };
40
+ }
41
+
42
+ if (!op.entity) {
43
+ return { index, message: 'Missing required field: entity' };
44
+ }
45
+
46
+ if (!SUPPORTED_MUTATE_ENTITIES.includes(op.entity)) {
47
+ return {
48
+ index,
49
+ message: `Unsupported entity: ${op.entity}. Supported: ${SUPPORTED_MUTATE_ENTITIES.join(', ')}`
50
+ };
51
+ }
52
+
53
+ if (!op.action) {
54
+ return { index, message: 'Missing required field: action' };
55
+ }
56
+
57
+ if (!SUPPORTED_MUTATE_ACTIONS.includes(op.action)) {
58
+ return {
59
+ index,
60
+ message: `Unsupported action: ${op.action}. Supported: ${SUPPORTED_MUTATE_ACTIONS.join(', ')}`
61
+ };
62
+ }
63
+
64
+ if ((op.action === 'create' || op.action === 'update') && (!op.params || typeof op.params !== 'object' || Array.isArray(op.params))) {
65
+ return {
66
+ index,
67
+ message: `${op.action} requires params object`
68
+ };
69
+ }
70
+
71
+ const idOptional = op.action === 'create' || (op.action === 'update' && op.entity === 'ad');
72
+ if (!idOptional && !op.id) {
73
+ return {
74
+ index,
75
+ message: `${op.action} requires id`
76
+ };
77
+ }
78
+
79
+ return null;
80
+ }
81
+
82
+ /**
83
+ * Validate mutate operation array shape and action/entity compatibility.
84
+ * @param {unknown} operations
85
+ * @returns {Array<{ index: number, message: string }>}
86
+ */
87
+ export function validateOperations(operations) {
88
+ validateArray(operations, 'operations');
89
+
90
+ const errors = [];
91
+ for (let index = 0; index < operations.length; index += 1) {
92
+ const issue = validateOperationShape(operations[index], index);
93
+ if (issue) {
94
+ errors.push(issue);
95
+ }
96
+ }
97
+
98
+ return errors;
99
+ }
100
+
101
+ /**
102
+ * Build a TikTok Business API request payload from a single mutate operation.
103
+ * @param {{ entity: string, action: string, id?: string, params?: Record<string, unknown> }} operation
104
+ * @param {string} advertiserId
105
+ * @returns {{ method: string, path: string, body: Record<string, unknown> }}
106
+ */
107
+ export function buildApiRequest(operation, advertiserId) {
108
+ const { entity, action } = operation;
109
+
110
+ if (action === 'create') {
111
+ const defaults = PAUSED_CREATE_ENTITIES.has(entity) ? { operation_status: 'DISABLE' } : {};
112
+ return {
113
+ method: 'POST',
114
+ path: `/${entity}/create/`,
115
+ body: {
116
+ advertiser_id: advertiserId,
117
+ ...defaults,
118
+ ...operation.params
119
+ }
120
+ };
121
+ }
122
+
123
+ if (action === 'update') {
124
+ const idField = UPDATE_ID_FIELDS[entity];
125
+ const idParams = idField && operation.id ? { [idField]: String(operation.id) } : {};
126
+ return {
127
+ method: 'POST',
128
+ path: `/${entity}/update/`,
129
+ body: {
130
+ advertiser_id: advertiserId,
131
+ ...idParams,
132
+ ...operation.params
133
+ }
134
+ };
135
+ }
136
+
137
+ return {
138
+ method: 'POST',
139
+ path: `/${entity}/status/update/`,
140
+ body: {
141
+ advertiser_id: advertiserId,
142
+ [STATUS_ID_FIELDS[entity]]: [String(operation.id)],
143
+ operation_status: STATUS_BY_ACTION[action]
144
+ }
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Build a human-readable preview of API requests derived from operations.
150
+ * @param {Array<{ entity: string, action: string, id?: string, params?: Record<string, unknown> }>} operations
151
+ * @param {string} advertiserId
152
+ * @returns {{ requests: Array<Record<string, unknown>> }}
153
+ */
154
+ export function buildRequestPreview(operations, advertiserId) {
155
+ return {
156
+ requests: operations.map((operation, index) => {
157
+ const request = buildApiRequest(operation, advertiserId);
158
+ return {
159
+ index,
160
+ entity: operation.entity,
161
+ action: operation.action,
162
+ method: request.method,
163
+ path: request.path,
164
+ body: request.body
165
+ };
166
+ })
167
+ };
168
+ }
@@ -0,0 +1,63 @@
1
+ let SdkMcpError = null;
2
+ let SdkErrorCode = null;
3
+
4
+ try {
5
+ const sdkTypes = await import('@modelcontextprotocol/sdk/types.js');
6
+ SdkMcpError = sdkTypes.McpError;
7
+ SdkErrorCode = sdkTypes.ErrorCode;
8
+ } catch {
9
+ // SDK may be absent in minimal test environments.
10
+ }
11
+
12
+ /**
13
+ * Format a successful tool result payload in MCP text content structure.
14
+ * @param {{ summary: string, data: any, metadata?: Record<string, unknown> }} payload
15
+ * @returns {import('@modelcontextprotocol/sdk/types.js').CallToolResult}
16
+ */
17
+ export function formatSuccess({ summary, data, metadata = {} }) {
18
+ return {
19
+ content: [{
20
+ type: 'text',
21
+ text: JSON.stringify({
22
+ success: true,
23
+ summary,
24
+ data,
25
+ metadata: {
26
+ rowCount: Array.isArray(data) ? data.length : undefined,
27
+ warnings: [],
28
+ ...metadata
29
+ }
30
+ }, null, 2)
31
+ }]
32
+ };
33
+ }
34
+
35
+ function createMcpStyleError(code, message) {
36
+ if (SdkMcpError && SdkErrorCode && SdkErrorCode[code]) {
37
+ return new SdkMcpError(SdkErrorCode[code], message);
38
+ }
39
+
40
+ const error = new Error(message);
41
+ error.code = code;
42
+ return error;
43
+ }
44
+
45
+ /**
46
+ * Convert thrown errors into MCP-compatible typed errors.
47
+ * @param {unknown} error
48
+ * @throws {Error}
49
+ */
50
+ export function formatError(error) {
51
+ const message = error?.message || String(error) || 'Unknown error';
52
+ const explicitCode = error?.mcpCode === 'InvalidParams' || error?.code === 'InvalidParams'
53
+ ? 'InvalidParams'
54
+ : error?.mcpCode === 'InternalError' || error?.code === 'InternalError'
55
+ ? 'InternalError'
56
+ : null;
57
+
58
+ if (explicitCode) {
59
+ throw createMcpStyleError(explicitCode, message);
60
+ }
61
+
62
+ throw createMcpStyleError('InternalError', message);
63
+ }
@@ -0,0 +1,70 @@
1
+ import { invalidParamsError } from './errors.js';
2
+
3
+ /**
4
+ * Validate that required fields are present on the params object.
5
+ */
6
+ export function validateRequired(params, fields) {
7
+ const missing = fields.filter((field) => {
8
+ const value = params[field];
9
+ return value === undefined || value === null || value === '';
10
+ });
11
+
12
+ if (missing.length > 0) {
13
+ throw invalidParamsError(`Missing required parameter${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`);
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Validate an enum value against an allowed set.
19
+ */
20
+ export function validateEnum(value, allowed, paramName = 'value') {
21
+ if (!allowed.includes(value)) {
22
+ throw invalidParamsError(`Invalid ${paramName}: ${value}. Allowed values: ${allowed.join(', ')}`);
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Validate that a value is a non-empty array.
28
+ */
29
+ export function validateArray(value, paramName = 'value') {
30
+ if (!Array.isArray(value) || value.length === 0) {
31
+ throw invalidParamsError(`${paramName} must be a non-empty array`);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Validate a YYYY-MM-DD date string.
37
+ */
38
+ export function validateDate(value, label = 'date') {
39
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(String(value))) {
40
+ throw invalidParamsError(`Invalid ${label} format: ${value}. Expected YYYY-MM-DD`);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Resolve a positive row limit with default and cap.
46
+ */
47
+ export function getRequestedLimit(limitValue, { defaultLimit = 100, maxLimit = 1000 } = {}) {
48
+ if (limitValue === undefined || limitValue === null || limitValue === '') {
49
+ return defaultLimit;
50
+ }
51
+
52
+ const parsed = Number(limitValue);
53
+ if (!Number.isFinite(parsed) || parsed <= 0) {
54
+ return defaultLimit;
55
+ }
56
+
57
+ return Math.min(Math.floor(parsed), maxLimit);
58
+ }
59
+
60
+ /**
61
+ * Resolve advertiser ID from params or environment.
62
+ */
63
+ export function getAdvertiserId(params = {}) {
64
+ const advertiserId = params.advertiser_id || process.env.TIKTOK_ADS_ADVERTISER_ID;
65
+ if (!advertiserId) {
66
+ throw invalidParamsError('advertiser_id parameter or TIKTOK_ADS_ADVERTISER_ID environment variable required');
67
+ }
68
+
69
+ return String(advertiserId).trim();
70
+ }