@zackbart/connecta 0.22.0 → 0.22.1
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/CHANGELOG.md +16 -0
- package/README.md +3 -3
- package/dist/providers/vercel.d.ts +25 -0
- package/dist/providers/vercel.js +1132 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/connector-guides.md +2 -2
- package/documentation/connectors.md +8 -6
- package/documentation/operations.md +2 -1
- package/documentation/provider-conventions.md +5 -5
- package/documentation/upgrading.md +5 -5
- package/documentation/vercel.md +194 -0
- package/package.json +5 -1
- package/templates/node/package.json +1 -1
|
@@ -0,0 +1,1132 @@
|
|
|
1
|
+
/** See documentation/vercel.md#no-sdk-on-purpose. */
|
|
2
|
+
import { api } from "../connectors/api.js";
|
|
3
|
+
import { guardedFetch, retryAfterMs, } from "../connectors/guarded-fetch.js";
|
|
4
|
+
import { ConnectorCallError } from "../errors.js";
|
|
5
|
+
import { withDeadline } from "../timeout.js";
|
|
6
|
+
/** Vercel's public REST origin. Override only for a proxy or test double. */
|
|
7
|
+
export const VERCEL_API_BASE_URL = "https://api.vercel.com";
|
|
8
|
+
const MAX_PAGE_SIZE = 100;
|
|
9
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
10
|
+
const VERCEL_MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
11
|
+
const MAX_RUNTIME_LOG_ROWS = 500;
|
|
12
|
+
const DEFAULT_RUNTIME_LOG_ROWS = 100;
|
|
13
|
+
const RUNTIME_LOG_TIMEOUT_MS = 10_000;
|
|
14
|
+
function asRecord(value) {
|
|
15
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
16
|
+
? value
|
|
17
|
+
: {};
|
|
18
|
+
}
|
|
19
|
+
function asArray(value) {
|
|
20
|
+
return Array.isArray(value) ? value : [];
|
|
21
|
+
}
|
|
22
|
+
function compact(value) {
|
|
23
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
24
|
+
}
|
|
25
|
+
function detailFor(payload, status) {
|
|
26
|
+
const root = asRecord(payload);
|
|
27
|
+
const error = asRecord(root["error"]);
|
|
28
|
+
const code = typeof error["code"] === "string" ? error["code"] : undefined;
|
|
29
|
+
const message = typeof error["message"] === "string" && error["message"].trim()
|
|
30
|
+
? error["message"].trim()
|
|
31
|
+
: typeof root["message"] === "string" && root["message"].trim()
|
|
32
|
+
? root["message"].trim()
|
|
33
|
+
: `Vercel returned HTTP ${status}.`;
|
|
34
|
+
return code ? `Vercel ${code}: ${message}` : message;
|
|
35
|
+
}
|
|
36
|
+
function resetAfterMs(headers) {
|
|
37
|
+
const retryAfter = retryAfterMs(headers);
|
|
38
|
+
if (retryAfter !== undefined)
|
|
39
|
+
return retryAfter;
|
|
40
|
+
const raw = headers.get("x-ratelimit-reset");
|
|
41
|
+
if (!raw)
|
|
42
|
+
return undefined;
|
|
43
|
+
const seconds = Number(raw);
|
|
44
|
+
if (!Number.isFinite(seconds))
|
|
45
|
+
return undefined;
|
|
46
|
+
return Math.max(0, Math.trunc(seconds * 1_000 - Date.now()));
|
|
47
|
+
}
|
|
48
|
+
/** Map Vercel failures by the caller's useful next move. */
|
|
49
|
+
function vercelFailure(status, headers, payload) {
|
|
50
|
+
const detail = detailFor(payload, status);
|
|
51
|
+
if (status === 429) {
|
|
52
|
+
const wait = resetAfterMs(headers);
|
|
53
|
+
return new ConnectorCallError("rate_limited", `${detail} Vercel meters endpoints separately; wait for the reported reset before retrying this operation.`, wait === undefined ? {} : { retryAfterMs: wait });
|
|
54
|
+
}
|
|
55
|
+
if (status === 401 || status === 403) {
|
|
56
|
+
return new ConnectorCallError("auth_required", `${detail} The configured access token is invalid, expired, outside this team, or lacks the required scope. An operator must replace it or widen its Vercel scope.`);
|
|
57
|
+
}
|
|
58
|
+
if (status === 404) {
|
|
59
|
+
return new ConnectorCallError("not_found", `${detail} Confirm the project, deployment, domain, or environment-variable id with its list tool.`);
|
|
60
|
+
}
|
|
61
|
+
if (status === 400 || status === 409 || status === 422) {
|
|
62
|
+
return new ConnectorCallError("invalid_args", detail);
|
|
63
|
+
}
|
|
64
|
+
if (status >= 500) {
|
|
65
|
+
const wait = resetAfterMs(headers);
|
|
66
|
+
return new ConnectorCallError("unavailable", `${detail} Vercel is failing upstream.`, wait === undefined ? {} : { retryAfterMs: wait });
|
|
67
|
+
}
|
|
68
|
+
return new ConnectorCallError("connector_call_failed", detail, {
|
|
69
|
+
retryable: false,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function parseBody(text, contentType) {
|
|
73
|
+
if (!text)
|
|
74
|
+
return undefined;
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(text);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
if (contentType?.includes("stream+json") || contentType?.includes("ndjson")) {
|
|
80
|
+
const rows = [];
|
|
81
|
+
for (const line of text.split("\n")) {
|
|
82
|
+
if (!line.trim())
|
|
83
|
+
continue;
|
|
84
|
+
try {
|
|
85
|
+
rows.push(JSON.parse(line));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
rows.push({ message: line });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return rows;
|
|
92
|
+
}
|
|
93
|
+
return text;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function parseStreamRows(text) {
|
|
97
|
+
if (!text.trim())
|
|
98
|
+
return [];
|
|
99
|
+
try {
|
|
100
|
+
const payload = JSON.parse(text);
|
|
101
|
+
return Array.isArray(payload) ? payload : [payload];
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
const rows = [];
|
|
105
|
+
for (const line of text.split("\n")) {
|
|
106
|
+
if (!line.trim())
|
|
107
|
+
continue;
|
|
108
|
+
try {
|
|
109
|
+
rows.push(JSON.parse(line));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
throw new ConnectorCallError("connector_call_failed", "Vercel returned a malformed runtime-log stream.", { retryable: false });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return rows;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function vercelTransport(baseUrl) {
|
|
119
|
+
return guardedFetch({
|
|
120
|
+
provider: "Vercel",
|
|
121
|
+
baseUrl,
|
|
122
|
+
headers: { Accept: "application/json" },
|
|
123
|
+
maxResponseBytes: VERCEL_MAX_RESPONSE_BYTES,
|
|
124
|
+
authenticate: async (ctx) => {
|
|
125
|
+
const token = (await ctx.credential?.get())?.trim();
|
|
126
|
+
if (!token) {
|
|
127
|
+
throw new ConnectorCallError("auth_required", "No Vercel access token is configured for this connector. An operator must add one on /credentials before any Vercel call can run.");
|
|
128
|
+
}
|
|
129
|
+
return { Authorization: `Bearer ${token}` };
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async function callVercel(send, request, ctx, parseSuccess) {
|
|
134
|
+
return await send(request, ctx, async (response) => {
|
|
135
|
+
const text = await response.text();
|
|
136
|
+
const payload = parseBody(text, response.headers.get("content-type"));
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
throw vercelFailure(response.status, response.headers, payload);
|
|
139
|
+
}
|
|
140
|
+
return parseSuccess ? parseSuccess(text) : payload;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function teamQuery(args, defaultTeamId) {
|
|
144
|
+
return {
|
|
145
|
+
teamId: args["teamId"] === null
|
|
146
|
+
? undefined
|
|
147
|
+
: args["teamId"] ?? defaultTeamId,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function nextCursor(payload) {
|
|
151
|
+
const pagination = asRecord(asRecord(payload)["pagination"]);
|
|
152
|
+
const next = pagination["next"];
|
|
153
|
+
return next === undefined || next === null || next === "" ? null : String(next);
|
|
154
|
+
}
|
|
155
|
+
function page(payload) {
|
|
156
|
+
const cursor = nextCursor(payload);
|
|
157
|
+
return { hasMore: cursor !== null, nextCursor: cursor };
|
|
158
|
+
}
|
|
159
|
+
function projectTeam(value) {
|
|
160
|
+
const team = asRecord(value);
|
|
161
|
+
return compact({
|
|
162
|
+
id: team["id"],
|
|
163
|
+
slug: team["slug"],
|
|
164
|
+
name: team["name"],
|
|
165
|
+
avatar: team["avatar"],
|
|
166
|
+
createdAt: team["createdAt"],
|
|
167
|
+
membership: asRecord(team["membership"])["role"],
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function projectProject(value) {
|
|
171
|
+
const project = asRecord(value);
|
|
172
|
+
const link = asRecord(project["link"]);
|
|
173
|
+
const targets = asRecord(project["targets"]);
|
|
174
|
+
const production = asRecord(targets["production"]);
|
|
175
|
+
return compact({
|
|
176
|
+
id: project["id"],
|
|
177
|
+
name: project["name"],
|
|
178
|
+
accountId: project["accountId"],
|
|
179
|
+
framework: project["framework"],
|
|
180
|
+
createdAt: project["createdAt"],
|
|
181
|
+
updatedAt: project["updatedAt"],
|
|
182
|
+
paused: project["paused"] === true,
|
|
183
|
+
productionBranch: project["productionBranch"] ?? link["productionBranch"],
|
|
184
|
+
rootDirectory: project["rootDirectory"],
|
|
185
|
+
nodeVersion: project["nodeVersion"],
|
|
186
|
+
buildCommand: project["buildCommand"],
|
|
187
|
+
installCommand: project["installCommand"],
|
|
188
|
+
devCommand: project["devCommand"],
|
|
189
|
+
outputDirectory: project["outputDirectory"],
|
|
190
|
+
repository: Object.keys(link).length === 0
|
|
191
|
+
? undefined
|
|
192
|
+
: compact({
|
|
193
|
+
type: link["type"],
|
|
194
|
+
org: link["org"],
|
|
195
|
+
repo: link["repo"],
|
|
196
|
+
repoId: link["repoId"],
|
|
197
|
+
}),
|
|
198
|
+
productionDeployment: Object.keys(production).length === 0
|
|
199
|
+
? undefined
|
|
200
|
+
: compact({
|
|
201
|
+
id: production["id"] ?? production["uid"],
|
|
202
|
+
url: production["url"],
|
|
203
|
+
state: production["readyState"] ?? production["state"],
|
|
204
|
+
createdAt: production["createdAt"] ?? production["created"],
|
|
205
|
+
}),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function projectDeployment(value) {
|
|
209
|
+
const deployment = asRecord(value);
|
|
210
|
+
const creator = asRecord(deployment["creator"]);
|
|
211
|
+
const meta = asRecord(deployment["meta"]);
|
|
212
|
+
return compact({
|
|
213
|
+
id: deployment["uid"] ?? deployment["id"],
|
|
214
|
+
name: deployment["name"],
|
|
215
|
+
url: deployment["url"],
|
|
216
|
+
state: deployment["readyState"] ?? deployment["state"],
|
|
217
|
+
target: deployment["target"],
|
|
218
|
+
source: deployment["source"],
|
|
219
|
+
createdAt: deployment["createdAt"] ?? deployment["created"],
|
|
220
|
+
buildingAt: deployment["buildingAt"],
|
|
221
|
+
readyAt: deployment["ready"] ?? deployment["readyAt"],
|
|
222
|
+
projectId: deployment["projectId"],
|
|
223
|
+
creator: Object.keys(creator).length === 0
|
|
224
|
+
? undefined
|
|
225
|
+
: compact({
|
|
226
|
+
id: creator["uid"] ?? creator["id"],
|
|
227
|
+
username: creator["username"],
|
|
228
|
+
email: creator["email"],
|
|
229
|
+
}),
|
|
230
|
+
git: meta["githubCommitRef"] || meta["gitlabCommitRef"] || meta["bitbucketCommitRef"]
|
|
231
|
+
? compact({
|
|
232
|
+
branch: meta["githubCommitRef"] ??
|
|
233
|
+
meta["gitlabCommitRef"] ??
|
|
234
|
+
meta["bitbucketCommitRef"],
|
|
235
|
+
sha: meta["githubCommitSha"] ??
|
|
236
|
+
meta["gitlabCommitSha"] ??
|
|
237
|
+
meta["bitbucketCommitSha"],
|
|
238
|
+
message: meta["githubCommitMessage"] ??
|
|
239
|
+
meta["gitlabCommitMessage"] ??
|
|
240
|
+
meta["bitbucketCommitMessage"],
|
|
241
|
+
})
|
|
242
|
+
: undefined,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function projectDomain(value) {
|
|
246
|
+
const domain = asRecord(value);
|
|
247
|
+
return compact({
|
|
248
|
+
name: domain["name"],
|
|
249
|
+
apexName: domain["apexName"],
|
|
250
|
+
projectId: domain["projectId"],
|
|
251
|
+
verified: domain["verified"] === true,
|
|
252
|
+
verification: domain["verification"],
|
|
253
|
+
redirect: domain["redirect"],
|
|
254
|
+
redirectStatusCode: domain["redirectStatusCode"],
|
|
255
|
+
gitBranch: domain["gitBranch"],
|
|
256
|
+
customEnvironmentId: domain["customEnvironmentId"],
|
|
257
|
+
createdAt: domain["createdAt"],
|
|
258
|
+
updatedAt: domain["updatedAt"],
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
/** Deliberately omits `value`, even if a raw Vercel response happens to carry it. */
|
|
262
|
+
function projectEnvironmentVariable(value) {
|
|
263
|
+
const variable = asRecord(value);
|
|
264
|
+
return compact({
|
|
265
|
+
id: variable["id"],
|
|
266
|
+
key: variable["key"],
|
|
267
|
+
type: variable["type"],
|
|
268
|
+
visibility: variable["visibility"],
|
|
269
|
+
target: variable["target"],
|
|
270
|
+
gitBranch: variable["gitBranch"],
|
|
271
|
+
customEnvironmentIds: variable["customEnvironmentIds"],
|
|
272
|
+
comment: variable["comment"],
|
|
273
|
+
createdAt: variable["createdAt"],
|
|
274
|
+
updatedAt: variable["updatedAt"],
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
const RAW_PROPERTY = {
|
|
278
|
+
type: "boolean",
|
|
279
|
+
description: "Return Vercel's untouched response instead of the lean projection.",
|
|
280
|
+
};
|
|
281
|
+
const TEAM_ID_PROPERTY = {
|
|
282
|
+
type: ["string", "null"],
|
|
283
|
+
minLength: 1,
|
|
284
|
+
description: "Vercel team id. Omit for the configured default; pass null for the token owner's personal account.",
|
|
285
|
+
};
|
|
286
|
+
const PROJECT_ID_PROPERTY = {
|
|
287
|
+
type: "string",
|
|
288
|
+
minLength: 1,
|
|
289
|
+
description: "Project id or project name from list_projects.",
|
|
290
|
+
};
|
|
291
|
+
const DEPLOYMENT_ID_PROPERTY = {
|
|
292
|
+
type: "string",
|
|
293
|
+
minLength: 1,
|
|
294
|
+
description: "Deployment id from list_deployments.",
|
|
295
|
+
};
|
|
296
|
+
const CURSOR_PROPERTY = {
|
|
297
|
+
type: "string",
|
|
298
|
+
minLength: 1,
|
|
299
|
+
description: "Opaque nextCursor returned by the previous page. Pass it back unchanged.",
|
|
300
|
+
};
|
|
301
|
+
function limitProperty(defaultPageSize) {
|
|
302
|
+
return {
|
|
303
|
+
type: "integer",
|
|
304
|
+
minimum: 1,
|
|
305
|
+
maximum: MAX_PAGE_SIZE,
|
|
306
|
+
description: `Rows per request, 1 to ${MAX_PAGE_SIZE}. Defaults to this connector's ${defaultPageSize}.`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const PAGE_SCHEMA = {
|
|
310
|
+
type: "object",
|
|
311
|
+
properties: {
|
|
312
|
+
hasMore: { type: "boolean" },
|
|
313
|
+
nextCursor: {
|
|
314
|
+
type: ["string", "null"],
|
|
315
|
+
description: "Pass back unchanged as cursor when hasMore is true.",
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
required: ["hasMore", "nextCursor"],
|
|
319
|
+
};
|
|
320
|
+
function listSchema(key, item) {
|
|
321
|
+
return {
|
|
322
|
+
type: "object",
|
|
323
|
+
properties: {
|
|
324
|
+
[key]: { type: "array", items: item },
|
|
325
|
+
page: PAGE_SCHEMA,
|
|
326
|
+
},
|
|
327
|
+
required: [key, "page"],
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const TEAM_SCHEMA = {
|
|
331
|
+
type: "object",
|
|
332
|
+
properties: {
|
|
333
|
+
id: { type: "string" },
|
|
334
|
+
slug: { type: "string" },
|
|
335
|
+
name: { type: "string" },
|
|
336
|
+
avatar: { type: ["string", "null"] },
|
|
337
|
+
createdAt: { type: "number" },
|
|
338
|
+
membership: { type: "string" },
|
|
339
|
+
},
|
|
340
|
+
required: ["id", "slug", "name"],
|
|
341
|
+
};
|
|
342
|
+
const PROJECT_SCHEMA = {
|
|
343
|
+
type: "object",
|
|
344
|
+
properties: {
|
|
345
|
+
id: { type: "string" },
|
|
346
|
+
name: { type: "string" },
|
|
347
|
+
accountId: { type: "string" },
|
|
348
|
+
framework: { type: ["string", "null"] },
|
|
349
|
+
createdAt: { type: "number" },
|
|
350
|
+
updatedAt: { type: "number" },
|
|
351
|
+
paused: { type: "boolean" },
|
|
352
|
+
productionBranch: { type: "string" },
|
|
353
|
+
rootDirectory: { type: ["string", "null"] },
|
|
354
|
+
nodeVersion: { type: "string" },
|
|
355
|
+
buildCommand: { type: ["string", "null"] },
|
|
356
|
+
installCommand: { type: ["string", "null"] },
|
|
357
|
+
devCommand: { type: ["string", "null"] },
|
|
358
|
+
outputDirectory: { type: ["string", "null"] },
|
|
359
|
+
repository: { type: "object" },
|
|
360
|
+
productionDeployment: { type: "object" },
|
|
361
|
+
},
|
|
362
|
+
required: ["id", "name"],
|
|
363
|
+
};
|
|
364
|
+
const DEPLOYMENT_SCHEMA = {
|
|
365
|
+
type: "object",
|
|
366
|
+
properties: {
|
|
367
|
+
id: { type: "string" },
|
|
368
|
+
name: { type: "string" },
|
|
369
|
+
url: { type: ["string", "null"] },
|
|
370
|
+
state: { type: "string" },
|
|
371
|
+
target: { type: ["string", "null"] },
|
|
372
|
+
source: { type: "string" },
|
|
373
|
+
createdAt: { type: "number" },
|
|
374
|
+
buildingAt: { type: "number" },
|
|
375
|
+
readyAt: { type: "number" },
|
|
376
|
+
projectId: { type: "string" },
|
|
377
|
+
creator: { type: "object" },
|
|
378
|
+
git: { type: "object" },
|
|
379
|
+
},
|
|
380
|
+
required: ["id", "name", "state"],
|
|
381
|
+
};
|
|
382
|
+
const DOMAIN_SCHEMA = {
|
|
383
|
+
type: "object",
|
|
384
|
+
properties: {
|
|
385
|
+
name: { type: "string" },
|
|
386
|
+
apexName: { type: "string" },
|
|
387
|
+
projectId: { type: "string" },
|
|
388
|
+
verified: { type: "boolean" },
|
|
389
|
+
verification: { type: "array" },
|
|
390
|
+
redirect: { type: ["string", "null"] },
|
|
391
|
+
redirectStatusCode: { type: ["integer", "null"] },
|
|
392
|
+
gitBranch: { type: ["string", "null"] },
|
|
393
|
+
customEnvironmentId: { type: ["string", "null"] },
|
|
394
|
+
createdAt: { type: "number" },
|
|
395
|
+
updatedAt: { type: "number" },
|
|
396
|
+
},
|
|
397
|
+
required: ["name", "projectId", "verified"],
|
|
398
|
+
};
|
|
399
|
+
const ENV_SCHEMA = {
|
|
400
|
+
type: "object",
|
|
401
|
+
properties: {
|
|
402
|
+
id: { type: "string" },
|
|
403
|
+
key: { type: "string" },
|
|
404
|
+
type: { type: "string" },
|
|
405
|
+
visibility: { type: "string" },
|
|
406
|
+
target: { type: ["array", "string"], items: { type: "string" } },
|
|
407
|
+
gitBranch: { type: ["string", "null"] },
|
|
408
|
+
customEnvironmentIds: { type: "array", items: { type: "string" } },
|
|
409
|
+
comment: { type: "string" },
|
|
410
|
+
createdAt: { type: "number" },
|
|
411
|
+
updatedAt: { type: "number" },
|
|
412
|
+
},
|
|
413
|
+
required: ["id", "key", "type"],
|
|
414
|
+
};
|
|
415
|
+
function namedInput(properties, required) {
|
|
416
|
+
return { type: "object", properties, required, additionalProperties: false };
|
|
417
|
+
}
|
|
418
|
+
function queryPairs(value) {
|
|
419
|
+
const query = {};
|
|
420
|
+
for (const row of asArray(value)) {
|
|
421
|
+
const pair = asRecord(row);
|
|
422
|
+
if (typeof pair["name"] !== "string")
|
|
423
|
+
continue;
|
|
424
|
+
const item = pair["value"];
|
|
425
|
+
if (typeof item === "string" ||
|
|
426
|
+
typeof item === "number" ||
|
|
427
|
+
typeof item === "boolean") {
|
|
428
|
+
query[pair["name"]] = item;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return query;
|
|
432
|
+
}
|
|
433
|
+
const QUERY_PROPERTY = {
|
|
434
|
+
type: "array",
|
|
435
|
+
description: "Provider query parameters as name/value pairs.",
|
|
436
|
+
items: {
|
|
437
|
+
type: "object",
|
|
438
|
+
properties: {
|
|
439
|
+
name: { type: "string", minLength: 1, description: "Query parameter name." },
|
|
440
|
+
value: {
|
|
441
|
+
type: ["string", "number", "boolean"],
|
|
442
|
+
description: "Query parameter value; guarded transport stringifies it once.",
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
required: ["name", "value"],
|
|
446
|
+
additionalProperties: false,
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
const HEADERS_PROPERTY = {
|
|
450
|
+
type: "array",
|
|
451
|
+
description: "Endpoint-specific request headers. Credential, cookie, host, framing, and content-type headers are connector-owned.",
|
|
452
|
+
items: {
|
|
453
|
+
type: "object",
|
|
454
|
+
properties: {
|
|
455
|
+
name: { type: "string", minLength: 1, description: "HTTP header name." },
|
|
456
|
+
value: { type: "string", description: "HTTP header value." },
|
|
457
|
+
},
|
|
458
|
+
required: ["name", "value"],
|
|
459
|
+
additionalProperties: false,
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
const FORBIDDEN_HATCH_HEADERS = new Set([
|
|
463
|
+
"authorization",
|
|
464
|
+
"content-length",
|
|
465
|
+
"content-type",
|
|
466
|
+
"cookie",
|
|
467
|
+
"host",
|
|
468
|
+
"transfer-encoding",
|
|
469
|
+
]);
|
|
470
|
+
function headerPairs(value) {
|
|
471
|
+
const headers = {};
|
|
472
|
+
for (const row of asArray(value)) {
|
|
473
|
+
const pair = asRecord(row);
|
|
474
|
+
if (typeof pair["name"] === "string" && typeof pair["value"] === "string") {
|
|
475
|
+
const normalized = pair["name"].trim().toLowerCase();
|
|
476
|
+
if (FORBIDDEN_HATCH_HEADERS.has(normalized)) {
|
|
477
|
+
throw new ConnectorCallError("invalid_args", `A Vercel upload may not set the ${normalized} header; the connector owns credentials, cookies, origin, framing, and content type.`);
|
|
478
|
+
}
|
|
479
|
+
headers[pair["name"]] = pair["value"];
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return headers;
|
|
483
|
+
}
|
|
484
|
+
function uploadBody(args) {
|
|
485
|
+
const hasText = typeof args["textBody"] === "string";
|
|
486
|
+
const hasBase64 = typeof args["base64Body"] === "string";
|
|
487
|
+
if (hasText === hasBase64) {
|
|
488
|
+
throw new ConnectorCallError("invalid_args", "Provide exactly one of textBody or base64Body for a Vercel upload.");
|
|
489
|
+
}
|
|
490
|
+
if (hasText)
|
|
491
|
+
return args["textBody"];
|
|
492
|
+
try {
|
|
493
|
+
return Uint8Array.from(atob(args["base64Body"]), (character) => character.charCodeAt(0));
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
throw new ConnectorCallError("invalid_args", "base64Body is not valid base64.");
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function rawRequest(args, defaultTeamId) {
|
|
500
|
+
const query = queryPairs(args["query"]);
|
|
501
|
+
if (args["personalAccount"] === true &&
|
|
502
|
+
(query["teamId"] !== undefined || query["slug"] !== undefined)) {
|
|
503
|
+
throw new ConnectorCallError("invalid_args", "personalAccount cannot be combined with a teamId or slug query parameter.");
|
|
504
|
+
}
|
|
505
|
+
if (args["personalAccount"] !== true &&
|
|
506
|
+
defaultTeamId &&
|
|
507
|
+
query["teamId"] === undefined &&
|
|
508
|
+
query["slug"] === undefined) {
|
|
509
|
+
query["teamId"] = defaultTeamId;
|
|
510
|
+
}
|
|
511
|
+
return { path: String(args["path"]), query };
|
|
512
|
+
}
|
|
513
|
+
const PERSONAL_ACCOUNT_PROPERTY = {
|
|
514
|
+
type: "boolean",
|
|
515
|
+
description: "True omits the configured default team. Do not combine with a teamId or slug query parameter.",
|
|
516
|
+
};
|
|
517
|
+
function tools(send, defaultPageSize, defaultTeamId) {
|
|
518
|
+
const readOnly = { readOnlyHint: true };
|
|
519
|
+
const destructive = { readOnlyHint: false, destructiveHint: true };
|
|
520
|
+
const team = (args) => teamQuery(args, defaultTeamId);
|
|
521
|
+
const limit = (args) => args["limit"] ?? defaultPageSize;
|
|
522
|
+
return [
|
|
523
|
+
{
|
|
524
|
+
name: "vercel_api_get",
|
|
525
|
+
description: "Call any Vercel REST GET endpoint and return its untouched response. Use named reads first for smaller results and stable projections.",
|
|
526
|
+
annotations: readOnly,
|
|
527
|
+
inputSchema: namedInput({
|
|
528
|
+
path: {
|
|
529
|
+
type: "string",
|
|
530
|
+
minLength: 1,
|
|
531
|
+
description: "Path below api.vercel.com beginning with '/', including its API version. No query string.",
|
|
532
|
+
},
|
|
533
|
+
query: QUERY_PROPERTY,
|
|
534
|
+
personalAccount: PERSONAL_ACCOUNT_PROPERTY,
|
|
535
|
+
}, ["path"]),
|
|
536
|
+
outputSchema: {
|
|
537
|
+
type: "object",
|
|
538
|
+
properties: { result: { description: "Vercel's untouched response body." } },
|
|
539
|
+
required: ["result"],
|
|
540
|
+
},
|
|
541
|
+
handler: async (args, ctx) => ({
|
|
542
|
+
result: (await callVercel(send, { method: "GET", ...rawRequest(args, defaultTeamId) }, ctx)) ?? null,
|
|
543
|
+
}),
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
name: "vercel_api_mutate",
|
|
547
|
+
description: "Call any JSON Vercel REST mutation endpoint. The approval-gated hatch for API operations the named tools do not cover; no file uploads.",
|
|
548
|
+
annotations: destructive,
|
|
549
|
+
inputSchema: namedInput({
|
|
550
|
+
method: {
|
|
551
|
+
type: "string",
|
|
552
|
+
enum: ["POST", "PUT", "PATCH", "DELETE"],
|
|
553
|
+
description: "HTTP mutation method required by the Vercel endpoint.",
|
|
554
|
+
},
|
|
555
|
+
path: {
|
|
556
|
+
type: "string",
|
|
557
|
+
minLength: 1,
|
|
558
|
+
description: "Path below api.vercel.com beginning with '/', including its API version. No query string.",
|
|
559
|
+
},
|
|
560
|
+
query: QUERY_PROPERTY,
|
|
561
|
+
personalAccount: PERSONAL_ACCOUNT_PROPERTY,
|
|
562
|
+
body: {
|
|
563
|
+
type: ["object", "array", "string", "number", "boolean", "null"],
|
|
564
|
+
description: "JSON body exactly as documented by Vercel. Omit when the endpoint has no body.",
|
|
565
|
+
},
|
|
566
|
+
}, ["method", "path"]),
|
|
567
|
+
outputSchema: {
|
|
568
|
+
type: "object",
|
|
569
|
+
properties: { result: { description: "Vercel's untouched response body, or null for an empty response." } },
|
|
570
|
+
required: ["result"],
|
|
571
|
+
},
|
|
572
|
+
handler: async (args, ctx) => ({
|
|
573
|
+
result: (await callVercel(send, {
|
|
574
|
+
method: args["method"],
|
|
575
|
+
...rawRequest(args, defaultTeamId),
|
|
576
|
+
...(args["body"] !== undefined ? { body: args["body"] } : {}),
|
|
577
|
+
}, ctx)) ?? null,
|
|
578
|
+
}),
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
name: "vercel_api_upload",
|
|
582
|
+
description: "Upload explicit text or base64 bytes to a Vercel POST or PUT endpoint. Covers deployment files and other raw-body APIs; reads no local files.",
|
|
583
|
+
annotations: destructive,
|
|
584
|
+
inputSchema: namedInput({
|
|
585
|
+
method: {
|
|
586
|
+
type: "string",
|
|
587
|
+
enum: ["POST", "PUT"],
|
|
588
|
+
description: "Upload method required by the Vercel endpoint.",
|
|
589
|
+
},
|
|
590
|
+
path: {
|
|
591
|
+
type: "string",
|
|
592
|
+
minLength: 1,
|
|
593
|
+
description: "Path below api.vercel.com beginning with '/', including its API version. No query string.",
|
|
594
|
+
},
|
|
595
|
+
query: QUERY_PROPERTY,
|
|
596
|
+
personalAccount: PERSONAL_ACCOUNT_PROPERTY,
|
|
597
|
+
headers: HEADERS_PROPERTY,
|
|
598
|
+
contentType: {
|
|
599
|
+
type: "string",
|
|
600
|
+
minLength: 1,
|
|
601
|
+
description: "Content-Type for the raw body, such as application/octet-stream.",
|
|
602
|
+
},
|
|
603
|
+
textBody: {
|
|
604
|
+
type: "string",
|
|
605
|
+
description: "Raw UTF-8 body. Exclusive with base64Body.",
|
|
606
|
+
},
|
|
607
|
+
base64Body: {
|
|
608
|
+
type: "string",
|
|
609
|
+
description: "Base64-encoded bytes. Exclusive with textBody.",
|
|
610
|
+
},
|
|
611
|
+
}, ["method", "path", "contentType"]),
|
|
612
|
+
outputSchema: {
|
|
613
|
+
type: "object",
|
|
614
|
+
properties: {
|
|
615
|
+
result: {
|
|
616
|
+
description: "Vercel's untouched upload response body, or null for an empty response.",
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
required: ["result"],
|
|
620
|
+
},
|
|
621
|
+
handler: async (args, ctx) => ({
|
|
622
|
+
result: (await callVercel(send, {
|
|
623
|
+
method: args["method"],
|
|
624
|
+
...rawRequest(args, defaultTeamId),
|
|
625
|
+
headers: {
|
|
626
|
+
...headerPairs(args["headers"]),
|
|
627
|
+
"Content-Type": args["contentType"],
|
|
628
|
+
},
|
|
629
|
+
rawBody: uploadBody(args),
|
|
630
|
+
}, ctx)) ?? null,
|
|
631
|
+
}),
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
name: "list_teams",
|
|
635
|
+
description: "List teams the access token can reach. Supplies teamId for project, deployment, domain, environment, and raw API calls.",
|
|
636
|
+
annotations: readOnly,
|
|
637
|
+
inputSchema: namedInput({ limit: limitProperty(defaultPageSize), cursor: CURSOR_PROPERTY }, []),
|
|
638
|
+
outputSchema: listSchema("teams", TEAM_SCHEMA),
|
|
639
|
+
handler: async (args, ctx) => {
|
|
640
|
+
const payload = await callVercel(send, { method: "GET", path: "/v2/teams", query: { limit: limit(args), until: args["cursor"] } }, ctx);
|
|
641
|
+
return { teams: asArray(asRecord(payload)["teams"]).map(projectTeam), page: page(payload) };
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
name: "list_projects",
|
|
646
|
+
description: "List or search Vercel projects with repository, framework, and production-deployment identity. Returns lean project summaries.",
|
|
647
|
+
annotations: readOnly,
|
|
648
|
+
inputSchema: namedInput({
|
|
649
|
+
teamId: TEAM_ID_PROPERTY,
|
|
650
|
+
search: { type: "string", description: "Case-insensitive project-name search." },
|
|
651
|
+
limit: limitProperty(defaultPageSize),
|
|
652
|
+
cursor: CURSOR_PROPERTY,
|
|
653
|
+
raw: RAW_PROPERTY,
|
|
654
|
+
}, []),
|
|
655
|
+
outputSchema: listSchema("projects", PROJECT_SCHEMA),
|
|
656
|
+
handler: async (args, ctx) => {
|
|
657
|
+
const payload = await callVercel(send, {
|
|
658
|
+
method: "GET",
|
|
659
|
+
path: "/v10/projects",
|
|
660
|
+
query: { ...team(args), search: args["search"], limit: limit(args), from: args["cursor"] },
|
|
661
|
+
}, ctx);
|
|
662
|
+
const projects = asArray(asRecord(payload)["projects"]);
|
|
663
|
+
return {
|
|
664
|
+
projects: args["raw"] === true ? projects : projects.map(projectProject),
|
|
665
|
+
page: page(payload),
|
|
666
|
+
};
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
name: "get_project",
|
|
671
|
+
description: "Get one Vercel project by id or name, including build settings, Git identity, and current production deployment.",
|
|
672
|
+
annotations: readOnly,
|
|
673
|
+
inputSchema: namedInput({ projectId: PROJECT_ID_PROPERTY, teamId: TEAM_ID_PROPERTY, raw: RAW_PROPERTY }, ["projectId"]),
|
|
674
|
+
outputSchema: PROJECT_SCHEMA,
|
|
675
|
+
handler: async (args, ctx) => {
|
|
676
|
+
const payload = await callVercel(send, { method: "GET", path: `/v9/projects/${encodeURIComponent(args["projectId"])}`, query: team(args) }, ctx);
|
|
677
|
+
return args["raw"] === true ? payload : projectProject(payload);
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
name: "list_deployments",
|
|
682
|
+
description: "List Vercel deployments, filtered by project, target, state, branch, or commit SHA. Returns ids needed by log and lifecycle tools.",
|
|
683
|
+
annotations: readOnly,
|
|
684
|
+
inputSchema: namedInput({
|
|
685
|
+
teamId: TEAM_ID_PROPERTY,
|
|
686
|
+
projectId: { ...PROJECT_ID_PROPERTY, description: "Project id or name. Omit to list the whole account or team." },
|
|
687
|
+
target: { type: "string", description: "Deployment target, usually production or preview." },
|
|
688
|
+
state: {
|
|
689
|
+
type: "string",
|
|
690
|
+
enum: ["BUILDING", "ERROR", "INITIALIZING", "QUEUED", "READY", "CANCELED", "BLOCKED"],
|
|
691
|
+
description: "Exact Vercel deployment state.",
|
|
692
|
+
},
|
|
693
|
+
branch: { type: "string", description: "Git branch name." },
|
|
694
|
+
sha: { type: "string", description: "Git commit SHA." },
|
|
695
|
+
limit: limitProperty(defaultPageSize),
|
|
696
|
+
cursor: CURSOR_PROPERTY,
|
|
697
|
+
raw: RAW_PROPERTY,
|
|
698
|
+
}, []),
|
|
699
|
+
outputSchema: listSchema("deployments", DEPLOYMENT_SCHEMA),
|
|
700
|
+
handler: async (args, ctx) => {
|
|
701
|
+
const payload = await callVercel(send, {
|
|
702
|
+
method: "GET",
|
|
703
|
+
path: "/v7/deployments",
|
|
704
|
+
query: {
|
|
705
|
+
...team(args), projectId: args["projectId"], target: args["target"],
|
|
706
|
+
state: args["state"], branch: args["branch"], sha: args["sha"],
|
|
707
|
+
limit: limit(args), until: args["cursor"],
|
|
708
|
+
},
|
|
709
|
+
}, ctx);
|
|
710
|
+
const deployments = asArray(asRecord(payload)["deployments"]);
|
|
711
|
+
return {
|
|
712
|
+
deployments: args["raw"] === true
|
|
713
|
+
? deployments
|
|
714
|
+
: deployments.map(projectDeployment),
|
|
715
|
+
page: page(payload),
|
|
716
|
+
};
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
name: "get_deployment",
|
|
721
|
+
description: "Get one Vercel deployment by id or hostname, including its state, target, creator, Git commit, and timestamps.",
|
|
722
|
+
annotations: readOnly,
|
|
723
|
+
inputSchema: namedInput({
|
|
724
|
+
deploymentId: { ...DEPLOYMENT_ID_PROPERTY, description: "Deployment id or deployment hostname." },
|
|
725
|
+
teamId: TEAM_ID_PROPERTY,
|
|
726
|
+
raw: RAW_PROPERTY,
|
|
727
|
+
}, ["deploymentId"]),
|
|
728
|
+
outputSchema: DEPLOYMENT_SCHEMA,
|
|
729
|
+
handler: async (args, ctx) => {
|
|
730
|
+
const payload = await callVercel(send, { method: "GET", path: `/v13/deployments/${encodeURIComponent(args["deploymentId"])}`, query: team(args) }, ctx);
|
|
731
|
+
return args["raw"] === true ? payload : projectDeployment(payload);
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
name: "get_build_logs",
|
|
736
|
+
description: "Get bounded build events for one deployment, including stdout, stderr, command, exit, and deployment-state records. Does not follow live output.",
|
|
737
|
+
annotations: readOnly,
|
|
738
|
+
inputSchema: namedInput({
|
|
739
|
+
deploymentId: DEPLOYMENT_ID_PROPERTY,
|
|
740
|
+
teamId: TEAM_ID_PROPERTY,
|
|
741
|
+
direction: { type: "string", enum: ["forward", "backward"], description: "Chronological direction. Defaults to forward." },
|
|
742
|
+
limit: { type: "integer", minimum: 1, maximum: 1000, description: "Events per request, 1 to this connector's 1,000-event cap. Defaults to 100." },
|
|
743
|
+
since: { type: "number", description: "Only events at or after this JavaScript timestamp." },
|
|
744
|
+
until: { type: "number", description: "Only events at or before this JavaScript timestamp." },
|
|
745
|
+
raw: RAW_PROPERTY,
|
|
746
|
+
}, ["deploymentId"]),
|
|
747
|
+
outputSchema: {
|
|
748
|
+
type: "object",
|
|
749
|
+
properties: {
|
|
750
|
+
events: {
|
|
751
|
+
type: "array",
|
|
752
|
+
items: {
|
|
753
|
+
type: "object",
|
|
754
|
+
properties: {
|
|
755
|
+
type: { type: "string" }, createdAt: { type: "number" },
|
|
756
|
+
message: { type: "string" }, payload: { type: "object" },
|
|
757
|
+
},
|
|
758
|
+
required: ["type", "createdAt"],
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
required: ["events"],
|
|
763
|
+
},
|
|
764
|
+
handler: async (args, ctx) => {
|
|
765
|
+
const payload = await callVercel(send, {
|
|
766
|
+
method: "GET",
|
|
767
|
+
path: `/v3/deployments/${encodeURIComponent(args["deploymentId"])}/events`,
|
|
768
|
+
query: {
|
|
769
|
+
...team(args), direction: args["direction"] ?? "forward", follow: 0,
|
|
770
|
+
builds: 1, limit: args["limit"] ?? 100, since: args["since"], until: args["until"],
|
|
771
|
+
},
|
|
772
|
+
}, ctx);
|
|
773
|
+
if (args["raw"] === true)
|
|
774
|
+
return { events: asArray(payload) };
|
|
775
|
+
const events = asArray(payload).map((value) => {
|
|
776
|
+
const event = asRecord(value);
|
|
777
|
+
const eventPayload = asRecord(event["payload"]);
|
|
778
|
+
return compact({
|
|
779
|
+
type: event["type"], createdAt: event["created"] ?? event["date"],
|
|
780
|
+
message: eventPayload["text"] ?? eventPayload["message"],
|
|
781
|
+
payload: Object.keys(eventPayload).length === 0 ? undefined : eventPayload,
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
return { events };
|
|
785
|
+
},
|
|
786
|
+
},
|
|
787
|
+
{
|
|
788
|
+
name: "get_runtime_logs",
|
|
789
|
+
description: "Get a bounded runtime-log snapshot for one deployment. Returns at most 500 rows and stops a stream that stays open past 10 seconds.",
|
|
790
|
+
annotations: readOnly,
|
|
791
|
+
inputSchema: namedInput({
|
|
792
|
+
projectId: PROJECT_ID_PROPERTY,
|
|
793
|
+
deploymentId: DEPLOYMENT_ID_PROPERTY,
|
|
794
|
+
teamId: TEAM_ID_PROPERTY,
|
|
795
|
+
limit: {
|
|
796
|
+
type: "integer",
|
|
797
|
+
minimum: 1,
|
|
798
|
+
maximum: MAX_RUNTIME_LOG_ROWS,
|
|
799
|
+
description: `Rows returned, 1 to ${MAX_RUNTIME_LOG_ROWS}. Defaults to ${DEFAULT_RUNTIME_LOG_ROWS}.`,
|
|
800
|
+
},
|
|
801
|
+
}, ["projectId", "deploymentId"]),
|
|
802
|
+
outputSchema: {
|
|
803
|
+
type: "object",
|
|
804
|
+
properties: {
|
|
805
|
+
logs: {
|
|
806
|
+
type: "array",
|
|
807
|
+
items: {
|
|
808
|
+
type: "object",
|
|
809
|
+
properties: {
|
|
810
|
+
level: { type: "string" }, message: { type: "string" },
|
|
811
|
+
timestampInMs: { type: "number" }, source: { type: "string" },
|
|
812
|
+
domain: { type: "string" }, requestMethod: { type: "string" },
|
|
813
|
+
requestPath: { type: "string" }, responseStatusCode: { type: "number" },
|
|
814
|
+
messageTruncated: { type: "boolean" },
|
|
815
|
+
},
|
|
816
|
+
required: ["level", "message", "timestampInMs", "source"],
|
|
817
|
+
},
|
|
818
|
+
},
|
|
819
|
+
},
|
|
820
|
+
required: ["logs"],
|
|
821
|
+
},
|
|
822
|
+
handler: async (args, ctx) => {
|
|
823
|
+
const payload = await withDeadline((signal) => callVercel(send, {
|
|
824
|
+
method: "GET",
|
|
825
|
+
path: `/v1/projects/${encodeURIComponent(args["projectId"])}/deployments/${encodeURIComponent(args["deploymentId"])}/runtime-logs`,
|
|
826
|
+
query: team(args), headers: { Accept: "application/stream+json" },
|
|
827
|
+
}, { ...ctx, signal }, parseStreamRows), {
|
|
828
|
+
timeoutMs: RUNTIME_LOG_TIMEOUT_MS,
|
|
829
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
830
|
+
timeoutError: new ConnectorCallError("unavailable", `Vercel's runtime-log stream stayed open past this connector's ${RUNTIME_LOG_TIMEOUT_MS / 1_000}-second bound. Retry for a fresh snapshot.`),
|
|
831
|
+
});
|
|
832
|
+
const requested = args["limit"] ?? DEFAULT_RUNTIME_LOG_ROWS;
|
|
833
|
+
return { logs: asArray(payload).slice(0, requested) };
|
|
834
|
+
},
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
name: "list_project_domains",
|
|
838
|
+
description: "List domains assigned to one Vercel project, including verification challenges, redirects, branch bindings, and custom-environment bindings.",
|
|
839
|
+
annotations: readOnly,
|
|
840
|
+
inputSchema: namedInput({
|
|
841
|
+
projectId: PROJECT_ID_PROPERTY, teamId: TEAM_ID_PROPERTY,
|
|
842
|
+
verified: { type: "boolean", description: "Filter by verification state." },
|
|
843
|
+
limit: limitProperty(defaultPageSize), cursor: CURSOR_PROPERTY, raw: RAW_PROPERTY,
|
|
844
|
+
}, ["projectId"]),
|
|
845
|
+
outputSchema: listSchema("domains", DOMAIN_SCHEMA),
|
|
846
|
+
handler: async (args, ctx) => {
|
|
847
|
+
const payload = await callVercel(send, {
|
|
848
|
+
method: "GET", path: `/v9/projects/${encodeURIComponent(args["projectId"])}/domains`,
|
|
849
|
+
query: { ...team(args), verified: args["verified"], limit: limit(args), until: args["cursor"] },
|
|
850
|
+
}, ctx);
|
|
851
|
+
const domains = asArray(asRecord(payload)["domains"]);
|
|
852
|
+
return {
|
|
853
|
+
domains: args["raw"] === true ? domains : domains.map(projectDomain),
|
|
854
|
+
page: page(payload),
|
|
855
|
+
};
|
|
856
|
+
},
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
name: "add_project_domain",
|
|
860
|
+
description: "Add a domain, redirect, Git-branch domain, or custom-environment domain to a Vercel project. An unverified result includes its DNS challenge.",
|
|
861
|
+
annotations: destructive,
|
|
862
|
+
inputSchema: namedInput({
|
|
863
|
+
projectId: PROJECT_ID_PROPERTY, teamId: TEAM_ID_PROPERTY,
|
|
864
|
+
domain: { type: "string", minLength: 1, description: "Domain name to add." },
|
|
865
|
+
gitBranch: { type: "string", description: "Bind this domain to one Git branch." },
|
|
866
|
+
customEnvironmentId: { type: "string", description: "Bind this domain to one custom environment." },
|
|
867
|
+
redirect: { type: "string", description: "Target domain for a redirect." },
|
|
868
|
+
redirectStatusCode: { type: "integer", enum: [301, 302, 307, 308], description: "Redirect status; only valid with redirect." },
|
|
869
|
+
}, ["projectId", "domain"]),
|
|
870
|
+
outputSchema: DOMAIN_SCHEMA,
|
|
871
|
+
handler: async (args, ctx) => projectDomain(await callVercel(send, {
|
|
872
|
+
method: "POST", path: `/v10/projects/${encodeURIComponent(args["projectId"])}/domains`, query: team(args),
|
|
873
|
+
body: compact({ name: args["domain"], gitBranch: args["gitBranch"], customEnvironmentId: args["customEnvironmentId"], redirect: args["redirect"], redirectStatusCode: args["redirectStatusCode"] }),
|
|
874
|
+
}, ctx)),
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
name: "verify_project_domain",
|
|
878
|
+
description: "Ask Vercel to verify a project's pending domain after its DNS challenge has been completed. Returns the current domain state.",
|
|
879
|
+
annotations: destructive,
|
|
880
|
+
inputSchema: namedInput({ projectId: PROJECT_ID_PROPERTY, domain: { type: "string", minLength: 1, description: "Pending domain name from list_project_domains." }, teamId: TEAM_ID_PROPERTY }, ["projectId", "domain"]),
|
|
881
|
+
outputSchema: DOMAIN_SCHEMA,
|
|
882
|
+
handler: async (args, ctx) => projectDomain(await callVercel(send, { method: "POST", path: `/v9/projects/${encodeURIComponent(args["projectId"])}/domains/${encodeURIComponent(args["domain"])}/verify`, query: team(args) }, ctx)),
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
name: "remove_project_domain",
|
|
886
|
+
description: "Remove a domain from one Vercel project. Optionally remove project domains that redirect to it; this does not delete the account-level domain.",
|
|
887
|
+
annotations: destructive,
|
|
888
|
+
inputSchema: namedInput({
|
|
889
|
+
projectId: PROJECT_ID_PROPERTY,
|
|
890
|
+
domain: { type: "string", minLength: 1, description: "Project domain name from list_project_domains." },
|
|
891
|
+
removeRedirects: { type: "boolean", description: "Also remove project domains that redirect to this one." },
|
|
892
|
+
teamId: TEAM_ID_PROPERTY,
|
|
893
|
+
}, ["projectId", "domain"]),
|
|
894
|
+
outputSchema: {
|
|
895
|
+
type: "object", properties: { removed: { type: "boolean" }, domain: { type: "string" } }, required: ["removed", "domain"],
|
|
896
|
+
},
|
|
897
|
+
handler: async (args, ctx) => {
|
|
898
|
+
await callVercel(send, {
|
|
899
|
+
method: "DELETE", path: `/v9/projects/${encodeURIComponent(args["projectId"])}/domains/${encodeURIComponent(args["domain"])}`,
|
|
900
|
+
query: team(args), body: args["removeRedirects"] === undefined ? undefined : { removeRedirects: args["removeRedirects"] },
|
|
901
|
+
}, ctx);
|
|
902
|
+
return { removed: true, domain: args["domain"] };
|
|
903
|
+
},
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
name: "list_project_env_vars",
|
|
907
|
+
description: "List a project's environment-variable metadata without decrypting or returning values. Includes targets, visibility, branches, and custom environments.",
|
|
908
|
+
annotations: readOnly,
|
|
909
|
+
inputSchema: namedInput({
|
|
910
|
+
projectId: PROJECT_ID_PROPERTY, teamId: TEAM_ID_PROPERTY,
|
|
911
|
+
gitBranch: { type: "string", description: "Preview branch filter." },
|
|
912
|
+
customEnvironmentId: { type: "string", description: "Custom environment filter." },
|
|
913
|
+
}, ["projectId"]),
|
|
914
|
+
outputSchema: {
|
|
915
|
+
type: "object", properties: { variables: { type: "array", items: ENV_SCHEMA } }, required: ["variables"],
|
|
916
|
+
},
|
|
917
|
+
handler: async (args, ctx) => {
|
|
918
|
+
const payload = await callVercel(send, {
|
|
919
|
+
method: "GET", path: `/v10/projects/${encodeURIComponent(args["projectId"])}/env`,
|
|
920
|
+
query: { ...team(args), gitBranch: args["gitBranch"], customEnvironmentId: args["customEnvironmentId"], decrypt: "false" },
|
|
921
|
+
}, ctx);
|
|
922
|
+
return { variables: asArray(asRecord(payload)["envs"]).map(projectEnvironmentVariable) };
|
|
923
|
+
},
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
name: "upsert_project_env_var",
|
|
927
|
+
description: "Create or replace one Vercel project environment variable. Changes affect only future deployments; trigger a new deployment separately.",
|
|
928
|
+
annotations: destructive,
|
|
929
|
+
inputSchema: namedInput({
|
|
930
|
+
projectId: PROJECT_ID_PROPERTY, teamId: TEAM_ID_PROPERTY,
|
|
931
|
+
key: { type: "string", minLength: 1, maxLength: 256, description: "Environment variable name." },
|
|
932
|
+
value: { type: "string", maxLength: 65536, description: "New value. Vercel's total project-environment payload is capped at 64 KB." },
|
|
933
|
+
type: { type: "string", enum: ["plain", "encrypted", "sensitive"], description: "Storage type. Sensitive values cannot be read back." },
|
|
934
|
+
targets: { type: "array", minItems: 1, uniqueItems: true, items: { type: "string", enum: ["production", "preview", "development"] }, description: "Default Vercel environments that receive this value." },
|
|
935
|
+
gitBranch: { type: "string", description: "Optional preview-only Git branch." },
|
|
936
|
+
customEnvironmentIds: { type: "array", uniqueItems: true, items: { type: "string", minLength: 1 }, description: "Custom environment ids that receive this value." },
|
|
937
|
+
comment: { type: "string", maxLength: 500, description: "Operator-facing note explaining the variable." },
|
|
938
|
+
}, ["projectId", "key", "value", "type", "targets"]),
|
|
939
|
+
outputSchema: ENV_SCHEMA,
|
|
940
|
+
handler: async (args, ctx) => {
|
|
941
|
+
const payload = asRecord(await callVercel(send, {
|
|
942
|
+
method: "POST", path: `/v10/projects/${encodeURIComponent(args["projectId"])}/env`,
|
|
943
|
+
query: { ...team(args), upsert: "true" },
|
|
944
|
+
body: compact({ key: args["key"], value: args["value"], type: args["type"], target: args["targets"], gitBranch: args["gitBranch"], customEnvironmentIds: args["customEnvironmentIds"], comment: args["comment"] }),
|
|
945
|
+
}, ctx));
|
|
946
|
+
const failed = asArray(payload["failed"]);
|
|
947
|
+
if (failed.length > 0) {
|
|
948
|
+
const error = asRecord(asRecord(failed[0])["error"]);
|
|
949
|
+
const code = typeof error["code"] === "string" ? `${error["code"]}: ` : "";
|
|
950
|
+
const message = typeof error["message"] === "string"
|
|
951
|
+
? error["message"]
|
|
952
|
+
: "Vercel rejected the environment-variable write.";
|
|
953
|
+
throw new ConnectorCallError("invalid_args", `Vercel ${code}${message}`);
|
|
954
|
+
}
|
|
955
|
+
const created = Array.isArray(payload["created"])
|
|
956
|
+
? payload["created"][0]
|
|
957
|
+
: payload["created"];
|
|
958
|
+
const result = projectEnvironmentVariable(created ?? payload);
|
|
959
|
+
if (!result["id"] || !result["key"] || !result["type"]) {
|
|
960
|
+
throw new ConnectorCallError("connector_call_failed", "Vercel accepted the environment-variable write without returning the created variable.", { retryable: false });
|
|
961
|
+
}
|
|
962
|
+
return result;
|
|
963
|
+
},
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
name: "update_project_env_var",
|
|
967
|
+
description: "Update one Vercel project environment variable by id. Send only fields that should change; deployments keep their previous values.",
|
|
968
|
+
annotations: destructive,
|
|
969
|
+
inputSchema: namedInput({
|
|
970
|
+
projectId: PROJECT_ID_PROPERTY, teamId: TEAM_ID_PROPERTY,
|
|
971
|
+
envVarId: { type: "string", minLength: 1, description: "Environment-variable id from list_project_env_vars." },
|
|
972
|
+
key: { type: "string", minLength: 1, maxLength: 256, description: "Replacement variable name." },
|
|
973
|
+
value: { type: "string", maxLength: 65536, description: "Replacement value." },
|
|
974
|
+
type: { type: "string", enum: ["plain", "encrypted", "sensitive"], description: "Replacement storage type." },
|
|
975
|
+
targets: { type: "array", minItems: 1, uniqueItems: true, items: { type: "string", enum: ["production", "preview", "development"] }, description: "Replacement default environments." },
|
|
976
|
+
gitBranch: { type: ["string", "null"], description: "Replacement preview branch, or null to clear it." },
|
|
977
|
+
customEnvironmentIds: { type: "array", uniqueItems: true, items: { type: "string", minLength: 1 }, description: "Replacement custom environment ids." },
|
|
978
|
+
comment: { type: "string", maxLength: 500, description: "Replacement operator-facing note." },
|
|
979
|
+
}, ["projectId", "envVarId"]),
|
|
980
|
+
outputSchema: ENV_SCHEMA,
|
|
981
|
+
handler: async (args, ctx) => {
|
|
982
|
+
const body = compact({ key: args["key"], value: args["value"], type: args["type"], target: args["targets"], gitBranch: args["gitBranch"], customEnvironmentIds: args["customEnvironmentIds"], comment: args["comment"] });
|
|
983
|
+
if (Object.keys(body).length === 0) {
|
|
984
|
+
throw new ConnectorCallError("invalid_args", "Nothing to update: provide key, value, type, targets, gitBranch, customEnvironmentIds, or comment.");
|
|
985
|
+
}
|
|
986
|
+
return projectEnvironmentVariable(await callVercel(send, { method: "PATCH", path: `/v9/projects/${encodeURIComponent(args["projectId"])}/env/${encodeURIComponent(args["envVarId"])}`, query: team(args), body }, ctx));
|
|
987
|
+
},
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
name: "delete_project_env_var",
|
|
991
|
+
description: "Delete one environment variable from a Vercel project by id. Existing deployments keep their embedded value; future deployments do not.",
|
|
992
|
+
annotations: destructive,
|
|
993
|
+
inputSchema: namedInput({ projectId: PROJECT_ID_PROPERTY, envVarId: { type: "string", minLength: 1, description: "Environment-variable id from list_project_env_vars." }, teamId: TEAM_ID_PROPERTY }, ["projectId", "envVarId"]),
|
|
994
|
+
outputSchema: { type: "object", properties: { deleted: { type: "boolean" }, envVarId: { type: "string" } }, required: ["deleted", "envVarId"] },
|
|
995
|
+
handler: async (args, ctx) => {
|
|
996
|
+
await callVercel(send, { method: "DELETE", path: `/v9/projects/${encodeURIComponent(args["projectId"])}/env/${encodeURIComponent(args["envVarId"])}`, query: team(args) }, ctx);
|
|
997
|
+
return { deleted: true, envVarId: args["envVarId"] };
|
|
998
|
+
},
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
name: "promote_deployment",
|
|
1002
|
+
description: "Promote an existing Vercel deployment to production without rebuilding it. The deployment must belong to the named project.",
|
|
1003
|
+
annotations: destructive,
|
|
1004
|
+
inputSchema: namedInput({ projectId: PROJECT_ID_PROPERTY, deploymentId: DEPLOYMENT_ID_PROPERTY, teamId: TEAM_ID_PROPERTY }, ["projectId", "deploymentId"]),
|
|
1005
|
+
outputSchema: { type: "object", properties: { promoted: { type: "boolean" }, deploymentId: { type: "string" } }, required: ["promoted", "deploymentId"] },
|
|
1006
|
+
handler: async (args, ctx) => {
|
|
1007
|
+
await callVercel(send, { method: "POST", path: `/v10/projects/${encodeURIComponent(args["projectId"])}/promote/${encodeURIComponent(args["deploymentId"])}`, query: team(args) }, ctx);
|
|
1008
|
+
return { promoted: true, deploymentId: args["deploymentId"] };
|
|
1009
|
+
},
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
name: "cancel_deployment",
|
|
1013
|
+
description: "Cancel a queued, initializing, or building Vercel deployment. A deployment that is already ready, failed, canceled, or deleted cannot be canceled.",
|
|
1014
|
+
annotations: destructive,
|
|
1015
|
+
inputSchema: namedInput({ deploymentId: DEPLOYMENT_ID_PROPERTY, teamId: TEAM_ID_PROPERTY }, ["deploymentId"]),
|
|
1016
|
+
outputSchema: DEPLOYMENT_SCHEMA,
|
|
1017
|
+
handler: async (args, ctx) => projectDeployment(await callVercel(send, { method: "PATCH", path: `/v12/deployments/${encodeURIComponent(args["deploymentId"])}/cancel`, query: team(args) }, ctx)),
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
name: "delete_deployment",
|
|
1021
|
+
description: "Permanently delete one Vercel deployment and its deployment URL. This cannot be undone; use cancel_deployment for work still running.",
|
|
1022
|
+
annotations: destructive,
|
|
1023
|
+
inputSchema: namedInput({ deploymentId: DEPLOYMENT_ID_PROPERTY, teamId: TEAM_ID_PROPERTY }, ["deploymentId"]),
|
|
1024
|
+
outputSchema: { type: "object", properties: { deleted: { type: "boolean" }, deploymentId: { type: "string" } }, required: ["deleted", "deploymentId"] },
|
|
1025
|
+
handler: async (args, ctx) => {
|
|
1026
|
+
await callVercel(send, { method: "DELETE", path: `/v13/deployments/${encodeURIComponent(args["deploymentId"])}`, query: team(args) }, ctx);
|
|
1027
|
+
return { deleted: true, deploymentId: args["deploymentId"] };
|
|
1028
|
+
},
|
|
1029
|
+
},
|
|
1030
|
+
];
|
|
1031
|
+
}
|
|
1032
|
+
function usageGuide(purpose, teamId, instructions) {
|
|
1033
|
+
const accountInstructions = instructions?.trim();
|
|
1034
|
+
return `# Vercel usage
|
|
1035
|
+
|
|
1036
|
+
Account purpose: ${purpose}
|
|
1037
|
+
|
|
1038
|
+
## Scope before action
|
|
1039
|
+
|
|
1040
|
+
${teamId
|
|
1041
|
+
? `This connection defaults to team \`${teamId}\`. Every named account-scoped tool accepts a \`teamId\` override; pass \`null\` to target the token owner's personal account.`
|
|
1042
|
+
: "This connection defaults to the token owner's personal account. Call `list_teams`, then pass `teamId`, for team-owned resources."}
|
|
1043
|
+
|
|
1044
|
+
Project names are accepted where Vercel accepts an id or name, but deployment,
|
|
1045
|
+
environment-variable, and team ids are opaque. Read them from their list tool
|
|
1046
|
+
and pass them back unchanged.
|
|
1047
|
+
|
|
1048
|
+
## Diagnose deployments in order
|
|
1049
|
+
|
|
1050
|
+
- Read \`get_deployment\` first. Its state says whether logs can still change.
|
|
1051
|
+
- Use \`get_build_logs\` for install, build, and framework output.
|
|
1052
|
+
- Use \`get_runtime_logs\` for application requests after a deployment runs.
|
|
1053
|
+
- \`promote_deployment\` moves an existing build to production. It does not
|
|
1054
|
+
rebuild it. A rebuild or Git deployment belongs in \`vercel_api_mutate\`.
|
|
1055
|
+
|
|
1056
|
+
## Environment values
|
|
1057
|
+
|
|
1058
|
+
\`list_project_env_vars\` never decrypts or returns values. It reports names,
|
|
1059
|
+
targets, visibility, branch bindings, and ids. The create and update tools take
|
|
1060
|
+
values only as write input, and their projected results omit them. Environment
|
|
1061
|
+
changes apply to future deployments, not deployments that already exist.
|
|
1062
|
+
|
|
1063
|
+
## Named tools and the REST hatches
|
|
1064
|
+
|
|
1065
|
+
Use named tools when one exists. They validate arguments and return smaller,
|
|
1066
|
+
stable objects. \`vercel_api_get\` reaches every other GET endpoint and
|
|
1067
|
+
\`vercel_api_mutate\` reaches JSON POST, PUT, PATCH, and DELETE endpoints.
|
|
1068
|
+
\`vercel_api_upload\` sends explicit text or base64 bytes and never reads a
|
|
1069
|
+
local file. Paths include Vercel's API version, such as \`/v1/edge-config\`,
|
|
1070
|
+
and query parameters are name/value pairs. Pass \`personalAccount: true\` to
|
|
1071
|
+
omit this connection's default team. No hatch accepts an absolute URL.
|
|
1072
|
+
|
|
1073
|
+
## Pagination and rate limits
|
|
1074
|
+
|
|
1075
|
+
List tools return \`page.hasMore\` and \`page.nextCursor\`. Pass the cursor back
|
|
1076
|
+
unchanged. Vercel meters endpoints separately and returns the reset in response
|
|
1077
|
+
headers. A rate-limit failure carries that delay when Vercel supplies it.
|
|
1078
|
+
${accountInstructions
|
|
1079
|
+
? `\n## Account instructions\n\n${accountInstructions}\n`
|
|
1080
|
+
: ""}`;
|
|
1081
|
+
}
|
|
1082
|
+
/** A maintained Vercel connection over the public REST API. */
|
|
1083
|
+
export function vercel(id, options) {
|
|
1084
|
+
const purpose = options.purpose.trim();
|
|
1085
|
+
if (!purpose) {
|
|
1086
|
+
throw new Error("vercel() requires a non-empty account purpose.");
|
|
1087
|
+
}
|
|
1088
|
+
const defaultPageSize = options.defaultPageSize ?? DEFAULT_PAGE_SIZE;
|
|
1089
|
+
if (!Number.isInteger(defaultPageSize) ||
|
|
1090
|
+
defaultPageSize < 1 ||
|
|
1091
|
+
defaultPageSize > MAX_PAGE_SIZE) {
|
|
1092
|
+
throw new Error(`vercel() defaultPageSize must be a whole number between 1 and ${MAX_PAGE_SIZE}.`);
|
|
1093
|
+
}
|
|
1094
|
+
const teamId = options.teamId?.trim() || undefined;
|
|
1095
|
+
const send = vercelTransport(options.baseUrl ?? VERCEL_API_BASE_URL);
|
|
1096
|
+
return api(id, {
|
|
1097
|
+
...(options.authScope ? { authScope: options.authScope } : {}),
|
|
1098
|
+
title: options.title ?? "Vercel",
|
|
1099
|
+
description: `Vercel account and deployments: ${purpose}`,
|
|
1100
|
+
credential: {
|
|
1101
|
+
label: "Vercel access token",
|
|
1102
|
+
description: "Access token from Vercel Account Settings → Tokens. Choose the personal account or team scope this deployment needs and set an expiration date. The connector never sends it anywhere except api.vercel.com or the configured baseUrl proxy.",
|
|
1103
|
+
placeholder: "Paste Vercel access token",
|
|
1104
|
+
},
|
|
1105
|
+
testCredential: async (value, ctx) => {
|
|
1106
|
+
try {
|
|
1107
|
+
const payload = asRecord(await callVercel(send, { method: "GET", path: "/v2/user" }, { ...ctx, credential: { get: async () => value, getAll: async () => ({ value }) } }));
|
|
1108
|
+
const user = asRecord(payload["user"] ?? payload);
|
|
1109
|
+
const identity = user["username"] ?? user["email"] ?? user["name"] ?? user["id"] ?? "Vercel user";
|
|
1110
|
+
return { ok: true, message: `Authenticated as ${identity}.` };
|
|
1111
|
+
}
|
|
1112
|
+
catch (error) {
|
|
1113
|
+
return {
|
|
1114
|
+
ok: false,
|
|
1115
|
+
message: error instanceof ConnectorCallError ? error.message : "Vercel rejected the token.",
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
},
|
|
1119
|
+
usageGuide: {
|
|
1120
|
+
content: usageGuide(purpose, teamId, options.instructions),
|
|
1121
|
+
summary: "Team scoping, deployment diagnosis, value-safe environment variables, REST hatches, and cursor pagination.",
|
|
1122
|
+
required: true,
|
|
1123
|
+
},
|
|
1124
|
+
...(options.callAdmission
|
|
1125
|
+
? { callAdmission: options.callAdmission }
|
|
1126
|
+
: {}),
|
|
1127
|
+
tools: tools(send, defaultPageSize, teamId),
|
|
1128
|
+
...(options.maxResultBytes !== undefined
|
|
1129
|
+
? { maxResultBytes: options.maxResultBytes }
|
|
1130
|
+
: {}),
|
|
1131
|
+
});
|
|
1132
|
+
}
|