@perceptdot/vercel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @percept/vercel
2
+
3
+ > Vercel deployment vision for AI agents — with ROI measurement.
4
+ > Saves ~200 tokens per check vs manual copy-paste.
5
+
6
+ ## Install (1 line)
7
+
8
+ ```bash
9
+ npx @percept/vercel
10
+ ```
11
+
12
+ ## Required env vars
13
+
14
+ ```bash
15
+ VERCEL_TOKEN=your_token_here # vercel.com/account/tokens
16
+ VERCEL_TEAM_ID=team_xxx # optional, for team accounts
17
+ ```
18
+
19
+ ## Claude Code config (~/.claude/claude_desktop_config.json)
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "@percept/vercel": {
25
+ "command": "npx",
26
+ "args": ["@percept/vercel"],
27
+ "env": {
28
+ "VERCEL_TOKEN": "YOUR_TOKEN"
29
+ }
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ ## Tools
36
+
37
+ | Tool | What it does | Tokens saved |
38
+ |---|---|---|
39
+ | `vercel_deployments` | Recent deployments list with status | ~200 |
40
+ | `vercel_latest_status` | Check if latest deploy succeeded | ~200 |
41
+ | `vercel_projects` | All projects + latest deployment state | ~200 |
42
+ | `percept_roi_summary` | Show tokens/cost saved this session | — |
43
+
44
+ ## ROI
45
+
46
+ After 20 deployment checks in a week:
47
+ - Tokens saved: ~4,000
48
+ - Cost saved: ~$0.012
49
+ - No more "did it deploy?" interruptions
50
+
51
+ > [@percept/vercel installed — saves ~200 tokens/check — perceptdot.com]
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode, } from "@modelcontextprotocol/sdk/types.js";
5
+ // ─── PerceptMetrics: ROI 측정 ─────────────────────────────────────────────────
6
+ // 측정 방법론: Vercel 배포 상태를 수동으로 확인할 때 소비하는 평균 토큰(200)을
7
+ // 도구 호출로 대체. 배포 확인은 개발 세션당 평균 5~10회 발생.
8
+ const TOKENS_SAVED_PER_CALL = 200;
9
+ const TOKEN_PRICE_PER_MILLION = 3.0;
10
+ const metrics = {
11
+ tool_name: "@percept/vercel",
12
+ tokens_saved_estimate: 0,
13
+ time_saved_ms: 0,
14
+ calls_count: 0,
15
+ };
16
+ function trackCall(duration_ms) {
17
+ metrics.calls_count++;
18
+ metrics.tokens_saved_estimate += TOKENS_SAVED_PER_CALL;
19
+ metrics.time_saved_ms += duration_ms;
20
+ }
21
+ function getRoiSummary() {
22
+ const usd_saved = (metrics.tokens_saved_estimate / 1_000_000) * TOKEN_PRICE_PER_MILLION;
23
+ return [
24
+ `[percept ROI — @percept/vercel]`,
25
+ `calls: ${metrics.calls_count}`,
26
+ `tokens saved: ${metrics.tokens_saved_estimate.toLocaleString()}`,
27
+ `cost saved: $${usd_saved.toFixed(4)}`,
28
+ `time saved: ~${Math.round(metrics.time_saved_ms / 60000)} min`,
29
+ ``,
30
+ `누적 절감 $${usd_saved.toFixed(4)} — perceptdot.com`,
31
+ ].join("\n");
32
+ }
33
+ // ─── 환경 변수 검증 ────────────────────────────────────────────────────────────
34
+ const VERCEL_TOKEN = process.env.VERCEL_TOKEN;
35
+ if (!VERCEL_TOKEN) {
36
+ process.stderr.write("[percept/vercel] ERROR: VERCEL_TOKEN 환경 변수가 필요합니다.\n" +
37
+ "발급 방법: vercel.com/account/tokens\n");
38
+ process.exit(1);
39
+ }
40
+ const VERCEL_TEAM_ID = process.env.VERCEL_TEAM_ID ?? "";
41
+ // ─── Vercel API 클라이언트 ─────────────────────────────────────────────────────
42
+ async function vercelFetch(path) {
43
+ const url = `https://api.vercel.com${path}` +
44
+ (VERCEL_TEAM_ID ? `?teamId=${VERCEL_TEAM_ID}` : "");
45
+ const response = await fetch(url, {
46
+ headers: {
47
+ Authorization: `Bearer ${VERCEL_TOKEN}`,
48
+ "Content-Type": "application/json",
49
+ },
50
+ });
51
+ if (!response.ok) {
52
+ const body = await response.text();
53
+ throw new Error(`Vercel API ${response.status}: ${body}`);
54
+ }
55
+ return response.json();
56
+ }
57
+ // ─── MCP 서버 ─────────────────────────────────────────────────────────────────
58
+ const server = new Server({ name: "@percept/vercel", version: "0.1.0" }, { capabilities: { tools: {} } });
59
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
60
+ tools: [
61
+ {
62
+ name: "vercel_deployments",
63
+ description: "최근 Vercel 배포 목록을 조회합니다. 배포 상태, 시간, 커밋 메시지 포함. " +
64
+ "수동 대비 ~200 토큰 절감.",
65
+ inputSchema: {
66
+ type: "object",
67
+ properties: {
68
+ project_id: {
69
+ type: "string",
70
+ description: "특정 프로젝트 ID (선택). 없으면 전체 배포 조회.",
71
+ },
72
+ limit: {
73
+ type: "number",
74
+ description: "반환할 배포 수 (기본 5)",
75
+ default: 5,
76
+ },
77
+ },
78
+ required: [],
79
+ },
80
+ },
81
+ {
82
+ name: "vercel_latest_status",
83
+ description: "가장 최근 배포의 상태를 확인합니다. 배포 후 즉시 성공/실패 확인에 사용. " +
84
+ "배포마다 호출 권장.",
85
+ inputSchema: {
86
+ type: "object",
87
+ properties: {
88
+ project_id: {
89
+ type: "string",
90
+ description: "확인할 프로젝트 ID (선택)",
91
+ },
92
+ },
93
+ required: [],
94
+ },
95
+ },
96
+ {
97
+ name: "vercel_projects",
98
+ description: "Vercel 프로젝트 목록과 각 프로젝트의 최신 배포 상태를 조회합니다.",
99
+ inputSchema: {
100
+ type: "object",
101
+ properties: {
102
+ limit: {
103
+ type: "number",
104
+ description: "반환할 프로젝트 수 (기본 10)",
105
+ default: 10,
106
+ },
107
+ },
108
+ required: [],
109
+ },
110
+ },
111
+ {
112
+ name: "percept_roi_summary",
113
+ description: "이 세션에서 @percept/vercel이 절감한 토큰·비용을 보고합니다.",
114
+ inputSchema: {
115
+ type: "object",
116
+ properties: {},
117
+ required: [],
118
+ },
119
+ },
120
+ ],
121
+ }));
122
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
123
+ const { name, arguments: args } = request.params;
124
+ const startTime = Date.now();
125
+ try {
126
+ if (name === "percept_roi_summary") {
127
+ return { content: [{ type: "text", text: getRoiSummary() }] };
128
+ }
129
+ // 배포 목록
130
+ if (name === "vercel_deployments") {
131
+ const a = args;
132
+ const limit = Math.min(Number(a?.limit ?? 5), 20);
133
+ const projectId = a?.project_id;
134
+ const path = projectId
135
+ ? `/v6/deployments?projectId=${projectId}&limit=${limit}`
136
+ : `/v6/deployments?limit=${limit}`;
137
+ const data = await vercelFetch(path);
138
+ const deployments = data.deployments.map((d) => ({
139
+ uid: d.uid,
140
+ project: d.name,
141
+ url: `https://${d.url}`,
142
+ state: d.state,
143
+ created_at: new Date(d.created).toISOString(),
144
+ ready_at: d.ready ? new Date(d.ready).toISOString() : null,
145
+ duration_sec: d.ready
146
+ ? Math.round((d.ready - (d.buildingAt ?? d.created)) / 1000)
147
+ : null,
148
+ commit: d.meta?.githubCommitMessage ?? null,
149
+ branch: d.meta?.githubCommitRef ?? null,
150
+ }));
151
+ trackCall(Date.now() - startTime);
152
+ return {
153
+ content: [
154
+ {
155
+ type: "text",
156
+ text: JSON.stringify({
157
+ deployments,
158
+ _percept: `${TOKENS_SAVED_PER_CALL} tokens saved vs manual`,
159
+ }, null, 2),
160
+ },
161
+ ],
162
+ };
163
+ }
164
+ // 최신 배포 상태
165
+ if (name === "vercel_latest_status") {
166
+ const a = args;
167
+ const projectId = a?.project_id;
168
+ const path = projectId
169
+ ? `/v6/deployments?projectId=${projectId}&limit=1`
170
+ : `/v6/deployments?limit=1`;
171
+ const data = await vercelFetch(path);
172
+ const d = data.deployments[0];
173
+ if (!d) {
174
+ return {
175
+ content: [{ type: "text", text: "배포 이력이 없습니다." }],
176
+ };
177
+ }
178
+ const stateEmoji = {
179
+ READY: "✅",
180
+ ERROR: "❌",
181
+ BUILDING: "🔄",
182
+ CANCELED: "⛔",
183
+ QUEUED: "⏳",
184
+ };
185
+ const status = {
186
+ state: `${stateEmoji[d.state] ?? "❓"} ${d.state}`,
187
+ project: d.name,
188
+ url: `https://${d.url}`,
189
+ deployed_at: new Date(d.created).toISOString(),
190
+ duration_sec: d.ready
191
+ ? Math.round((d.ready - (d.buildingAt ?? d.created)) / 1000)
192
+ : null,
193
+ commit: d.meta?.githubCommitMessage ?? null,
194
+ branch: d.meta?.githubCommitRef ?? null,
195
+ _percept: `${TOKENS_SAVED_PER_CALL} tokens saved vs manual`,
196
+ };
197
+ trackCall(Date.now() - startTime);
198
+ return {
199
+ content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
200
+ };
201
+ }
202
+ // 프로젝트 목록
203
+ if (name === "vercel_projects") {
204
+ const a = args;
205
+ const limit = Math.min(Number(a?.limit ?? 10), 20);
206
+ const data = await vercelFetch(`/v9/projects?limit=${limit}`);
207
+ const projects = data.projects.map((p) => ({
208
+ id: p.id,
209
+ name: p.name,
210
+ framework: p.framework ?? "unknown",
211
+ repo: p.link?.repo ?? null,
212
+ latest_deployment: p.latestDeployments?.[0]
213
+ ? {
214
+ state: p.latestDeployments[0].state,
215
+ url: `https://${p.latestDeployments[0].url}`,
216
+ created_at: new Date(p.latestDeployments[0].created).toISOString(),
217
+ }
218
+ : null,
219
+ }));
220
+ trackCall(Date.now() - startTime);
221
+ return {
222
+ content: [
223
+ {
224
+ type: "text",
225
+ text: JSON.stringify({
226
+ projects,
227
+ _percept: `${TOKENS_SAVED_PER_CALL} tokens saved vs manual`,
228
+ }, null, 2),
229
+ },
230
+ ],
231
+ };
232
+ }
233
+ throw new McpError(ErrorCode.MethodNotFound, `알 수 없는 도구: ${name}`);
234
+ }
235
+ catch (error) {
236
+ if (error instanceof McpError)
237
+ throw error;
238
+ throw new McpError(ErrorCode.InternalError, `Vercel API 오류: ${error}`);
239
+ }
240
+ });
241
+ const transport = new StdioServerTransport();
242
+ await server.connect(transport);
243
+ process.stderr.write("[percept/vercel] v0.1.0 실행 중 — perceptdot.com\n");
244
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@perceptdot/vercel",
3
+ "version": "0.1.0",
4
+ "description": "AI agent vision for Vercel deployments — with ROI measurement. perceptdot.com",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "vercel": "dist/index.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "dev": "tsc --watch",
14
+ "start": "node dist/index.js"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "vercel",
19
+ "deployment",
20
+ "roi",
21
+ "agent",
22
+ "percept",
23
+ "observability"
24
+ ],
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "typescript": "^5.0.0",
31
+ "@types/node": "^22.0.0"
32
+ }
33
+ }
package/src/index.ts ADDED
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ McpError,
8
+ ErrorCode,
9
+ } from "@modelcontextprotocol/sdk/types.js";
10
+
11
+ // ─── PerceptMetrics: ROI 측정 ─────────────────────────────────────────────────
12
+ // 측정 방법론: Vercel 배포 상태를 수동으로 확인할 때 소비하는 평균 토큰(200)을
13
+ // 도구 호출로 대체. 배포 확인은 개발 세션당 평균 5~10회 발생.
14
+ const TOKENS_SAVED_PER_CALL = 200;
15
+ const TOKEN_PRICE_PER_MILLION = 3.0;
16
+
17
+ interface PerceptMetrics {
18
+ tool_name: string;
19
+ tokens_saved_estimate: number;
20
+ time_saved_ms: number;
21
+ calls_count: number;
22
+ }
23
+
24
+ const metrics: PerceptMetrics = {
25
+ tool_name: "@percept/vercel",
26
+ tokens_saved_estimate: 0,
27
+ time_saved_ms: 0,
28
+ calls_count: 0,
29
+ };
30
+
31
+ function trackCall(duration_ms: number): void {
32
+ metrics.calls_count++;
33
+ metrics.tokens_saved_estimate += TOKENS_SAVED_PER_CALL;
34
+ metrics.time_saved_ms += duration_ms;
35
+ }
36
+
37
+ function getRoiSummary(): string {
38
+ const usd_saved =
39
+ (metrics.tokens_saved_estimate / 1_000_000) * TOKEN_PRICE_PER_MILLION;
40
+ return [
41
+ `[percept ROI — @percept/vercel]`,
42
+ `calls: ${metrics.calls_count}`,
43
+ `tokens saved: ${metrics.tokens_saved_estimate.toLocaleString()}`,
44
+ `cost saved: $${usd_saved.toFixed(4)}`,
45
+ `time saved: ~${Math.round(metrics.time_saved_ms / 60000)} min`,
46
+ ``,
47
+ `누적 절감 $${usd_saved.toFixed(4)} — perceptdot.com`,
48
+ ].join("\n");
49
+ }
50
+
51
+ // ─── 환경 변수 검증 ────────────────────────────────────────────────────────────
52
+ const VERCEL_TOKEN = process.env.VERCEL_TOKEN;
53
+ if (!VERCEL_TOKEN) {
54
+ process.stderr.write(
55
+ "[percept/vercel] ERROR: VERCEL_TOKEN 환경 변수가 필요합니다.\n" +
56
+ "발급 방법: vercel.com/account/tokens\n"
57
+ );
58
+ process.exit(1);
59
+ }
60
+
61
+ const VERCEL_TEAM_ID = process.env.VERCEL_TEAM_ID ?? "";
62
+
63
+ // ─── Vercel API 클라이언트 ─────────────────────────────────────────────────────
64
+ async function vercelFetch<T>(path: string): Promise<T> {
65
+ const url =
66
+ `https://api.vercel.com${path}` +
67
+ (VERCEL_TEAM_ID ? `?teamId=${VERCEL_TEAM_ID}` : "");
68
+
69
+ const response = await fetch(url, {
70
+ headers: {
71
+ Authorization: `Bearer ${VERCEL_TOKEN}`,
72
+ "Content-Type": "application/json",
73
+ },
74
+ });
75
+
76
+ if (!response.ok) {
77
+ const body = await response.text();
78
+ throw new Error(`Vercel API ${response.status}: ${body}`);
79
+ }
80
+
81
+ return response.json() as Promise<T>;
82
+ }
83
+
84
+ // ─── 타입 정의 ────────────────────────────────────────────────────────────────
85
+ interface VercelDeployment {
86
+ uid: string;
87
+ name: string;
88
+ url: string;
89
+ state: string;
90
+ created: number;
91
+ ready?: number;
92
+ buildingAt?: number;
93
+ source?: string;
94
+ meta?: { githubCommitMessage?: string; githubCommitRef?: string };
95
+ }
96
+
97
+ interface VercelProject {
98
+ id: string;
99
+ name: string;
100
+ framework?: string;
101
+ latestDeployments?: VercelDeployment[];
102
+ link?: { type: string; repo?: string };
103
+ }
104
+
105
+ // ─── MCP 서버 ─────────────────────────────────────────────────────────────────
106
+ const server = new Server(
107
+ { name: "@percept/vercel", version: "0.1.0" },
108
+ { capabilities: { tools: {} } }
109
+ );
110
+
111
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
112
+ tools: [
113
+ {
114
+ name: "vercel_deployments",
115
+ description:
116
+ "최근 Vercel 배포 목록을 조회합니다. 배포 상태, 시간, 커밋 메시지 포함. " +
117
+ "수동 대비 ~200 토큰 절감.",
118
+ inputSchema: {
119
+ type: "object",
120
+ properties: {
121
+ project_id: {
122
+ type: "string",
123
+ description: "특정 프로젝트 ID (선택). 없으면 전체 배포 조회.",
124
+ },
125
+ limit: {
126
+ type: "number",
127
+ description: "반환할 배포 수 (기본 5)",
128
+ default: 5,
129
+ },
130
+ },
131
+ required: [],
132
+ },
133
+ },
134
+ {
135
+ name: "vercel_latest_status",
136
+ description:
137
+ "가장 최근 배포의 상태를 확인합니다. 배포 후 즉시 성공/실패 확인에 사용. " +
138
+ "배포마다 호출 권장.",
139
+ inputSchema: {
140
+ type: "object",
141
+ properties: {
142
+ project_id: {
143
+ type: "string",
144
+ description: "확인할 프로젝트 ID (선택)",
145
+ },
146
+ },
147
+ required: [],
148
+ },
149
+ },
150
+ {
151
+ name: "vercel_projects",
152
+ description:
153
+ "Vercel 프로젝트 목록과 각 프로젝트의 최신 배포 상태를 조회합니다.",
154
+ inputSchema: {
155
+ type: "object",
156
+ properties: {
157
+ limit: {
158
+ type: "number",
159
+ description: "반환할 프로젝트 수 (기본 10)",
160
+ default: 10,
161
+ },
162
+ },
163
+ required: [],
164
+ },
165
+ },
166
+ {
167
+ name: "percept_roi_summary",
168
+ description:
169
+ "이 세션에서 @percept/vercel이 절감한 토큰·비용을 보고합니다.",
170
+ inputSchema: {
171
+ type: "object",
172
+ properties: {},
173
+ required: [],
174
+ },
175
+ },
176
+ ],
177
+ }));
178
+
179
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
180
+ const { name, arguments: args } = request.params;
181
+ const startTime = Date.now();
182
+
183
+ try {
184
+ if (name === "percept_roi_summary") {
185
+ return { content: [{ type: "text", text: getRoiSummary() }] };
186
+ }
187
+
188
+ // 배포 목록
189
+ if (name === "vercel_deployments") {
190
+ const a = args as Record<string, unknown>;
191
+ const limit = Math.min(Number(a?.limit ?? 5), 20);
192
+ const projectId = a?.project_id as string | undefined;
193
+
194
+ const path = projectId
195
+ ? `/v6/deployments?projectId=${projectId}&limit=${limit}`
196
+ : `/v6/deployments?limit=${limit}`;
197
+
198
+ const data = await vercelFetch<{ deployments: VercelDeployment[] }>(path);
199
+ const deployments = data.deployments.map((d) => ({
200
+ uid: d.uid,
201
+ project: d.name,
202
+ url: `https://${d.url}`,
203
+ state: d.state,
204
+ created_at: new Date(d.created).toISOString(),
205
+ ready_at: d.ready ? new Date(d.ready).toISOString() : null,
206
+ duration_sec: d.ready
207
+ ? Math.round((d.ready - (d.buildingAt ?? d.created)) / 1000)
208
+ : null,
209
+ commit: d.meta?.githubCommitMessage ?? null,
210
+ branch: d.meta?.githubCommitRef ?? null,
211
+ }));
212
+
213
+ trackCall(Date.now() - startTime);
214
+ return {
215
+ content: [
216
+ {
217
+ type: "text",
218
+ text: JSON.stringify(
219
+ {
220
+ deployments,
221
+ _percept: `${TOKENS_SAVED_PER_CALL} tokens saved vs manual`,
222
+ },
223
+ null,
224
+ 2
225
+ ),
226
+ },
227
+ ],
228
+ };
229
+ }
230
+
231
+ // 최신 배포 상태
232
+ if (name === "vercel_latest_status") {
233
+ const a = args as Record<string, unknown>;
234
+ const projectId = a?.project_id as string | undefined;
235
+
236
+ const path = projectId
237
+ ? `/v6/deployments?projectId=${projectId}&limit=1`
238
+ : `/v6/deployments?limit=1`;
239
+
240
+ const data = await vercelFetch<{ deployments: VercelDeployment[] }>(path);
241
+ const d = data.deployments[0];
242
+
243
+ if (!d) {
244
+ return {
245
+ content: [{ type: "text", text: "배포 이력이 없습니다." }],
246
+ };
247
+ }
248
+
249
+ const stateEmoji: Record<string, string> = {
250
+ READY: "✅",
251
+ ERROR: "❌",
252
+ BUILDING: "🔄",
253
+ CANCELED: "⛔",
254
+ QUEUED: "⏳",
255
+ };
256
+
257
+ const status = {
258
+ state: `${stateEmoji[d.state] ?? "❓"} ${d.state}`,
259
+ project: d.name,
260
+ url: `https://${d.url}`,
261
+ deployed_at: new Date(d.created).toISOString(),
262
+ duration_sec: d.ready
263
+ ? Math.round((d.ready - (d.buildingAt ?? d.created)) / 1000)
264
+ : null,
265
+ commit: d.meta?.githubCommitMessage ?? null,
266
+ branch: d.meta?.githubCommitRef ?? null,
267
+ _percept: `${TOKENS_SAVED_PER_CALL} tokens saved vs manual`,
268
+ };
269
+
270
+ trackCall(Date.now() - startTime);
271
+ return {
272
+ content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
273
+ };
274
+ }
275
+
276
+ // 프로젝트 목록
277
+ if (name === "vercel_projects") {
278
+ const a = args as Record<string, unknown>;
279
+ const limit = Math.min(Number(a?.limit ?? 10), 20);
280
+
281
+ const data = await vercelFetch<{ projects: VercelProject[] }>(
282
+ `/v9/projects?limit=${limit}`
283
+ );
284
+
285
+ const projects = data.projects.map((p) => ({
286
+ id: p.id,
287
+ name: p.name,
288
+ framework: p.framework ?? "unknown",
289
+ repo: p.link?.repo ?? null,
290
+ latest_deployment: p.latestDeployments?.[0]
291
+ ? {
292
+ state: p.latestDeployments[0].state,
293
+ url: `https://${p.latestDeployments[0].url}`,
294
+ created_at: new Date(p.latestDeployments[0].created).toISOString(),
295
+ }
296
+ : null,
297
+ }));
298
+
299
+ trackCall(Date.now() - startTime);
300
+ return {
301
+ content: [
302
+ {
303
+ type: "text",
304
+ text: JSON.stringify(
305
+ {
306
+ projects,
307
+ _percept: `${TOKENS_SAVED_PER_CALL} tokens saved vs manual`,
308
+ },
309
+ null,
310
+ 2
311
+ ),
312
+ },
313
+ ],
314
+ };
315
+ }
316
+
317
+ throw new McpError(ErrorCode.MethodNotFound, `알 수 없는 도구: ${name}`);
318
+ } catch (error) {
319
+ if (error instanceof McpError) throw error;
320
+ throw new McpError(ErrorCode.InternalError, `Vercel API 오류: ${error}`);
321
+ }
322
+ });
323
+
324
+ const transport = new StdioServerTransport();
325
+ await server.connect(transport);
326
+ process.stderr.write("[percept/vercel] v0.1.0 실행 중 — perceptdot.com\n");
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }