@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.
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/package.json +47 -0
- package/server/auth.js +141 -0
- package/server/http.js +198 -0
- package/server/index.js +272 -0
- package/server/prompts/templates.js +130 -0
- package/server/resources/analytics-columns-reference.md +86 -0
- package/server/resources/api-reference.md +100 -0
- package/server/resources/index.js +65 -0
- package/server/resources/rate-limits-and-quotas.md +39 -0
- package/server/tools/analytics.js +102 -0
- package/server/tools/list-accounts.js +68 -0
- package/server/tools/mutate.js +134 -0
- package/server/tools/query.js +130 -0
- package/server/utils/analytics-params.js +106 -0
- package/server/utils/errors.js +12 -0
- package/server/utils/mutate-operations.js +174 -0
- package/server/utils/response-format.js +63 -0
- package/server/utils/validation.js +63 -0
|
@@ -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
|
+
}
|