@channel47/pinterest-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,174 @@
1
+ import { validateArray } from './validation.js';
2
+
3
+ /**
4
+ * Supported mutate entity identifiers.
5
+ */
6
+ export const SUPPORTED_MUTATE_ENTITIES = ['campaign', 'ad_group', 'ad'];
7
+ /**
8
+ * Supported mutate action identifiers. Pinterest v5 has no delete for ads
9
+ * entities — ARCHIVED (via archive) is the terminal state.
10
+ */
11
+ export const SUPPORTED_MUTATE_ACTIONS = ['create', 'update', 'pause', 'enable', 'archive'];
12
+
13
+ const ENTITY_COLLECTIONS = {
14
+ campaign: 'campaigns',
15
+ ad_group: 'ad_groups',
16
+ ad: 'ads'
17
+ };
18
+
19
+ const CREATE_REQUIRED_FIELDS = {
20
+ campaign: ['name', 'objective_type'],
21
+ ad_group: ['name', 'campaign_id', 'billable_event'],
22
+ ad: ['ad_group_id', 'pin_id', 'creative_type']
23
+ };
24
+
25
+ const STATUS_ACTIONS = {
26
+ pause: 'PAUSED',
27
+ enable: 'ACTIVE',
28
+ archive: 'ARCHIVED'
29
+ };
30
+
31
+ function validateOperationShape(op, index) {
32
+ if (!op || typeof op !== 'object' || Array.isArray(op)) {
33
+ return { index, message: 'Operation must be an object' };
34
+ }
35
+
36
+ if (!op.entity) {
37
+ return { index, message: 'Missing required field: entity' };
38
+ }
39
+
40
+ if (!SUPPORTED_MUTATE_ENTITIES.includes(op.entity)) {
41
+ return {
42
+ index,
43
+ message: `Unsupported entity: ${op.entity}. Supported: ${SUPPORTED_MUTATE_ENTITIES.join(', ')}`
44
+ };
45
+ }
46
+
47
+ if (!op.action) {
48
+ return { index, message: 'Missing required field: action' };
49
+ }
50
+
51
+ if (!SUPPORTED_MUTATE_ACTIONS.includes(op.action)) {
52
+ return {
53
+ index,
54
+ message: `Unsupported action: ${op.action}. Supported: ${SUPPORTED_MUTATE_ACTIONS.join(', ')} (Pinterest has no delete — use archive)`
55
+ };
56
+ }
57
+
58
+ if ((op.action === 'create' || op.action === 'update') && (!op.params || typeof op.params !== 'object' || Array.isArray(op.params))) {
59
+ return {
60
+ index,
61
+ message: `${op.action} requires params object`
62
+ };
63
+ }
64
+
65
+ if (op.action !== 'create' && !op.id) {
66
+ return {
67
+ index,
68
+ message: `${op.action} requires id`
69
+ };
70
+ }
71
+
72
+ if (op.action === 'create') {
73
+ const requiredFields = CREATE_REQUIRED_FIELDS[op.entity];
74
+ const missing = requiredFields.filter((field) => {
75
+ const value = op.params[field];
76
+ return value === undefined || value === null || value === '';
77
+ });
78
+
79
+ if (missing.length > 0) {
80
+ return {
81
+ index,
82
+ message: `${op.entity} create requires params: ${missing.join(', ')}`
83
+ };
84
+ }
85
+ }
86
+
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * Validate mutate operation array shape and action/entity compatibility.
92
+ * @param {unknown} operations
93
+ * @returns {Array<{ index: number, message: string }>}
94
+ */
95
+ export function validateOperations(operations) {
96
+ validateArray(operations, 'operations');
97
+
98
+ const errors = [];
99
+ for (let index = 0; index < operations.length; index += 1) {
100
+ const issue = validateOperationShape(operations[index], index);
101
+ if (issue) {
102
+ errors.push(issue);
103
+ }
104
+ }
105
+
106
+ return errors;
107
+ }
108
+
109
+ /**
110
+ * Build a Pinterest v5 API request from a single mutate operation.
111
+ * Creates are POST with a one-element array body; updates and status changes
112
+ * are PATCH with a one-element array body of { id, ...changes }.
113
+ * @param {{ entity: string, action: string, id?: string, params?: Record<string, unknown> }} operation
114
+ * @param {string} adAccountId
115
+ * @returns {{ method: string, path: string, body: Array<Record<string, unknown>> }}
116
+ */
117
+ export function buildApiRequest(operation, adAccountId) {
118
+ const action = operation.action;
119
+ const path = `/ad_accounts/${adAccountId}/${ENTITY_COLLECTIONS[operation.entity]}`;
120
+
121
+ if (action === 'create') {
122
+ return {
123
+ method: 'POST',
124
+ path,
125
+ body: [{
126
+ status: 'PAUSED',
127
+ ...operation.params
128
+ }]
129
+ };
130
+ }
131
+
132
+ if (action === 'update') {
133
+ return {
134
+ method: 'PATCH',
135
+ path,
136
+ body: [{
137
+ ...operation.params,
138
+ id: String(operation.id)
139
+ }]
140
+ };
141
+ }
142
+
143
+ return {
144
+ method: 'PATCH',
145
+ path,
146
+ body: [{
147
+ ...(operation.params || {}),
148
+ id: String(operation.id),
149
+ status: STATUS_ACTIONS[action]
150
+ }]
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Build a human-readable preview of API requests derived from operations.
156
+ * @param {Array<{ entity: string, action: string, id?: string, params?: Record<string, unknown> }>} operations
157
+ * @param {string} adAccountId
158
+ * @returns {{ requests: Array<Record<string, unknown>> }}
159
+ */
160
+ export function buildRequestPreview(operations, adAccountId) {
161
+ return {
162
+ requests: operations.map((operation, index) => {
163
+ const request = buildApiRequest(operation, adAccountId);
164
+ return {
165
+ index,
166
+ entity: operation.entity,
167
+ action: operation.action,
168
+ method: request.method,
169
+ path: request.path,
170
+ body: request.body
171
+ };
172
+ })
173
+ };
174
+ }
@@ -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,63 @@
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
+ * Coerce a comma-separated string or array into a trimmed string array.
37
+ */
38
+ export function toIdList(value) {
39
+ if (value === undefined || value === null || value === '') {
40
+ return [];
41
+ }
42
+
43
+ if (Array.isArray(value)) {
44
+ return value.map((entry) => String(entry).trim()).filter(Boolean);
45
+ }
46
+
47
+ return String(value)
48
+ .split(',')
49
+ .map((entry) => entry.trim())
50
+ .filter(Boolean);
51
+ }
52
+
53
+ /**
54
+ * Resolve the ad account ID from params or environment.
55
+ */
56
+ export function getAdAccountId(params = {}) {
57
+ const adAccountId = params.ad_account_id || process.env.PINTEREST_ADS_AD_ACCOUNT_ID;
58
+ if (!adAccountId) {
59
+ throw invalidParamsError('ad_account_id parameter or PINTEREST_ADS_AD_ACCOUNT_ID environment variable required');
60
+ }
61
+
62
+ return String(adAccountId).trim();
63
+ }