@zackbart/connecta 0.29.0 → 0.31.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.
Files changed (116) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +126 -134
  3. package/bin/connecta.mjs +13 -2
  4. package/bin/version.mjs +1 -1
  5. package/dist/argument-redaction.js +230 -0
  6. package/dist/auth-elicitation.js +14 -9
  7. package/dist/authorization-handoff.js +111 -0
  8. package/dist/call-recovery.js +25 -0
  9. package/dist/catalog-cache.js +2 -2
  10. package/dist/catalog-service.js +103 -63
  11. package/dist/config-schema.d.ts +14 -7
  12. package/dist/config-schema.js +29 -10
  13. package/dist/config.d.ts +2 -0
  14. package/dist/config.js +12 -7
  15. package/dist/connectors/guarded-fetch.d.ts +9 -1
  16. package/dist/connectors/guarded-fetch.js +17 -5
  17. package/dist/connectors/negotiation-cache.js +1 -1
  18. package/dist/downstream-input-context.js +9 -0
  19. package/dist/downstream-input.js +13 -11
  20. package/dist/errors.d.ts +38 -12
  21. package/dist/errors.js +19 -9
  22. package/dist/execute.js +375 -43
  23. package/dist/executor-result.js +11 -9
  24. package/dist/executors/quickjs-child.js +12 -5
  25. package/dist/executors/quickjs-protocol.js +5 -6
  26. package/dist/executors/quickjs-runtime.js +6 -1
  27. package/dist/executors/quickjs.js +20 -4
  28. package/dist/guest-runtime.js +52 -2
  29. package/dist/guest-types.d.ts +47 -5
  30. package/dist/index.d.ts +1 -1
  31. package/dist/invocation.js +72 -14
  32. package/dist/meta-output.js +0 -15
  33. package/dist/meta-tools.js +166 -95
  34. package/dist/oauth-handoff.js +88 -11
  35. package/dist/operator-ui/generated.js +2 -2
  36. package/dist/operator-ui/view.d.ts +9 -0
  37. package/dist/operator-ui/view.js +11 -0
  38. package/dist/program-result.js +5 -0
  39. package/dist/program-writes.js +7 -48
  40. package/dist/provider.d.ts +8 -6
  41. package/dist/provider.js +3 -4
  42. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  43. package/dist/providers/_shared/request-token.js +83 -0
  44. package/dist/providers/_shared/rest/dispatch.js +41 -0
  45. package/dist/providers/_shared/rest/operation-index.d.ts +133 -0
  46. package/dist/providers/_shared/rest/operation-index.js +527 -0
  47. package/dist/providers/_shared/rest/tools.d.ts +141 -0
  48. package/dist/providers/_shared/rest/tools.js +569 -0
  49. package/dist/providers/_shared/rest/value-safety.d.ts +151 -0
  50. package/dist/providers/_shared/rest/value-safety.js +580 -0
  51. package/dist/providers/cloudflare/graphql.js +385 -0
  52. package/dist/providers/cloudflare/index.d.ts +47 -51
  53. package/dist/providers/cloudflare/index.js +188 -1997
  54. package/dist/providers/cloudflare/openapi.generated.js +3347 -0
  55. package/dist/providers/cloudflare/rest.d.ts +37 -0
  56. package/dist/providers/cloudflare/rest.js +1056 -0
  57. package/dist/providers/cloudflare/skill.generated.js +7 -4
  58. package/dist/providers/cloudflare/value-safety.js +1247 -0
  59. package/dist/providers/infisical/index.d.ts +7 -0
  60. package/dist/providers/infisical/index.js +713 -0
  61. package/dist/providers/infisical/skill.generated.js +8 -0
  62. package/dist/providers/notion/index.d.ts +35 -31
  63. package/dist/providers/notion/index.js +60 -1499
  64. package/dist/providers/notion/openapi.generated.js +75 -0
  65. package/dist/providers/notion/rest.d.ts +24 -0
  66. package/dist/providers/notion/rest.js +1067 -0
  67. package/dist/providers/notion/skill.generated.js +5 -4
  68. package/dist/providers/notion/value-safety.js +31 -0
  69. package/dist/providers/stripe/index.d.ts +26 -20
  70. package/dist/providers/stripe/index.js +103 -130
  71. package/dist/providers/stripe/openapi.generated.js +649 -0
  72. package/dist/providers/stripe/rest.d.ts +14 -0
  73. package/dist/providers/stripe/rest.js +395 -0
  74. package/dist/providers/stripe/skill.generated.js +7 -5
  75. package/dist/providers/stripe/value-safety.js +705 -0
  76. package/dist/providers/vercel/index.d.ts +31 -22
  77. package/dist/providers/vercel/index.js +96 -853
  78. package/dist/providers/vercel/openapi.generated.js +456 -0
  79. package/dist/providers/vercel/rest.d.ts +20 -0
  80. package/dist/providers/vercel/rest.js +1002 -0
  81. package/dist/providers/vercel/skill.generated.js +4 -5
  82. package/dist/providers/vercel/value-safety.js +328 -0
  83. package/dist/registry.d.ts +8 -6
  84. package/dist/registry.js +134 -61
  85. package/dist/resource-read.js +4 -1
  86. package/dist/routes/mcp.js +11 -2
  87. package/dist/sent-secrets.d.ts +54 -13
  88. package/dist/sent-secrets.js +770 -231
  89. package/dist/skills.js +26 -16
  90. package/dist/sqlite.js +21 -6
  91. package/dist/storage/keys.js +20 -5
  92. package/dist/storage/memory.js +9 -4
  93. package/dist/storage/sql.js +18 -9
  94. package/dist/types.d.ts +17 -3
  95. package/dist/usage-guide.js +49 -40
  96. package/dist/version.d.ts +1 -1
  97. package/dist/version.js +1 -1
  98. package/dist/worker.js +2 -1
  99. package/dist/write-outcome.js +47 -0
  100. package/documentation/README.md +64 -0
  101. package/documentation/architecture.md +241 -97
  102. package/documentation/auth.md +138 -5
  103. package/documentation/code-mode.md +224 -107
  104. package/documentation/deploying.md +146 -0
  105. package/documentation/integrating.md +170 -0
  106. package/documentation/meta-tools.md +142 -30
  107. package/documentation/operating.md +135 -0
  108. package/documentation/operator-tests.md +4 -0
  109. package/documentation/operator-ui.md +132 -0
  110. package/documentation/provider-auth-migration.md +307 -0
  111. package/documentation/provider-migration-0.29.md +22 -5
  112. package/examples/worker/README.md +9 -2
  113. package/package.json +9 -2
  114. package/templates/node/README.md +9 -2
  115. package/templates/node/package.json +1 -1
  116. package/dist/providers/vercel/mcp-ownership.js +0 -212
@@ -1,811 +1,31 @@
1
1
  import { skill } from "./skill.generated.js";
2
- import { HOSTED_REST_OPERATIONS, UNCOVERED_REST_OPERATIONS } from "./mcp-ownership.js";
3
2
  /**
4
3
  * No `@vercel/sdk` on purpose — not a dependency and not an optional peer.
5
- * Direct fetch keeps the root Workers-safe, avoids shipping the SDK's generated
6
- * model graph, and lets the reviewed named operations and the REST hatches share
7
- * one guarded transport. The trade is API drift, handled explicitly rather than
8
- * by the SDK's version bumps: `src/providers/vercel/drift.json` records the
9
- * method, versioned path, spec revision, and request/response digest for every
10
- * fixed endpoint, and `npm run providers:check -- --provider vercel` compares
11
- * those rows with Vercel's published OpenAPI document at
12
- * https://openapi.vercel.sh/ without needing a credential.
4
+ * Direct fetch keeps the root Workers-safe and avoids shipping the SDK's
5
+ * generated model graph. The token connector reads a pinned operation index
6
+ * compiled from Vercel's published OpenAPI document (https://openapi.vercel.sh/,
7
+ * unversioned, so pinned by content hash in `openapi.source.json`);
8
+ * `npm run providers:check -- --provider vercel` reports when the live document
9
+ * moves, and `src/providers/vercel/drift.json` also records the contract digest
10
+ * of every endpoint a named tool is written against.
13
11
  */
14
- import { apiConnector as api } from "../../connectors/api-connector.js";
15
- import { remoteMcp } from "../../connectors/remote-mcp.js";
12
+ import { apiConnector } from "../../connectors/api-connector.js";
16
13
  import { reviewedCatalog } from "../../catalog-drift.js";
17
- import { guardedFetch, retryAfterMs, } from "../../connectors/guarded-fetch.js";
18
- import { ConnectorCallError } from "../../errors.js";
19
14
  import { keys, optionsOf, variants } from "../../config-schema.js";
20
- import { PROVIDER_COMMON } from "../../connectors/option-shapes.js";
21
- import { defineProvider } from "../../provider.js";
22
- /** Vercel's public REST origin. Override only for a proxy or test double. */
23
- export const VERCEL_API_BASE_URL = "https://api.vercel.com";
15
+ import { CALL_ADMISSION, defineProvider } from "../../provider.js";
16
+ import { byAuth, hostedOAuth } from "../_shared/rest/dispatch.js";
17
+ import { restTools } from "../_shared/rest/tools.js";
18
+ import { VERCEL_API_BASE_URL, vercelRest } from "./rest.js";
19
+ export { VERCEL_API_BASE_URL } from "./rest.js";
24
20
  /** Vercel's official hosted MCP endpoint. */
25
21
  export const VERCEL_MCP_ENDPOINT = "https://mcp.vercel.com";
26
- const MAX_PAGE_SIZE = 100;
27
- const DEFAULT_PAGE_SIZE = 20;
28
- const VERCEL_MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
29
- function asRecord(value) {
30
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
31
- }
32
- function asArray(value) {
33
- return Array.isArray(value) ? value : [];
34
- }
35
- function compact(value) {
36
- return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
37
- }
38
- function detailFor(payload, status) {
39
- const root = asRecord(payload);
40
- const error = asRecord(root["error"]);
41
- const code = typeof error["code"] === "string" ? error["code"] : undefined;
42
- const message = typeof error["message"] === "string" && error["message"].trim()
43
- ? error["message"].trim()
44
- : typeof root["message"] === "string" && root["message"].trim()
45
- ? root["message"].trim()
46
- : `Vercel returned HTTP ${status}.`;
47
- return code ? `Vercel ${code}: ${message}` : message;
48
- }
49
- function resetAfterMs(headers) {
50
- const retryAfter = retryAfterMs(headers);
51
- if (retryAfter !== undefined)
52
- return retryAfter;
53
- const raw = headers.get("x-ratelimit-reset");
54
- if (!raw)
55
- return undefined;
56
- const seconds = Number(raw);
57
- if (!Number.isFinite(seconds))
58
- return undefined;
59
- return Math.max(0, Math.trunc(seconds * 1_000 - Date.now()));
60
- }
61
- /** Map Vercel failures by the caller's useful next move. */
62
- function vercelFailure(status, headers, payload) {
63
- const detail = detailFor(payload, status);
64
- if (status === 429) {
65
- const wait = resetAfterMs(headers);
66
- return new ConnectorCallError("rate_limited", `${detail} Vercel meters endpoints separately; wait for the reported reset before retrying this operation.`, wait === undefined ? {} : { retryAfterMs: wait });
67
- }
68
- if (status === 401) {
69
- 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.`);
70
- }
71
- if (status === 403) {
72
- return new ConnectorCallError("provider_permission_denied", `${detail} Vercel refused access to this team or resource. Ask the team administrator to grant access and an operator to widen the configured token's Vercel scope.`);
73
- }
74
- if (status === 404) {
75
- return new ConnectorCallError("not_found", `${detail} Confirm the project, deployment, domain, or environment-variable id with its list tool.`);
76
- }
77
- if (status === 400 || status === 409 || status === 422) {
78
- return new ConnectorCallError("invalid_args", detail);
79
- }
80
- if (status >= 500) {
81
- const wait = resetAfterMs(headers);
82
- return new ConnectorCallError("unavailable", `${detail} Vercel is failing upstream.`, wait === undefined ? {} : { retryAfterMs: wait });
83
- }
84
- return new ConnectorCallError("connector_call_failed", detail, {
85
- retryable: false,
86
- });
87
- }
88
- function parseBody(text, contentType, strict = false) {
89
- if (!text)
90
- return undefined;
91
- try {
92
- return JSON.parse(text);
93
- }
94
- catch {
95
- if (contentType?.includes("stream+json") || contentType?.includes("ndjson")) {
96
- if (strict)
97
- return parseStreamRows(text);
98
- const rows = [];
99
- for (const line of text.split("\n")) {
100
- if (!line.trim())
101
- continue;
102
- try {
103
- rows.push(JSON.parse(line));
104
- }
105
- catch {
106
- rows.push({ message: line });
107
- }
108
- }
109
- return rows;
110
- }
111
- if (strict) {
112
- throw new ConnectorCallError("connector_call_failed", "Vercel returned a malformed successful response.", {
113
- retryable: false,
114
- });
115
- }
116
- return text;
117
- }
118
- }
119
- function parseStreamRows(text) {
120
- if (!text.trim())
121
- return [];
122
- try {
123
- const payload = JSON.parse(text);
124
- return Array.isArray(payload) ? payload : [payload];
125
- }
126
- catch {
127
- const rows = [];
128
- for (const line of text.split("\n")) {
129
- if (!line.trim())
130
- continue;
131
- try {
132
- rows.push(JSON.parse(line));
133
- }
134
- catch {
135
- throw new ConnectorCallError("connector_call_failed", "Vercel returned a malformed runtime-log stream.", {
136
- retryable: false,
137
- });
138
- }
139
- }
140
- return rows;
141
- }
142
- }
143
- function vercelTransport(baseUrl) {
144
- return guardedFetch({
145
- provider: "Vercel",
146
- baseUrl,
147
- headers: { Accept: "application/json" },
148
- maxResponseBytes: VERCEL_MAX_RESPONSE_BYTES,
149
- authenticate: async (ctx) => {
150
- const token = (await ctx.credential?.get())?.trim();
151
- if (!token) {
152
- throw new ConnectorCallError("auth_required", "No Vercel access token is configured for this connector. Call authorize_connector for recovery options. When available, an operator can add the token in this connection in the operator UI.");
153
- }
154
- return { Authorization: `Bearer ${token}` };
155
- },
156
- });
157
- }
158
- async function callVercel(send, request, ctx, success = {}) {
159
- return await send(request, ctx, async (response) => {
160
- const text = await response.text();
161
- if (!response.ok) {
162
- throw vercelFailure(response.status, response.headers, parseBody(text, response.headers.get("content-type")));
163
- }
164
- return success.parse ? success.parse(text) : parseBody(text, response.headers.get("content-type"), !success.raw);
165
- });
166
- }
167
- function teamQuery(args, defaultTeamId) {
168
- return {
169
- teamId: args["teamId"] === null ? undefined : (args["teamId"] ?? defaultTeamId),
170
- };
171
- }
172
- function projectDomain(value) {
173
- const domain = asRecord(value);
174
- return compact({
175
- name: domain["name"],
176
- apexName: domain["apexName"],
177
- projectId: domain["projectId"],
178
- verified: domain["verified"] === true,
179
- verification: domain["verification"],
180
- redirect: domain["redirect"],
181
- redirectStatusCode: domain["redirectStatusCode"],
182
- gitBranch: domain["gitBranch"],
183
- customEnvironmentId: domain["customEnvironmentId"],
184
- createdAt: domain["createdAt"],
185
- updatedAt: domain["updatedAt"],
186
- });
187
- }
188
- /** Deliberately omits `value`, even if a raw Vercel response happens to carry it. */
189
- function projectEnvironmentVariable(value) {
190
- const variable = asRecord(value);
191
- return compact({
192
- id: variable["id"],
193
- key: variable["key"],
194
- type: variable["type"],
195
- visibility: variable["visibility"],
196
- target: variable["target"],
197
- gitBranch: variable["gitBranch"],
198
- customEnvironmentIds: variable["customEnvironmentIds"],
199
- comment: variable["comment"],
200
- createdAt: variable["createdAt"],
201
- updatedAt: variable["updatedAt"],
202
- });
203
- }
204
- const TEAM_ID_PROPERTY = {
205
- type: ["string", "null"],
206
- minLength: 1,
207
- description: "Vercel team id. Omit for the configured default; pass null for the token owner's personal account.",
208
- };
209
- const PROJECT_ID_PROPERTY = {
210
- type: "string",
211
- minLength: 1,
212
- description: "Project id or project name from list_projects.",
213
- };
214
- const DEPLOYMENT_ID_PROPERTY = {
215
- type: "string",
216
- minLength: 1,
217
- description: "Deployment id from list_deployments.",
218
- };
219
- const DOMAIN_SCHEMA = {
220
- type: "object",
221
- properties: {
222
- name: { type: "string" },
223
- apexName: { type: "string" },
224
- projectId: { type: "string" },
225
- verified: { type: "boolean" },
226
- verification: { type: "array" },
227
- redirect: { type: ["string", "null"] },
228
- redirectStatusCode: { type: ["integer", "null"] },
229
- gitBranch: { type: ["string", "null"] },
230
- customEnvironmentId: { type: ["string", "null"] },
231
- createdAt: { type: "number" },
232
- updatedAt: { type: "number" },
233
- },
234
- required: ["name", "projectId", "verified"],
235
- };
236
- const ENV_SCHEMA = {
237
- type: "object",
238
- properties: {
239
- id: { type: "string" },
240
- key: { type: "string" },
241
- type: { type: "string" },
242
- visibility: { type: "string" },
243
- target: { type: ["array", "string"], items: { type: "string" } },
244
- gitBranch: { type: ["string", "null"] },
245
- customEnvironmentIds: { type: "array", items: { type: "string" } },
246
- comment: { type: "string" },
247
- createdAt: { type: "number" },
248
- updatedAt: { type: "number" },
249
- },
250
- required: ["id", "key", "type"],
251
- };
252
- function namedInput(properties, required) {
253
- return { type: "object", properties, required, additionalProperties: false };
254
- }
255
- function queryPairs(value) {
256
- const query = {};
257
- for (const row of asArray(value)) {
258
- const pair = asRecord(row);
259
- if (typeof pair["name"] !== "string")
260
- continue;
261
- const item = pair["value"];
262
- if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
263
- query[pair["name"]] = item;
264
- }
265
- }
266
- return query;
267
- }
268
- const QUERY_PROPERTY = {
269
- type: "array",
270
- description: "Provider query parameters as name/value pairs.",
271
- items: {
272
- type: "object",
273
- properties: {
274
- name: { type: "string", minLength: 1, description: "Query parameter name." },
275
- value: {
276
- type: ["string", "number", "boolean"],
277
- description: "Query parameter value; guarded transport stringifies it once.",
278
- },
279
- },
280
- required: ["name", "value"],
281
- additionalProperties: false,
282
- },
283
- };
284
- const HEADERS_PROPERTY = {
285
- type: "array",
286
- description: "Endpoint-specific request headers. Credential, cookie, host, framing, and content-type headers are connector-owned.",
287
- items: {
288
- type: "object",
289
- properties: {
290
- name: { type: "string", minLength: 1, description: "HTTP header name." },
291
- value: { type: "string", description: "HTTP header value." },
292
- },
293
- required: ["name", "value"],
294
- additionalProperties: false,
295
- },
296
- };
297
- const FORBIDDEN_HATCH_HEADERS = new Set([
298
- "authorization",
299
- "content-length",
300
- "content-type",
301
- "cookie",
302
- "host",
303
- "transfer-encoding",
304
- ]);
305
- function headerPairs(value) {
306
- const headers = {};
307
- for (const row of asArray(value)) {
308
- const pair = asRecord(row);
309
- if (typeof pair["name"] === "string" && typeof pair["value"] === "string") {
310
- const normalized = pair["name"].trim().toLowerCase();
311
- if (FORBIDDEN_HATCH_HEADERS.has(normalized)) {
312
- throw new ConnectorCallError("invalid_args", `A Vercel upload may not set the ${normalized} header; the connector owns credentials, cookies, origin, framing, and content type.`);
313
- }
314
- headers[pair["name"]] = pair["value"];
315
- }
316
- }
317
- return headers;
318
- }
319
- function uploadBody(args) {
320
- const hasText = typeof args["textBody"] === "string";
321
- const hasBase64 = typeof args["base64Body"] === "string";
322
- if (hasText === hasBase64) {
323
- throw new ConnectorCallError("invalid_args", "Provide exactly one of textBody or base64Body for a Vercel upload.");
324
- }
325
- if (hasText)
326
- return args["textBody"];
327
- try {
328
- return Uint8Array.from(atob(args["base64Body"]), (character) => character.charCodeAt(0));
329
- }
330
- catch {
331
- throw new ConnectorCallError("invalid_args", "base64Body is not valid base64.");
332
- }
333
- }
334
- function rawRequest(args, defaultTeamId) {
335
- const method = args["method"] ?? "GET";
336
- let path;
337
- try {
338
- path = decodeURIComponent(new URL(`https://api.vercel.com${String(args["path"])}`).pathname).replace(/\/{2,}/g, "/");
339
- }
340
- catch {
341
- throw new ConnectorCallError("invalid_args", "The REST path contains an invalid escape or URL.");
342
- }
343
- const matches = ([verb, pattern]) => verb === method && pattern.test(path);
344
- const canonical = VERCEL_EXACT_CANONICAL_ROUTES.find(matches) ?? VERCEL_CANONICAL_ROUTES.find(matches);
345
- if (canonical?.[2]) {
346
- throw new ConnectorCallError("invalid_args", `Use ${canonical[2]} on its owning connector. The REST complement cannot repeat that operation.`);
347
- }
348
- const query = queryPairs(args["query"]);
349
- if (args["personalAccount"] === true && (query["teamId"] !== undefined || query["slug"] !== undefined)) {
350
- throw new ConnectorCallError("invalid_args", "personalAccount cannot be combined with a teamId or slug query parameter.");
351
- }
352
- if (args["personalAccount"] !== true &&
353
- defaultTeamId &&
354
- query["teamId"] === undefined &&
355
- query["slug"] === undefined) {
356
- query["teamId"] = defaultTeamId;
357
- }
358
- return { path: String(args["path"]), query };
359
- }
360
- function hostedRestRoute([method, path, name], anyVersion) {
361
- const pattern = path
362
- .replace(/\/+$/, "")
363
- .split("/")
364
- .map((segment, index) => {
365
- if (anyVersion && index === 1 && /^v\d+$/.test(segment))
366
- return "v\\d+";
367
- if (/^\{[^}]+\}$/.test(segment))
368
- return "[^/]+";
369
- return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
370
- })
371
- .join("/");
372
- return [method, new RegExp(`^${pattern}\\/?$`), `MCP ${name}`];
373
- }
374
- // Resolve actual published method/path/version contracts first, including
375
- // uncovered concrete endpoints. An id wildcard cannot consume a REST gap.
376
- const VERCEL_EXACT_CANONICAL_ROUTES = [
377
- ...UNCOVERED_REST_OPERATIONS.map(([method, path]) => [
378
- method,
379
- hostedRestRoute([method, path, ""], false)[1],
380
- null,
381
- ]),
382
- ...HOSTED_REST_OPERATIONS.map((operation) => hostedRestRoute(operation, false)),
383
- ];
384
- // Version-independent matches keep a raw hatch from restoring a removed
385
- // duplicate by selecting an older REST version. Env routes also protect the
386
- // value-safe named API implementations from an unprojected response.
387
- const VERCEL_CANONICAL_ROUTES = [
388
- // Specific published routes precede generic id patterns (e.g. projects/traces).
389
- ...HOSTED_REST_OPERATIONS.map((operation) => hostedRestRoute(operation, true)),
390
- ["GET", /^\/v\d+\/teams\/?$/, "MCP list_teams"],
391
- ["GET", /^\/v\d+\/projects\/?$/, "MCP list_projects"],
392
- ["GET", /^\/v\d+\/projects\/[^/]+\/?$/, "MCP get_project"],
393
- ["GET", /^\/v\d+\/deployments\/?$/, "MCP list_deployments"],
394
- ["GET", /^\/v\d+\/deployments\/[^/]+\/?$/, "MCP get_deployment"],
395
- ["GET", /^\/v\d+\/deployments\/[^/]+\/events\/?$/, "MCP get_deployment_build_logs or list_deployment_events"],
396
- ["GET", /^\/v\d+\/projects\/[^/]+\/deployments\/[^/]+\/runtime-logs\/?$/, "MCP get_runtime_logs"],
397
- ["GET", /^\/v\d+\/projects\/[^/]+\/domains\/?$/, "MCP list_project_domains"],
398
- ["POST", /^\/v\d+\/projects\/[^/]+\/domains\/?$/, "MCP add_project_domain"],
399
- ["PATCH", /^\/v\d+\/deployments\/[^/]+\/cancel\/?$/, "MCP cancel_deployment"],
400
- ["POST", /^\/v\d+\/files\/?$/, "MCP upload_file"],
401
- ["POST", /^\/v\d+\/deployments\/?$/, "MCP create_deployment"],
402
- ["DELETE", /^\/v\d+\/deployments\/[^/]+\/?$/, "API delete_deployment"],
403
- ["POST", /^\/v\d+\/projects\/[^/]+\/promote\/[^/]+\/?$/, "MCP request_promote"],
404
- ["POST", /^\/v\d+\/projects\/[^/]+\/domains\/[^/]+\/verify\/?$/, "API verify_project_domain"],
405
- ["DELETE", /^\/v\d+\/projects\/[^/]+\/domains\/[^/]+\/?$/, "API remove_project_domain"],
406
- ["GET", /^\/v\d+\/projects\/[^/]+\/env(?:\/[^/]+)?\/?$/, "API list_project_env_vars"],
407
- ["POST", /^\/v\d+\/projects\/[^/]+\/env\/?$/, "API upsert_project_env_var"],
408
- ["PATCH", /^\/v\d+\/projects\/[^/]+\/env\/[^/]+\/?$/, "API update_project_env_var"],
409
- ["DELETE", /^\/v\d+\/projects\/[^/]+\/env\/[^/]+\/?$/, "API delete_project_env_var"],
410
- ];
411
- const API_OWNED_MCP_TOOLS = new Set([
412
- "filter_project_envs",
413
- "get_project_env",
414
- "create_project_env",
415
- "edit_project_env",
416
- ]);
417
- /** Preserve vendor contracts for retained tools and refuse hidden direct calls. */
418
- function vercelCatalog(connector) {
419
- return Object.assign(Object.create(connector), connector, {
420
- async listTools(ctx) {
421
- const tools = (await connector.listTools(ctx)).filter((tool) => !API_OWNED_MCP_TOOLS.has(tool.name));
422
- if (new Set(tools.map((tool) => tool.name)).size !== tools.length) {
423
- throw new ConnectorCallError("connector_call_failed", "Vercel returned duplicate tool names.", {
424
- retryable: false,
425
- });
426
- }
427
- return tools;
428
- },
429
- async callTool(name, args, ctx, options) {
430
- if (API_OWNED_MCP_TOOLS.has(name)) {
431
- throw new ConnectorCallError("invalid_args", "Project environment variables belong to the value-safe Vercel REST complement.");
432
- }
433
- return await connector.callTool(name, args, ctx, options);
434
- },
435
- });
436
- }
437
- const PERSONAL_ACCOUNT_PROPERTY = {
438
- type: "boolean",
439
- description: "True omits the configured default team. Do not combine with a teamId or slug query parameter.",
440
- };
441
- function tools(send, defaultTeamId) {
442
- const readOnly = { readOnlyHint: true };
443
- const destructive = { readOnlyHint: false, destructiveHint: true };
444
- const team = (args) => teamQuery(args, defaultTeamId);
445
- return [
446
- {
447
- name: "vercel_api_get",
448
- description: "Call any Vercel REST GET endpoint and return its untouched response. Use named reads first for smaller results and stable projections.",
449
- annotations: readOnly,
450
- inputSchema: namedInput({
451
- path: {
452
- type: "string",
453
- minLength: 1,
454
- description: "Path below api.vercel.com beginning with '/', including its API version. No query string.",
455
- },
456
- query: QUERY_PROPERTY,
457
- personalAccount: PERSONAL_ACCOUNT_PROPERTY,
458
- }, ["path"]),
459
- outputSchema: {
460
- type: "object",
461
- properties: { result: { description: "Vercel's untouched response body." } },
462
- required: ["result"],
463
- },
464
- handler: async (args, ctx) => ({
465
- result: (await callVercel(send, { method: "GET", ...rawRequest(args, defaultTeamId) }, ctx, { raw: true })) ?? null,
466
- }),
467
- },
468
- {
469
- name: "vercel_api_mutate",
470
- description: "Call any JSON Vercel REST mutation endpoint. The approval-gated hatch for API operations the named tools do not cover; no file uploads.",
471
- annotations: destructive,
472
- inputSchema: namedInput({
473
- method: {
474
- type: "string",
475
- enum: ["POST", "PUT", "PATCH", "DELETE"],
476
- description: "HTTP mutation method required by the Vercel endpoint.",
477
- },
478
- path: {
479
- type: "string",
480
- minLength: 1,
481
- description: "Path below api.vercel.com beginning with '/', including its API version. No query string.",
482
- },
483
- query: QUERY_PROPERTY,
484
- personalAccount: PERSONAL_ACCOUNT_PROPERTY,
485
- body: {
486
- type: ["object", "array", "string", "number", "boolean", "null"],
487
- description: "JSON body exactly as documented by Vercel. Omit when the endpoint has no body.",
488
- },
489
- }, ["method", "path"]),
490
- outputSchema: {
491
- type: "object",
492
- properties: { result: { description: "Vercel's untouched response body, or null for an empty response." } },
493
- required: ["result"],
494
- },
495
- handler: async (args, ctx) => ({
496
- result: (await callVercel(send, {
497
- method: args["method"],
498
- ...rawRequest(args, defaultTeamId),
499
- ...(args["body"] !== undefined ? { body: args["body"] } : {}),
500
- }, ctx, { raw: true })) ?? null,
501
- }),
502
- },
503
- {
504
- name: "vercel_api_upload",
505
- 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.",
506
- annotations: destructive,
507
- inputSchema: namedInput({
508
- method: {
509
- type: "string",
510
- enum: ["POST", "PUT"],
511
- description: "Upload method required by the Vercel endpoint.",
512
- },
513
- path: {
514
- type: "string",
515
- minLength: 1,
516
- description: "Path below api.vercel.com beginning with '/', including its API version. No query string.",
517
- },
518
- query: QUERY_PROPERTY,
519
- personalAccount: PERSONAL_ACCOUNT_PROPERTY,
520
- headers: HEADERS_PROPERTY,
521
- contentType: {
522
- type: "string",
523
- minLength: 1,
524
- description: "Content-Type for the raw body, such as application/octet-stream.",
525
- },
526
- textBody: {
527
- type: "string",
528
- description: "Raw UTF-8 body. Exclusive with base64Body.",
529
- },
530
- base64Body: {
531
- type: "string",
532
- description: "Base64-encoded bytes. Exclusive with textBody.",
533
- },
534
- }, ["method", "path", "contentType"]),
535
- outputSchema: {
536
- type: "object",
537
- properties: {
538
- result: {
539
- description: "Vercel's untouched upload response body, or null for an empty response.",
540
- },
541
- },
542
- required: ["result"],
543
- },
544
- handler: async (args, ctx) => ({
545
- result: (await callVercel(send, {
546
- method: args["method"],
547
- ...rawRequest(args, defaultTeamId),
548
- headers: {
549
- ...headerPairs(args["headers"]),
550
- "Content-Type": args["contentType"],
551
- },
552
- rawBody: uploadBody(args),
553
- }, ctx, { raw: true })) ?? null,
554
- }),
555
- },
556
- {
557
- name: "verify_project_domain",
558
- description: "Ask Vercel to verify a project's pending domain after its DNS challenge has been completed. Returns the current domain state.",
559
- annotations: destructive,
560
- inputSchema: namedInput({
561
- projectId: PROJECT_ID_PROPERTY,
562
- domain: { type: "string", minLength: 1, description: "Pending domain name from list_project_domains." },
563
- teamId: TEAM_ID_PROPERTY,
564
- }, ["projectId", "domain"]),
565
- outputSchema: DOMAIN_SCHEMA,
566
- handler: async (args, ctx) => projectDomain(await callVercel(send, {
567
- method: "POST",
568
- path: `/v9/projects/${encodeURIComponent(args["projectId"])}/domains/${encodeURIComponent(args["domain"])}/verify`,
569
- query: team(args),
570
- }, ctx)),
571
- },
572
- {
573
- name: "remove_project_domain",
574
- description: "Remove a domain from one Vercel project. Optionally remove project domains that redirect to it; this does not delete the account-level domain.",
575
- annotations: destructive,
576
- inputSchema: namedInput({
577
- projectId: PROJECT_ID_PROPERTY,
578
- domain: { type: "string", minLength: 1, description: "Project domain name from list_project_domains." },
579
- removeRedirects: { type: "boolean", description: "Also remove project domains that redirect to this one." },
580
- teamId: TEAM_ID_PROPERTY,
581
- }, ["projectId", "domain"]),
582
- outputSchema: {
583
- type: "object",
584
- properties: { removed: { type: "boolean" }, domain: { type: "string" } },
585
- required: ["removed", "domain"],
586
- },
587
- handler: async (args, ctx) => {
588
- await callVercel(send, {
589
- method: "DELETE",
590
- path: `/v9/projects/${encodeURIComponent(args["projectId"])}/domains/${encodeURIComponent(args["domain"])}`,
591
- query: team(args),
592
- body: args["removeRedirects"] === undefined ? undefined : { removeRedirects: args["removeRedirects"] },
593
- }, ctx);
594
- return { removed: true, domain: args["domain"] };
595
- },
596
- },
597
- {
598
- name: "list_project_env_vars",
599
- description: "List a project's environment-variable metadata without decrypting or returning values. Includes targets, visibility, branches, and custom environments.",
600
- annotations: readOnly,
601
- inputSchema: namedInput({
602
- projectId: PROJECT_ID_PROPERTY,
603
- teamId: TEAM_ID_PROPERTY,
604
- gitBranch: { type: "string", description: "Preview branch filter." },
605
- customEnvironmentId: { type: "string", description: "Custom environment filter." },
606
- }, ["projectId"]),
607
- outputSchema: {
608
- type: "object",
609
- properties: { variables: { type: "array", items: ENV_SCHEMA } },
610
- required: ["variables"],
611
- },
612
- handler: async (args, ctx) => {
613
- const payload = await callVercel(send, {
614
- method: "GET",
615
- path: `/v10/projects/${encodeURIComponent(args["projectId"])}/env`,
616
- query: {
617
- ...team(args),
618
- gitBranch: args["gitBranch"],
619
- customEnvironmentId: args["customEnvironmentId"],
620
- decrypt: "false",
621
- },
622
- }, ctx);
623
- return { variables: asArray(asRecord(payload)["envs"]).map(projectEnvironmentVariable) };
624
- },
625
- },
626
- {
627
- name: "upsert_project_env_var",
628
- description: "Create or replace one Vercel project environment variable. Changes affect only future deployments; trigger a new deployment separately.",
629
- annotations: destructive,
630
- inputSchema: namedInput({
631
- projectId: PROJECT_ID_PROPERTY,
632
- teamId: TEAM_ID_PROPERTY,
633
- key: { type: "string", minLength: 1, maxLength: 256, description: "Environment variable name." },
634
- value: {
635
- type: "string",
636
- maxLength: 65536,
637
- description: "New value. Vercel's total project-environment payload is capped at 64 KB.",
638
- },
639
- type: {
640
- type: "string",
641
- enum: ["plain", "encrypted", "sensitive"],
642
- description: "Storage type. Sensitive values cannot be read back.",
643
- },
644
- targets: {
645
- type: "array",
646
- minItems: 1,
647
- uniqueItems: true,
648
- items: { type: "string", enum: ["production", "preview", "development"] },
649
- description: "Default Vercel environments that receive this value.",
650
- },
651
- gitBranch: { type: "string", description: "Optional preview-only Git branch." },
652
- customEnvironmentIds: {
653
- type: "array",
654
- uniqueItems: true,
655
- items: { type: "string", minLength: 1 },
656
- description: "Custom environment ids that receive this value.",
657
- },
658
- comment: { type: "string", maxLength: 500, description: "Operator-facing note explaining the variable." },
659
- upsert: {
660
- type: "boolean",
661
- description: "Defaults to true. Set false for create-only behavior that refuses to overwrite an existing variable.",
662
- },
663
- }, ["projectId", "key", "value", "type", "targets"]),
664
- outputSchema: ENV_SCHEMA,
665
- handler: async (args, ctx) => {
666
- const payload = asRecord(await callVercel(send, {
667
- method: "POST",
668
- path: `/v10/projects/${encodeURIComponent(args["projectId"])}/env`,
669
- query: { ...team(args), upsert: args["upsert"] === false ? "false" : "true" },
670
- body: compact({
671
- key: args["key"],
672
- value: args["value"],
673
- type: args["type"],
674
- target: args["targets"],
675
- gitBranch: args["gitBranch"],
676
- customEnvironmentIds: args["customEnvironmentIds"],
677
- comment: args["comment"],
678
- }),
679
- }, ctx));
680
- const failed = asArray(payload["failed"]);
681
- if (failed.length > 0) {
682
- const error = asRecord(asRecord(failed[0])["error"]);
683
- const code = typeof error["code"] === "string" ? `${error["code"]}: ` : "";
684
- const message = typeof error["message"] === "string" ? error["message"] : "Vercel rejected the environment-variable write.";
685
- throw new ConnectorCallError("invalid_args", `Vercel ${code}${message}`);
686
- }
687
- const created = Array.isArray(payload["created"]) ? payload["created"][0] : payload["created"];
688
- const result = projectEnvironmentVariable(created ?? payload);
689
- if (!result["id"] || !result["key"] || !result["type"]) {
690
- throw new ConnectorCallError("connector_call_failed", "Vercel accepted the environment-variable write without returning the created variable.", { retryable: false });
691
- }
692
- return result;
693
- },
694
- },
695
- {
696
- name: "update_project_env_var",
697
- description: "Update one Vercel project environment variable by id. Send only fields that should change; deployments keep their previous values.",
698
- annotations: destructive,
699
- inputSchema: namedInput({
700
- projectId: PROJECT_ID_PROPERTY,
701
- teamId: TEAM_ID_PROPERTY,
702
- envVarId: {
703
- type: "string",
704
- minLength: 1,
705
- description: "Environment-variable id from list_project_env_vars.",
706
- },
707
- key: { type: "string", minLength: 1, maxLength: 256, description: "Replacement variable name." },
708
- value: { type: "string", maxLength: 65536, description: "Replacement value." },
709
- type: { type: "string", enum: ["plain", "encrypted", "sensitive"], description: "Replacement storage type." },
710
- targets: {
711
- type: "array",
712
- minItems: 1,
713
- uniqueItems: true,
714
- items: { type: "string", enum: ["production", "preview", "development"] },
715
- description: "Replacement default environments.",
716
- },
717
- gitBranch: { type: ["string", "null"], description: "Replacement preview branch, or null to clear it." },
718
- customEnvironmentIds: {
719
- type: "array",
720
- uniqueItems: true,
721
- items: { type: "string", minLength: 1 },
722
- description: "Replacement custom environment ids.",
723
- },
724
- comment: { type: "string", maxLength: 500, description: "Replacement operator-facing note." },
725
- }, ["projectId", "envVarId"]),
726
- outputSchema: ENV_SCHEMA,
727
- handler: async (args, ctx) => {
728
- const body = compact({
729
- key: args["key"],
730
- value: args["value"],
731
- type: args["type"],
732
- target: args["targets"],
733
- gitBranch: args["gitBranch"],
734
- customEnvironmentIds: args["customEnvironmentIds"],
735
- comment: args["comment"],
736
- });
737
- if (Object.keys(body).length === 0) {
738
- throw new ConnectorCallError("invalid_args", "Nothing to update: provide key, value, type, targets, gitBranch, customEnvironmentIds, or comment.");
739
- }
740
- return projectEnvironmentVariable(await callVercel(send, {
741
- method: "PATCH",
742
- path: `/v9/projects/${encodeURIComponent(args["projectId"])}/env/${encodeURIComponent(args["envVarId"])}`,
743
- query: team(args),
744
- body,
745
- }, ctx));
746
- },
747
- },
748
- {
749
- name: "delete_project_env_var",
750
- description: "Delete one environment variable from a Vercel project by id. Existing deployments keep their embedded value; future deployments do not.",
751
- annotations: destructive,
752
- inputSchema: namedInput({
753
- projectId: PROJECT_ID_PROPERTY,
754
- envVarId: {
755
- type: "string",
756
- minLength: 1,
757
- description: "Environment-variable id from list_project_env_vars.",
758
- },
759
- teamId: TEAM_ID_PROPERTY,
760
- }, ["projectId", "envVarId"]),
761
- outputSchema: {
762
- type: "object",
763
- properties: { deleted: { type: "boolean" }, envVarId: { type: "string" } },
764
- required: ["deleted", "envVarId"],
765
- },
766
- handler: async (args, ctx) => {
767
- await callVercel(send, {
768
- method: "DELETE",
769
- path: `/v9/projects/${encodeURIComponent(args["projectId"])}/env/${encodeURIComponent(args["envVarId"])}`,
770
- query: team(args),
771
- }, ctx);
772
- return { deleted: true, envVarId: args["envVarId"] };
773
- },
774
- },
775
- {
776
- name: "delete_deployment",
777
- description: "Permanently delete one Vercel deployment and its deployment URL. This cannot be undone; use cancel_deployment for work still running.",
778
- annotations: destructive,
779
- inputSchema: namedInput({ deploymentId: DEPLOYMENT_ID_PROPERTY, teamId: TEAM_ID_PROPERTY }, ["deploymentId"]),
780
- outputSchema: {
781
- type: "object",
782
- properties: { deleted: { type: "boolean" }, deploymentId: { type: "string" } },
783
- required: ["deleted", "deploymentId"],
784
- },
785
- handler: async (args, ctx) => {
786
- await callVercel(send, { method: "DELETE", path: `/v13/deployments/${encodeURIComponent(args["deploymentId"])}`, query: team(args) }, ctx);
787
- return { deleted: true, deploymentId: args["deploymentId"] };
788
- },
789
- },
790
- ];
791
- }
792
- function apiUsageGuide(purpose, teamId, instructions) {
793
- const accountInstructions = instructions?.trim();
794
- return `# Vercel usage
795
-
796
- Account purpose: ${purpose}
797
-
798
- ## Scope before action
799
-
800
- ${teamId
801
- ? `This connection defaults to team \`${teamId}${skill.fragments.guide_0}`
802
- : "This connection defaults to the token owner's personal account. Use hosted MCP `list_teams`, then pass `teamId`, for team-owned resources."}${skill.fragments.guide_1}${accountInstructions ? `\n## ${skill.instructionsHeading}\n\n${accountInstructions}\n` : ""}`;
803
- }
804
22
  /**
805
23
  * Reviewed in #705's provider audit against https://vercel.com/docs/mcp/vercel-mcp/tools.
806
24
  * Retains the release-reviewed inventory, including names absent from today's
807
25
  * public reference. Live annotations were not reverified without credentials.
808
- * No schema digest is asserted without a captured schema to review.
26
+ * No schema digest is asserted without a captured schema to review. This
27
+ * classifies the hosted OAuth catalog only; the REST connector annotates each
28
+ * tool it authors.
809
29
  */
810
30
  const VERCEL_MCP_CLASSIFICATION = {
811
31
  tools: {
@@ -947,92 +167,115 @@ const VERCEL_MCP_CLASSIFICATION = {
947
167
  "verdict": "destructive",
948
168
  "reason": "edit toolbar message changes existing Vercel state or removes it.",
949
169
  },
170
+ "filter_project_envs": {
171
+ "verdict": "write",
172
+ "reason": "Lists project environment variables and can return decrypted values; reviewed as a write so the pool trust policy gates the disclosure (#801).",
173
+ },
174
+ "get_project_env": {
175
+ "verdict": "write",
176
+ "reason": "Returns one environment variable's decrypted value; reviewed as a write so the pool trust policy gates the disclosure (#801).",
177
+ },
178
+ "create_project_env": {
179
+ "verdict": "destructive",
180
+ "reason": "Creates or overwrites project environment variables that future deployments embed.",
181
+ },
182
+ "edit_project_env": {
183
+ "verdict": "destructive",
184
+ "reason": "Replaces an existing environment variable's value or targets.",
185
+ },
950
186
  },
951
187
  };
952
- function mcpUsageGuide(purpose, instructions) {
953
- const accountInstructions = instructions?.trim();
954
- return `${skill.fragments.guide_2}${purpose}${skill.fragments.guide_3}${accountInstructions ? `\n## ${skill.instructionsHeading}\n\n${accountInstructions}\n` : ""}`;
188
+ function instructionsSection(instructions) {
189
+ const text = instructions?.trim();
190
+ return text ? `\n## ${skill.instructionsHeading}\n\n${text}\n` : "";
191
+ }
192
+ function oauthUsageGuide(purpose, instructions) {
193
+ const { oauth, shared } = skill.fragments;
194
+ return `# Vercel usage
195
+
196
+ Vercel's hosted MCP server over OAuth. Account purpose: ${purpose}${oauth}${shared}${instructionsSection(instructions)}`;
955
197
  }
956
- function vercelMcp(id, purpose, options, provider) {
957
- const connector = remoteMcp(id, {
198
+ function tokenUsageGuide(purpose, teamId, instructions) {
199
+ const { key, shared } = skill.fragments;
200
+ const scope = teamId
201
+ ? `This connection defaults to team \`${teamId}\`: operations that accept a team get it unless a call passes \`teamId\` (or \`slug\`), and \`teamId: null\` targets the token owner's personal account.`
202
+ : "This connection defaults to the token owner's personal account. Call `list_teams`, then pass `teamId` for team-owned resources.";
203
+ return `# Vercel usage
204
+
205
+ Connecta's REST connector to the Vercel API with one access token. Account purpose: ${purpose}
206
+
207
+ ${scope}${key}${shared}${instructionsSection(instructions)}`;
208
+ }
209
+ const COMMON = {
210
+ ...keys("title", "authScope", "purpose", "instructions", "maxResultBytes"),
211
+ callAdmission: CALL_ADMISSION,
212
+ };
213
+ /** The closed options vercel() accepts, selected by `auth.type`; see `assertKnownOptions`. */
214
+ const VERCEL_OPTIONS = variants(["auth", "type"], {
215
+ // The never-typed keys stay out of the shape, so the walk refuses them by name.
216
+ oauth: optionsOf()({
217
+ ...COMMON,
218
+ auth: optionsOf()(keys("type")),
219
+ }).shape,
220
+ token: optionsOf()({
221
+ ...COMMON,
222
+ ...keys("teamId", "baseUrl"),
223
+ auth: optionsOf()(keys("type")),
224
+ }).shape,
225
+ });
226
+ function vercelOAuth(id, options, provider) {
227
+ return hostedOAuth(id, provider, {
958
228
  url: VERCEL_MCP_ENDPOINT,
959
- ...provider.connectorOptions,
960
229
  title: options.title ?? "Vercel (MCP)",
961
- description: `Vercel's official hosted MCP surface: ${purpose}`,
962
- auth: { type: "oauth" },
963
- requireHttps: true,
964
- classify: provider.classify,
230
+ description: `Vercel's official hosted MCP surface: ${options.purpose}`,
965
231
  usageGuide: {
966
- content: mcpUsageGuide(purpose, options.instructions),
232
+ content: oauthUsageGuide(options.purpose, options.instructions),
967
233
  summary: "Official MCP. Live Vercel schemas, id resolution, deployment diagnosis, purchases, and access grants.",
968
234
  required: true,
969
235
  },
970
236
  });
971
- return vercelCatalog(connector);
972
237
  }
973
- function vercelApi(id, purpose, options) {
974
- const defaultPageSize = options.defaultPageSize ?? DEFAULT_PAGE_SIZE;
975
- if (!Number.isInteger(defaultPageSize) || defaultPageSize < 1 || defaultPageSize > MAX_PAGE_SIZE) {
976
- throw new Error(`vercel() defaultPageSize must be a whole number between 1 and ${MAX_PAGE_SIZE}.`);
977
- }
978
- const teamId = options.teamId?.trim() || undefined;
979
- const send = vercelTransport(options.baseUrl ?? VERCEL_API_BASE_URL);
980
- return api(id, {
981
- ...(options.authScope ? { authScope: options.authScope } : {}),
238
+ function vercelToken(id, options, provider) {
239
+ const teamId = typeof options.teamId === "string" ? options.teamId.trim() || undefined : undefined;
240
+ const baseUrl = options.baseUrl ?? VERCEL_API_BASE_URL;
241
+ const rest = vercelRest({ baseUrl, teamId });
242
+ return apiConnector(id, {
243
+ ...provider.connectorOptions,
982
244
  title: options.title ?? "Vercel",
983
- description: `Vercel account and deployments: ${purpose}`,
245
+ description: `Vercel account and deployments: ${options.purpose}`,
984
246
  credential: {
985
247
  label: "Vercel access token",
986
248
  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.",
987
249
  placeholder: "Paste Vercel access token",
988
250
  },
989
- testCredential: async (value, ctx) => {
990
- try {
991
- const payload = asRecord(await callVercel(send, { method: "GET", path: "/v2/user" }, { ...ctx, credential: { get: async () => value, getAll: async () => ({ value }) } }));
992
- const user = asRecord(payload["user"] ?? payload);
993
- const identity = user["username"] ?? user["email"] ?? user["name"] ?? user["id"] ?? "Vercel user";
994
- return { ok: true, message: `Authenticated as ${identity}.` };
995
- }
996
- catch (error) {
997
- return {
998
- ok: false,
999
- message: error instanceof ConnectorCallError ? error.message : "Vercel rejected the token.",
1000
- };
1001
- }
1002
- },
251
+ testCredential: rest.testCredential,
1003
252
  usageGuide: {
1004
- content: apiUsageGuide(purpose, teamId, options.instructions),
1005
- summary: "Team scoping, deployment diagnosis, value-safe environment variables, REST hatches, and cursor pagination.",
253
+ content: tokenUsageGuide(options.purpose, teamId, options.instructions),
254
+ summary: "One access token over REST: search operations, read details, then read or write. Value-safe env vars.",
1006
255
  required: true,
1007
256
  },
1008
- ...(options.callAdmission ? { callAdmission: options.callAdmission } : {}),
1009
- tools: tools(send, teamId),
1010
- ...(options.maxResultBytes !== undefined ? { maxResultBytes: options.maxResultBytes } : {}),
257
+ tools: [...restTools(rest.vendor), ...rest.tools],
1011
258
  });
1012
259
  }
1013
- /** The closed options vercel() accepts; see `assertKnownOptions`. */
1014
- const VERCEL_OPTIONS = variants("surface", {
1015
- api: optionsOf()({
1016
- ...PROVIDER_COMMON,
1017
- ...keys("surface", "teamId", "baseUrl", "defaultPageSize"),
1018
- }).shape,
1019
- mcp: optionsOf()({ ...PROVIDER_COMMON, ...keys("surface") }).shape,
1020
- }, "mcp");
1021
- /** A maintained Vercel connection using the selected provider surface. */
260
+ /**
261
+ * A maintained Vercel connection. `auth` selects the implementation: OAuth
262
+ * reaches Vercel's hosted MCP server; an access token reaches Connecta's REST
263
+ * connector to `api.vercel.com`.
264
+ */
1022
265
  export const vercel = defineProvider({
1023
266
  name: "vercel",
1024
267
  title: "Vercel",
1025
- kind: "composed",
268
+ kind: "dual",
1026
269
  readme: "Vercel",
1027
- bundle: { "baselineGzip": 143649, "maxGzip": 203649 },
270
+ bundle: {
271
+ "baselineGzip": 228431,
272
+ "maxGzip": 288431,
273
+ "note": "./providers/vercel remeasures at 228,431 B gzip (#801), from a 143,649 B baseline recorded before the shared entry grew (shared code grew after it was recorded, as other entries measured before #801 show): auth now selects the implementation, so the entry carries the hosted MCP client and Connecta's REST connector (the shared REST module) plus the pinned Vercel operation index (445 operations; request details to depth 2, without descriptions or enums over 50 values; 26,274 B gzip as source; the reviewed value-safety table adds about 5 KB). Details were shrunk before the cap moved: descriptions alone added 15 KB gzip. The cap uses the existing baseline + 60,000 B policy.",
274
+ },
1028
275
  skill,
1029
276
  options: VERCEL_OPTIONS,
1030
277
  classify: VERCEL_MCP_CLASSIFICATION,
1031
- create: vercelConnector,
278
+ create: byAuth({ oauth: vercelOAuth, token: vercelToken }),
1032
279
  });
1033
- function vercelConnector(id, options, provider) {
1034
- const purpose = options.purpose.trim();
1035
- return options.surface === "api" ? vercelApi(id, purpose, options) : vercelMcp(id, purpose, options, provider);
1036
- }
1037
280
  /** @deprecated Read `vercel.definition.classify` instead. Kept for existing imports. */
1038
281
  export const VERCEL_MCP_VETTED_CATALOG = reviewedCatalog(vercel.definition.classify, 'defineProvider("vercel")');