nworks 1.2.1 → 1.3.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/dist/mcp.js CHANGED
@@ -2,7 +2,14 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
 
5
- // src/mcp/tools.ts
5
+ // src/mcp/tools/setup.ts
6
+ import { z as z2 } from "zod";
7
+
8
+ // src/auth/config.ts
9
+ import { readFile, writeFile, mkdir, chmod, rename } from "fs/promises";
10
+ import { existsSync } from "fs";
11
+ import { homedir } from "os";
12
+ import { join } from "path";
6
13
  import { z } from "zod";
7
14
 
8
15
  // src/utils/error.ts
@@ -24,21 +31,47 @@ var ApiError = class extends Error {
24
31
  };
25
32
 
26
33
  // src/auth/config.ts
27
- import { readFile, writeFile, mkdir } from "fs/promises";
28
- import { existsSync } from "fs";
29
- import { homedir } from "os";
30
- import { join } from "path";
31
34
  function hasServiceAccountCreds(creds) {
32
35
  return !!(creds.serviceAccount && creds.privateKeyPath && creds.botId);
33
36
  }
37
+ var IS_UNIX = process.platform !== "win32";
34
38
  var CONFIG_DIR = join(homedir(), ".config", "nworks");
35
39
  var CREDENTIALS_PATH = join(CONFIG_DIR, "credentials.json");
36
40
  var TOKEN_PATH = join(CONFIG_DIR, "token.json");
37
41
  var USER_TOKEN_PATH = join(CONFIG_DIR, "user-token.json");
42
+ var PROFILE_NAME_PATTERN = /^[\w.-]+$/;
43
+ var FORBIDDEN_PROFILE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
44
+ var DOTS_ONLY = /^\.+$/;
45
+ function validateProfileName(profile) {
46
+ if (!PROFILE_NAME_PATTERN.test(profile) || FORBIDDEN_PROFILE_KEYS.has(profile) || DOTS_ONLY.test(profile)) {
47
+ throw new AuthError(
48
+ `Invalid profile name "${profile}". Use only letters, digits, ".", "_", or "-" (and not "." / ".." alone).`
49
+ );
50
+ }
51
+ }
52
+ var credentialsSchema = z.object({
53
+ clientId: z.string(),
54
+ clientSecret: z.string(),
55
+ serviceAccount: z.string().optional(),
56
+ privateKeyPath: z.string().optional(),
57
+ botId: z.string().optional(),
58
+ domainId: z.string().optional()
59
+ }).passthrough();
38
60
  async function ensureConfigDir() {
39
61
  if (!existsSync(CONFIG_DIR)) {
40
- await mkdir(CONFIG_DIR, { recursive: true });
62
+ await mkdir(CONFIG_DIR, { recursive: true, ...IS_UNIX && { mode: 448 } });
63
+ } else if (IS_UNIX) {
64
+ await chmod(CONFIG_DIR, 448);
65
+ }
66
+ }
67
+ var tempFileCounter = 0;
68
+ async function writeSecureFile(path, data) {
69
+ const tempPath = `${path}.${process.pid}.${tempFileCounter++}.tmp`;
70
+ await writeFile(tempPath, data, { encoding: "utf-8", ...IS_UNIX && { mode: 384 } });
71
+ if (IS_UNIX) {
72
+ await chmod(tempPath, 384);
41
73
  }
74
+ await rename(tempPath, path);
42
75
  }
43
76
  function getCredentialsFromEnv() {
44
77
  const clientId = process.env["NWORKS_CLIENT_ID"];
@@ -56,6 +89,7 @@ function getCredentialsFromEnv() {
56
89
  async function loadCredentials(profile = "default") {
57
90
  const envCreds = getCredentialsFromEnv();
58
91
  if (envCreds) return envCreds;
92
+ validateProfileName(profile);
59
93
  if (!existsSync(CREDENTIALS_PATH)) {
60
94
  throw new AuthError(
61
95
  "Not logged in. Run `nworks login` or set environment variables."
@@ -67,9 +101,16 @@ async function loadCredentials(profile = "default") {
67
101
  if (!creds) {
68
102
  throw new AuthError(`Profile "${profile}" not found in credentials.`);
69
103
  }
70
- return creds;
104
+ const parsed = credentialsSchema.safeParse(creds);
105
+ if (!parsed.success) {
106
+ throw new AuthError(
107
+ `Credentials for profile "${profile}" are malformed. Run \`nworks login\` to reconfigure.`
108
+ );
109
+ }
110
+ return parsed.data;
71
111
  }
72
112
  async function saveCredentials(creds, profile = "default") {
113
+ validateProfileName(profile);
73
114
  await ensureConfigDir();
74
115
  let profiles = {};
75
116
  if (existsSync(CREDENTIALS_PATH)) {
@@ -77,9 +118,10 @@ async function saveCredentials(creds, profile = "default") {
77
118
  profiles = JSON.parse(raw);
78
119
  }
79
120
  profiles[profile] = creds;
80
- await writeFile(CREDENTIALS_PATH, JSON.stringify(profiles, null, 2), "utf-8");
121
+ await writeSecureFile(CREDENTIALS_PATH, JSON.stringify(profiles, null, 2));
81
122
  }
82
123
  async function loadToken(profile = "default") {
124
+ validateProfileName(profile);
83
125
  if (!existsSync(TOKEN_PATH)) return null;
84
126
  const raw = await readFile(TOKEN_PATH, "utf-8");
85
127
  const tokens = JSON.parse(raw);
@@ -91,6 +133,7 @@ async function loadToken(profile = "default") {
91
133
  };
92
134
  }
93
135
  async function saveToken(token, profile = "default") {
136
+ validateProfileName(profile);
94
137
  await ensureConfigDir();
95
138
  let tokens = {};
96
139
  if (existsSync(TOKEN_PATH)) {
@@ -98,9 +141,10 @@ async function saveToken(token, profile = "default") {
98
141
  tokens = JSON.parse(raw);
99
142
  }
100
143
  tokens[profile] = token;
101
- await writeFile(TOKEN_PATH, JSON.stringify(tokens, null, 2), "utf-8");
144
+ await writeSecureFile(TOKEN_PATH, JSON.stringify(tokens, null, 2));
102
145
  }
103
146
  async function loadUserToken(profile = "default") {
147
+ validateProfileName(profile);
104
148
  if (!existsSync(USER_TOKEN_PATH)) return null;
105
149
  const raw = await readFile(USER_TOKEN_PATH, "utf-8");
106
150
  const tokens = JSON.parse(raw);
@@ -114,6 +158,7 @@ async function loadUserToken(profile = "default") {
114
158
  };
115
159
  }
116
160
  async function saveUserToken(token, profile = "default") {
161
+ validateProfileName(profile);
117
162
  await ensureConfigDir();
118
163
  let tokens = {};
119
164
  if (existsSync(USER_TOKEN_PATH)) {
@@ -121,35 +166,264 @@ async function saveUserToken(token, profile = "default") {
121
166
  tokens = JSON.parse(raw);
122
167
  }
123
168
  tokens[profile] = token;
124
- await writeFile(USER_TOKEN_PATH, JSON.stringify(tokens, null, 2), "utf-8");
169
+ await writeSecureFile(USER_TOKEN_PATH, JSON.stringify(tokens, null, 2));
125
170
  }
126
171
  async function clearCredentials(profile = "default") {
172
+ validateProfileName(profile);
127
173
  if (existsSync(CREDENTIALS_PATH)) {
128
174
  const raw = await readFile(CREDENTIALS_PATH, "utf-8");
129
175
  const profiles = JSON.parse(raw);
130
176
  delete profiles[profile];
131
- await writeFile(
132
- CREDENTIALS_PATH,
133
- JSON.stringify(profiles, null, 2),
134
- "utf-8"
135
- );
177
+ await writeSecureFile(CREDENTIALS_PATH, JSON.stringify(profiles, null, 2));
136
178
  }
137
179
  if (existsSync(TOKEN_PATH)) {
138
180
  const raw = await readFile(TOKEN_PATH, "utf-8");
139
181
  const tokens = JSON.parse(raw);
140
182
  delete tokens[profile];
141
- await writeFile(TOKEN_PATH, JSON.stringify(tokens, null, 2), "utf-8");
183
+ await writeSecureFile(TOKEN_PATH, JSON.stringify(tokens, null, 2));
142
184
  }
143
185
  if (existsSync(USER_TOKEN_PATH)) {
144
186
  const raw = await readFile(USER_TOKEN_PATH, "utf-8");
145
187
  const tokens = JSON.parse(raw);
146
188
  delete tokens[profile];
147
- await writeFile(USER_TOKEN_PATH, JSON.stringify(tokens, null, 2), "utf-8");
189
+ await writeSecureFile(USER_TOKEN_PATH, JSON.stringify(tokens, null, 2));
190
+ }
191
+ }
192
+
193
+ // src/utils/error-hints.ts
194
+ var REQUIRED_SCOPES = {
195
+ calendar: "calendar",
196
+ "calendar.list": "calendar.read",
197
+ "calendar.create": "calendar calendar.read",
198
+ "calendar.update": "calendar calendar.read",
199
+ "calendar.delete": "calendar calendar.read",
200
+ mail: "mail",
201
+ "mail.send": "mail",
202
+ "mail.list": "mail.read",
203
+ "mail.read": "mail.read",
204
+ task: "task",
205
+ "task.list": "task.read",
206
+ "task.create": "task user.read",
207
+ "task.update": "task user.read",
208
+ "task.delete": "task user.read",
209
+ drive: "file",
210
+ "drive.list": "file.read",
211
+ "drive.upload": "file",
212
+ "drive.download": "file.read",
213
+ board: "board",
214
+ "board.list": "board.read",
215
+ "board.posts": "board.read",
216
+ "board.read": "board.read",
217
+ "board.create": "board"
218
+ };
219
+ var ERROR_HINTS_CLI = {
220
+ FORBIDDEN: "Insufficient permission. Check the OAuth scopes in the Developer Console.",
221
+ ACCESS_DENIED: "Access denied. Make sure the Bot is added in the Admin console.",
222
+ SERVICE_ACCOUNT_NOT_ALLOWED: "This API is not available with a Service Account. Log in with User OAuth via `nworks login --user`.",
223
+ UNAUTHORIZED: "Authentication expired. Log in again with `nworks login`."
224
+ };
225
+ var ERROR_HINTS_MCP = {
226
+ FORBIDDEN: "\uAD8C\uD55C\uC774 \uBD80\uC871\uD569\uB2C8\uB2E4. Developer Console\uC5D0\uC11C OAuth Scope\uB97C \uD655\uC778\uD558\uC138\uC694.",
227
+ ACCESS_DENIED: "\uC811\uADFC\uC774 \uAC70\uBD80\uB410\uC2B5\uB2C8\uB2E4. Admin\uC5D0\uC11C Bot\uC744 \uCD94\uAC00\uD588\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.",
228
+ SERVICE_ACCOUNT_NOT_ALLOWED: "\uC11C\uBE44\uC2A4 \uACC4\uC815\uC73C\uB85C\uB294 \uC774 API\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. nworks_login_user tool\uB85C User OAuth \uB85C\uADF8\uC778\uC744 \uBA3C\uC800 \uD574\uC8FC\uC138\uC694.",
229
+ UNAUTHORIZED: "\uC778\uC99D\uC774 \uB9CC\uB8CC\uB410\uC2B5\uB2C8\uB2E4. nworks_login_user tool\uB85C \uB2E4\uC2DC \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uD558\uC138\uC694. \uADF8\uB798\uB3C4 \uC548 \uB418\uBA74 nworks_setup tool\uB85C \uC7AC\uC124\uC815 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694."
230
+ };
231
+ function cliErrorHint(err, area) {
232
+ if (err instanceof ApiError) {
233
+ const hint = ERROR_HINTS_CLI[err.code];
234
+ const scopeHint = area ? buildScopeHint(area, "cli") : "";
235
+ if (hint) {
236
+ return `[${err.code}] ${err.message}
237
+ \u2192 ${hint}${scopeHint}`;
238
+ }
239
+ return `[${err.code}] ${err.message}${scopeHint}`;
240
+ }
241
+ if (err instanceof AuthError) {
242
+ return `${err.message}
243
+ \u2192 ${ERROR_HINTS_CLI["UNAUTHORIZED"]}`;
244
+ }
245
+ return err.message;
246
+ }
247
+ function mcpErrorHint(err, area) {
248
+ if (err instanceof ApiError) {
249
+ const hint = ERROR_HINTS_MCP[err.code];
250
+ const scopeHint = area ? buildScopeHint(area, "mcp") : "";
251
+ if (hint) {
252
+ return `Error: [${err.code}] ${err.message}
253
+
254
+ [\uC548\uB0B4] ${hint}${scopeHint}`;
255
+ }
256
+ return `Error: [${err.code}] ${err.message}${scopeHint}`;
257
+ }
258
+ if (err instanceof AuthError) {
259
+ return `Error: ${err.message}
260
+
261
+ [\uC548\uB0B4] \uC778\uC99D \uC815\uBCF4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. nworks_setup tool\uC744 \uBA3C\uC800 \uD638\uCD9C\uD558\uC138\uC694 (Client ID \uD544\uC694, Client Secret\uC740 \uD658\uACBD\uBCC0\uC218 NWORKS_CLIENT_SECRET\uC5D0\uC11C \uC77D\uC74C). \uD658\uACBD\uBCC0\uC218\uAC00 \uC5C6\uC73C\uBA74 \uC0AC\uC6A9\uC790\uC5D0\uAC8C MCP \uC124\uC815 \uD30C\uC77C\uC758 env \uD544\uB4DC\uC5D0 NWORKS_CLIENT_SECRET \uCD94\uAC00\uB97C \uC548\uB0B4\uD558\uC138\uC694.`;
262
+ }
263
+ return `Error: ${err.message}`;
264
+ }
265
+ function buildScopeHint(area, mode) {
266
+ const scope = REQUIRED_SCOPES[area];
267
+ if (!scope) return "";
268
+ const scopes = scope.split(" ").join(", ");
269
+ if (mode === "cli") {
270
+ return `
271
+ \u2192 This command requires the ${scopes} scope(s). Run \`nworks login --user --scope "${scope}"\`.`;
148
272
  }
273
+ return `
274
+ \u2192 \uC774 API\uB294 ${scopes} scope\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. nworks_login_user tool\uB85C \uB85C\uADF8\uC778\uD558\uC138\uC694 (scope\uB97C \uC9C0\uC815\uD558\uC9C0 \uC54A\uC73C\uBA74 \uC804\uCCB4 \uAD8C\uD55C\uC774 \uC790\uB3D9 \uD3EC\uD568\uB429\uB2C8\uB2E4).`;
275
+ }
276
+
277
+ // src/mcp/tool-registry.ts
278
+ var TOOL_ANNOTATIONS = {
279
+ nworks_setup: { title: "Save credentials", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
280
+ nworks_message_send: { title: "Send message", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
281
+ nworks_message_members: { title: "List channel members", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
282
+ nworks_directory_members: { title: "List organization members", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
283
+ nworks_calendar_list: { title: "List calendar events", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
284
+ nworks_calendar_create: { title: "Create calendar event", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
285
+ nworks_calendar_update: { title: "Update calendar event", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
286
+ nworks_calendar_delete: { title: "Delete calendar event", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
287
+ nworks_drive_list: { title: "List drive files", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
288
+ nworks_drive_upload: { title: "Upload file to drive", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
289
+ // download는 outputDir 지정 시 로컬 파일을 쓰므로 순수 읽기가 아니다(readOnlyHint=false).
290
+ nworks_drive_download: { title: "Download drive file", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
291
+ nworks_sharedrive_list: { title: "List shared drives", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
292
+ nworks_sharedrive_files: { title: "List shared drive files", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
293
+ nworks_sharedrive_download: { title: "Download shared drive file", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
294
+ nworks_mail_send: { title: "Send mail", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
295
+ nworks_mail_list: { title: "List mail", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
296
+ nworks_mail_read: { title: "Read mail", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
297
+ // drive_download와 같은 이유로 outputDir 지정 시 로컬 파일을 쓴다(readOnlyHint=false).
298
+ nworks_mail_download_attachment: { title: "Download mail attachment", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
299
+ nworks_task_list: { title: "List tasks", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
300
+ nworks_task_create: { title: "Create task", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
301
+ nworks_task_update: { title: "Update task", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
302
+ nworks_task_delete: { title: "Delete task", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
303
+ nworks_board_list: { title: "List boards", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
304
+ nworks_board_posts: { title: "List board posts", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
305
+ nworks_board_read: { title: "Read board post", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
306
+ nworks_board_create: { title: "Create board post", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
307
+ nworks_contact_list: { title: "List contacts", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
308
+ nworks_contact_get: { title: "Get contact", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
309
+ nworks_contact_create: { title: "Create contact", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
310
+ nworks_contact_update: { title: "Update contact", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
311
+ nworks_contact_delete: { title: "Delete contact", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
312
+ nworks_contact_list_tags: { title: "List contact tags", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
313
+ nworks_login_user: { title: "Log in with User OAuth", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
314
+ nworks_whoami: { title: "Show auth status", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
315
+ nworks_logout: { title: "Log out", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
316
+ nworks_doctor: { title: "Diagnose connection", readOnlyHint: true, idempotentHint: true, openWorldHint: true }
317
+ };
318
+ function defineTool(server, name, description, inputSchema, handler) {
319
+ const annotations = TOOL_ANNOTATIONS[name];
320
+ server.registerTool(
321
+ name,
322
+ { description, inputSchema, ...annotations ? { annotations } : {} },
323
+ handler
324
+ );
325
+ }
326
+
327
+ // src/mcp/tools/setup.ts
328
+ function registerSetupTools(server) {
329
+ defineTool(
330
+ server,
331
+ "nworks_setup",
332
+ `NAVER WORKS API \uC778\uC99D \uC815\uBCF4\uB97C \uC124\uC815\uD569\uB2C8\uB2E4.
333
+
334
+ \u25A0 \uC0AC\uC804 \uC900\uBE44 (\uC0AC\uC6A9\uC790\uAC00 \uC9C1\uC811 \uD574\uC57C \uD568):
335
+ 1. https://dev.worksmobile.com \uC5D0\uC11C \uC571 \uC0DD\uC131 \uD6C4 Client ID\uC640 Client Secret\uC744 \uBC1C\uAE09\uBC1B\uC2B5\uB2C8\uB2E4.
336
+ 2. MCP \uC124\uC815 \uD30C\uC77C(\uC608: claude_desktop_config.json)\uC758 nworks \uC11C\uBC84\uC5D0 env \uD544\uB4DC\uB97C \uCD94\uAC00\uD569\uB2C8\uB2E4:
337
+ { "env": { "NWORKS_CLIENT_SECRET": "<\uBC1C\uAE09\uBC1B\uC740 Client Secret>" } }
338
+ 3. MCP \uD074\uB77C\uC774\uC5B8\uD2B8(\uC608: Claude Desktop)\uB97C \uC7AC\uC2DC\uC791\uD569\uB2C8\uB2E4.
339
+
340
+ \u25A0 \uC774 tool\uC758 \uC5ED\uD560:
341
+ - clientId(\uD544\uC218)\uC640 serviceAccount, botId, domainId(\uC120\uD0DD)\uB97C \uD30C\uB77C\uBBF8\uD130\uB85C \uBC1B\uC544 \uC800\uC7A5\uD569\uB2C8\uB2E4.
342
+ - Client Secret\uC740 \uBCF4\uC548\uC744 \uC704\uD574 \uD30C\uB77C\uBBF8\uD130\uB85C \uBC1B\uC9C0 \uC54A\uC73C\uBA70, \uD658\uACBD\uBCC0\uC218 NWORKS_CLIENT_SECRET\uC5D0\uC11C \uC790\uB3D9\uC73C\uB85C \uC77D\uC2B5\uB2C8\uB2E4.
343
+ - Service Account \uC0AC\uC6A9 \uC2DC \uD658\uACBD\uBCC0\uC218 NWORKS_PRIVATE_KEY_PATH\uB3C4 \uD544\uC694\uD569\uB2C8\uB2E4.
344
+
345
+ \u25A0 \uC124\uC815 \uD6C4 \uB2E4\uC74C \uB2E8\uACC4:
346
+ - \uCE98\uB9B0\uB354/\uBA54\uC77C/\uB4DC\uB77C\uC774\uBE0C/\uD560\uC77C/\uAC8C\uC2DC\uD310 \u2192 nworks_login_user tool\uB85C \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778 \uD544\uC694
347
+ - \uBA54\uC2DC\uC9C0/\uAD6C\uC131\uC6D0\uC870\uD68C \u2192 Service Account \uC778\uC99D (serviceAccount + botId + NWORKS_PRIVATE_KEY_PATH)
348
+
349
+ \u25A0 \uD658\uACBD\uBCC0\uC218 NWORKS_CLIENT_SECRET\uC774 \uC5C6\uC73C\uBA74 \uC774 tool\uC740 \uC2E4\uD328\uD569\uB2C8\uB2E4. \uC2E4\uD328 \uC2DC \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC704 \uC0AC\uC804 \uC900\uBE44 \uB2E8\uACC4\uB97C \uC548\uB0B4\uD558\uC138\uC694.
350
+
351
+ OAuth Redirect URI: http://localhost:9876/callback`,
352
+ {
353
+ clientId: z2.string().describe("Client ID (Developer Console\uC5D0\uC11C \uBC1C\uAE09)"),
354
+ serviceAccount: z2.string().optional().describe("Service Account ID (\uC608: xxxxx.serviceaccount@domain)"),
355
+ botId: z2.string().optional().describe("Bot ID (\uBA54\uC2DC\uC9C0 \uC804\uC1A1 \uC2DC \uD544\uC694)"),
356
+ domainId: z2.string().optional().describe("Domain ID")
357
+ },
358
+ async ({ clientId, serviceAccount, botId, domainId }) => {
359
+ try {
360
+ const resolvedSecret = process.env["NWORKS_CLIENT_SECRET"];
361
+ if (!resolvedSecret) {
362
+ return {
363
+ content: [{ type: "text", text: JSON.stringify({
364
+ error: true,
365
+ message: "\uD658\uACBD\uBCC0\uC218 NWORKS_CLIENT_SECRET\uC774 \uC124\uC815\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
366
+ userAction: [
367
+ "1. https://dev.worksmobile.com \uC5D0\uC11C \uC571\uC758 Client Secret\uC744 \uD655\uC778\uD569\uB2C8\uB2E4.",
368
+ "2. MCP \uC124\uC815 \uD30C\uC77C(\uC608: claude_desktop_config.json)\uC744 \uC5F4\uACE0, nworks \uC11C\uBC84 \uC124\uC815\uC5D0 \uB2E4\uC74C\uC744 \uCD94\uAC00\uD569\uB2C8\uB2E4:",
369
+ ' "env": { "NWORKS_CLIENT_SECRET": "<Client Secret>" }',
370
+ "3. MCP \uD074\uB77C\uC774\uC5B8\uD2B8(\uC608: Claude Desktop)\uB97C \uC7AC\uC2DC\uC791\uD569\uB2C8\uB2E4.",
371
+ "4. \uC7AC\uC2DC\uC791 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694."
372
+ ],
373
+ configExample: '{ "mcpServers": { "nworks": { "command": "npx", "args": ["-y", "nworks", "mcp"], "env": { "NWORKS_CLIENT_SECRET": "<Client Secret>" } } } }'
374
+ }) }],
375
+ isError: true
376
+ };
377
+ }
378
+ const resolvedPrivateKeyPath = process.env["NWORKS_PRIVATE_KEY_PATH"];
379
+ await saveCredentials({
380
+ clientId,
381
+ clientSecret: resolvedSecret,
382
+ serviceAccount,
383
+ privateKeyPath: resolvedPrivateKeyPath,
384
+ botId,
385
+ domainId
386
+ });
387
+ const nextSteps = [];
388
+ if (serviceAccount && resolvedPrivateKeyPath && botId) {
389
+ nextSteps.push("Service Account \uC778\uC99D \uC900\uBE44 \uC644\uB8CC \u2014 \uBD07 \uBA54\uC2DC\uC9C0 \uB4F1 \uBC14\uB85C \uC0AC\uC6A9 \uAC00\uB2A5");
390
+ } else if (serviceAccount && botId && !resolvedPrivateKeyPath) {
391
+ nextSteps.push('NWORKS_PRIVATE_KEY_PATH \uD658\uACBD\uBCC0\uC218\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. Service Account \uC778\uC99D\uC5D0\uB294 Private Key \uD30C\uC77C \uACBD\uB85C\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC548\uB0B4\uD558\uC138\uC694: (1) Developer Console(https://dev.worksmobile.com)\uC5D0\uC11C Private Key\uB97C \uB2E4\uC6B4\uB85C\uB4DC (2) MCP \uC124\uC815 \uD30C\uC77C\uC758 env\uC5D0 NWORKS_PRIVATE_KEY_PATH\uB97C \uCD94\uAC00 (\uC608: "NWORKS_PRIVATE_KEY_PATH": "C:/keys/private.key") (3) MCP \uD074\uB77C\uC774\uC5B8\uD2B8 \uC7AC\uC2DC\uC791');
392
+ }
393
+ nextSteps.push("User OAuth\uAC00 \uD544\uC694\uD55C API\uB294 nworks_login_user tool\uB85C \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uC744 \uC9C4\uD589\uD558\uC138\uC694");
394
+ const mask2 = (s) => s.length <= 4 ? "****" : `****${s.slice(-Math.min(4, Math.floor(s.length / 3)))}`;
395
+ return {
396
+ content: [
397
+ {
398
+ type: "text",
399
+ text: JSON.stringify({
400
+ success: true,
401
+ message: "\uC778\uC99D \uC815\uBCF4\uAC00 \uC800\uC7A5\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
402
+ nextSteps,
403
+ clientId: mask2(clientId),
404
+ clientSecret: `${mask2(resolvedSecret)} (\uD658\uACBD\uBCC0\uC218)`,
405
+ serviceAccount: serviceAccount ?? null,
406
+ privateKeyPath: resolvedPrivateKeyPath ? `${mask2(resolvedPrivateKeyPath)} (\uD658\uACBD\uBCC0\uC218)` : null,
407
+ botId: botId ?? null
408
+ })
409
+ }
410
+ ]
411
+ };
412
+ } catch (err) {
413
+ return {
414
+ content: [{ type: "text", text: mcpErrorHint(err) }],
415
+ isError: true
416
+ };
417
+ }
418
+ }
419
+ );
149
420
  }
150
421
 
422
+ // src/mcp/tools/message.ts
423
+ import { z as z3 } from "zod";
424
+
151
425
  // src/auth/jwt.ts
152
- import { readFile as readFile2 } from "fs/promises";
426
+ import { readFile as readFile2, stat } from "fs/promises";
153
427
  import jwt from "jsonwebtoken";
154
428
  async function createJWT(creds) {
155
429
  if (!creds.serviceAccount || !creds.privateKeyPath) {
@@ -158,6 +432,15 @@ async function createJWT(creds) {
158
432
  );
159
433
  }
160
434
  const privateKey = await readFile2(creds.privateKeyPath, "utf-8");
435
+ if (process.platform !== "win32") {
436
+ const fileStat = await stat(creds.privateKeyPath);
437
+ const mode = fileStat.mode & 511;
438
+ if (mode & 63) {
439
+ console.error(
440
+ `[nworks] Warning: Private key file has permissions ${mode.toString(8)}. Recommended: 600`
441
+ );
442
+ }
443
+ }
161
444
  const now = Math.floor(Date.now() / 1e3);
162
445
  const payload = {
163
446
  iss: creds.clientId,
@@ -194,7 +477,8 @@ async function refreshToken(profile = "default") {
194
477
  });
195
478
  if (!res.ok) {
196
479
  const text = await res.text();
197
- throw new AuthError(`Token exchange failed (${res.status}): ${text}`);
480
+ const truncated = text.length > 200 ? text.substring(0, 200) + "..." : text;
481
+ throw new AuthError(`Token exchange failed (${res.status}): ${truncated}`);
198
482
  }
199
483
  const data = await res.json();
200
484
  const expiresIn = Number(data.expires_in);
@@ -210,7 +494,7 @@ async function refreshToken(profile = "default") {
210
494
  var BASE_URL = "https://www.worksapis.com/v1.0";
211
495
  var MAX_RETRIES = 3;
212
496
  function sleep(ms) {
213
- return new Promise((resolve) => setTimeout(resolve, ms));
497
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
214
498
  }
215
499
  async function request(opts, _retryCount = 0) {
216
500
  const { method, path, body, profile = "default" } = opts;
@@ -232,7 +516,9 @@ async function request(opts, _retryCount = 0) {
232
516
  return request(opts, _retryCount + 1);
233
517
  }
234
518
  if (res.status === 429 && _retryCount < MAX_RETRIES) {
235
- const retryAfter = parseInt(res.headers.get("Retry-After") ?? "5", 10);
519
+ const MAX_RETRY_AFTER = 60;
520
+ const rawRetry = parseInt(res.headers.get("Retry-After") ?? "5", 10);
521
+ const retryAfter = Math.min(Number.isNaN(rawRetry) ? 5 : rawRetry, MAX_RETRY_AFTER);
236
522
  await sleep(retryAfter * 1e3);
237
523
  return request(opts, _retryCount + 1);
238
524
  }
@@ -257,6 +543,59 @@ async function request(opts, _retryCount = 0) {
257
543
  return JSON.parse(text);
258
544
  }
259
545
 
546
+ // src/utils/sanitize.ts
547
+ import { resolve, sep } from "path";
548
+ import { randomBytes } from "crypto";
549
+ function sanitizePathSegment(value) {
550
+ if (!value || typeof value !== "string") {
551
+ throw new Error("Path segment must be a non-empty string");
552
+ }
553
+ if (value === "me") return value;
554
+ if (/[/\\]/.test(value) || value.includes("..")) {
555
+ throw new Error(`Invalid path segment: "${value}"`);
556
+ }
557
+ return encodeURIComponent(value);
558
+ }
559
+ function sanitizeFileName(name) {
560
+ if (!name || typeof name !== "string") {
561
+ throw new Error("File name must be a non-empty string");
562
+ }
563
+ const segments = name.split(/[/\\]/).filter((s) => s.length > 0);
564
+ const base = segments[segments.length - 1] ?? "";
565
+ return base.replace(/[\r\n"]/g, "_");
566
+ }
567
+ function validateLocalPath(filePath, allowedBase) {
568
+ const resolved = resolve(filePath);
569
+ if (allowedBase) {
570
+ const resolvedBase = resolve(allowedBase);
571
+ if (resolved !== resolvedBase && !resolved.startsWith(resolvedBase + sep)) {
572
+ throw new Error(`Path "${filePath}" escapes the allowed directory "${allowedBase}"`);
573
+ }
574
+ }
575
+ return resolved;
576
+ }
577
+ function validateRedirectUrl(location, allowedHosts) {
578
+ let parsed;
579
+ try {
580
+ parsed = new URL(location);
581
+ } catch {
582
+ throw new Error(`Invalid redirect URL: "${location}"`);
583
+ }
584
+ if (parsed.protocol !== "https:") {
585
+ throw new Error(`Redirect URL must use HTTPS: "${location}"`);
586
+ }
587
+ const isAllowed = allowedHosts.some(
588
+ (host) => parsed.hostname === host || parsed.hostname.endsWith("." + host)
589
+ );
590
+ if (!isAllowed) {
591
+ throw new Error(`Redirect to untrusted host: "${parsed.hostname}"`);
592
+ }
593
+ return location;
594
+ }
595
+ function generateSecureState() {
596
+ return randomBytes(32).toString("hex");
597
+ }
598
+
260
599
  // src/api/message.ts
261
600
  function buildContent(opts) {
262
601
  const type = opts.type ?? "text";
@@ -264,7 +603,11 @@ function buildContent(opts) {
264
603
  return { type: "text", text: opts.text };
265
604
  }
266
605
  if (type === "button") {
267
- const actions = opts.actions ? JSON.parse(opts.actions) : [];
606
+ const actions = opts.actions ? (() => {
607
+ const parsed = JSON.parse(opts.actions);
608
+ if (!Array.isArray(parsed)) throw new Error("actions must be a JSON array");
609
+ return parsed;
610
+ })() : [];
268
611
  return {
269
612
  type: "button_template",
270
613
  contentText: opts.text,
@@ -272,7 +615,11 @@ function buildContent(opts) {
272
615
  };
273
616
  }
274
617
  if (type === "list") {
275
- const elements = opts.elements ? JSON.parse(opts.elements) : [];
618
+ const elements = opts.elements ? (() => {
619
+ const parsed = JSON.parse(opts.elements);
620
+ if (!Array.isArray(parsed)) throw new Error("elements must be a JSON array");
621
+ return parsed;
622
+ })() : [];
276
623
  return {
277
624
  type: "list_template",
278
625
  coverData: { text: opts.text },
@@ -294,7 +641,7 @@ async function send(opts) {
294
641
  if (opts.to) {
295
642
  const result = await request({
296
643
  method: "POST",
297
- path: `/bots/${creds.botId}/users/${opts.to}/messages`,
644
+ path: `/bots/${sanitizePathSegment(creds.botId)}/users/${sanitizePathSegment(opts.to)}/messages`,
298
645
  body,
299
646
  profile
300
647
  });
@@ -303,7 +650,7 @@ async function send(opts) {
303
650
  if (opts.channel) {
304
651
  const result = await request({
305
652
  method: "POST",
306
- path: `/bots/${creds.botId}/channels/${opts.channel}/messages`,
653
+ path: `/bots/${sanitizePathSegment(creds.botId)}/channels/${sanitizePathSegment(opts.channel)}/messages`,
307
654
  body,
308
655
  profile
309
656
  });
@@ -320,28 +667,126 @@ async function listMembers(channelId, profile = "default") {
320
667
  }
321
668
  const result = await request({
322
669
  method: "GET",
323
- path: `/bots/${creds.botId}/channels/${channelId}/members`,
670
+ path: `/bots/${sanitizePathSegment(creds.botId)}/channels/${sanitizePathSegment(channelId)}/members`,
324
671
  profile
325
672
  });
326
673
  return { members: result.members ?? [], responseMetaData: result.responseMetaData };
327
674
  }
328
675
 
676
+ // src/mcp/tools/message.ts
677
+ function registerMessageTools(server) {
678
+ defineTool(
679
+ server,
680
+ "nworks_message_send",
681
+ "NAVER WORKS \uBA54\uC2DC\uC9C0\uB97C \uC804\uC1A1\uD569\uB2C8\uB2E4 (\uBD07\uC774 \uC0AC\uC6A9\uC790 \uB610\uB294 \uCC44\uB110\uC5D0 \uBC1C\uC1A1). Service Account \uC778\uC99D \uC0AC\uC6A9 (nworks_setup\uC5D0\uC11C serviceAccount, botId \uC124\uC815 + \uD658\uACBD\uBCC0\uC218 NWORKS_PRIVATE_KEY_PATH \uD544\uC694. User OAuth \uBD88\uD544\uC694)",
682
+ {
683
+ to: z3.string().optional().describe("\uC218\uC2E0\uC790 userId (channel\uACFC \uD0DD 1). nworks_directory_members\uB85C userId \uC870\uD68C \uAC00\uB2A5"),
684
+ channel: z3.string().optional().describe("\uCC44\uB110 channelId (to\uC640 \uD0DD 1). nworks_message_members\uB85C \uCC44\uB110 \uAD6C\uC131\uC6D0 \uD655\uC778 \uAC00\uB2A5"),
685
+ text: z3.string().describe("\uBA54\uC2DC\uC9C0 \uBCF8\uBB38"),
686
+ type: z3.enum(["text", "button", "list"]).optional().describe("\uBA54\uC2DC\uC9C0 \uD0C0\uC785 (\uAE30\uBCF8: text)"),
687
+ actions: z3.string().optional().describe("\uBC84\uD2BC \uC561\uC158 JSON (type=button\uC77C \uB54C)"),
688
+ elements: z3.string().optional().describe("\uB9AC\uC2A4\uD2B8 \uD56D\uBAA9 JSON (type=list\uC77C \uB54C)")
689
+ },
690
+ async ({ to, channel, text, type, actions, elements }) => {
691
+ try {
692
+ const result = await send({
693
+ to,
694
+ channel,
695
+ text,
696
+ type,
697
+ actions,
698
+ elements
699
+ });
700
+ return {
701
+ content: [{ type: "text", text: JSON.stringify(result) }]
702
+ };
703
+ } catch (err) {
704
+ return {
705
+ content: [{ type: "text", text: mcpErrorHint(err) }],
706
+ isError: true
707
+ };
708
+ }
709
+ }
710
+ );
711
+ defineTool(
712
+ server,
713
+ "nworks_message_members",
714
+ "\uD2B9\uC815 \uCC44\uB110\uC758 \uAD6C\uC131\uC6D0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uC774 \uCC44\uB110\uC5D0 \uB204\uAC00 \uC788\uC5B4?' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. Service Account \uC778\uC99D \uC0AC\uC6A9 (nworks_setup \uD544\uC694)",
715
+ {
716
+ channel: z3.string().describe("\uCC44\uB110 channelId")
717
+ },
718
+ async ({ channel }) => {
719
+ try {
720
+ const result = await listMembers(channel);
721
+ return {
722
+ content: [{ type: "text", text: JSON.stringify(result) }]
723
+ };
724
+ } catch (err) {
725
+ return {
726
+ content: [{ type: "text", text: mcpErrorHint(err) }],
727
+ isError: true
728
+ };
729
+ }
730
+ }
731
+ );
732
+ }
733
+
734
+ // src/mcp/tools/directory.ts
735
+ import { z as z4 } from "zod";
736
+
329
737
  // src/api/directory.ts
330
- async function listUsers(profile = "default") {
738
+ async function listUsers(count = 100, cursor, profile = "default") {
739
+ const params = new URLSearchParams();
740
+ params.set("count", String(count));
741
+ if (cursor) params.set("cursor", cursor);
331
742
  const result = await request({
332
743
  method: "GET",
333
- path: "/users",
744
+ path: `/users?${params.toString()}`,
334
745
  profile
335
746
  });
336
747
  return { users: result.users ?? [], responseMetaData: result.responseMetaData };
337
748
  }
338
749
 
339
- // src/api/calendar.ts
340
- import { randomUUID } from "crypto";
341
-
342
- // src/auth/oauth-user.ts
343
- import { createServer } from "http";
344
- import { URL } from "url";
750
+ // src/mcp/tools/directory.ts
751
+ function registerDirectoryTools(server) {
752
+ defineTool(
753
+ server,
754
+ "nworks_directory_members",
755
+ "NAVER WORKS \uC870\uC9C1 \uAD6C\uC131\uC6D0(\uC9C1\uC6D0) \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uAD6C\uC131\uC6D0 \uBAA9\uB85D \uBCF4\uC5EC\uC918', '\uD300\uC6D0 \uCC3E\uC544\uC918', '\uB204\uAD6C\uD55C\uD14C \uBA54\uC2DC\uC9C0 \uBCF4\uB0BC\uC9C0 userId \uCC3E\uAE30' \uB4F1\uC5D0 \uC0AC\uC6A9. Service Account \uC778\uC99D \uC0AC\uC6A9 (nworks_setup \uD544\uC694). \uBA54\uC2DC\uC9C0 \uC804\uC1A1 \uC2DC \uC218\uC2E0\uC790 userId\uB97C \uC5EC\uAE30\uC11C \uC870\uD68C \uAC00\uB2A5. \uAD6C\uC131\uC6D0\uC774 \uB9CE\uC73C\uBA74 nextCursor\uB85C \uB2E4\uC74C \uD398\uC774\uC9C0\uB97C \uC774\uC5B4\uC11C \uC870\uD68C",
756
+ {
757
+ count: z4.number().int().min(1).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 100)"),
758
+ cursor: z4.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C (\uC774\uC804 \uC751\uB2F5\uC758 nextCursor \uAC12)")
759
+ },
760
+ async ({ count, cursor }) => {
761
+ try {
762
+ const result = await listUsers(count ?? 100, cursor);
763
+ return {
764
+ content: [{ type: "text", text: JSON.stringify({
765
+ users: result.users,
766
+ count: result.users.length,
767
+ hasMore: !!result.responseMetaData?.nextCursor,
768
+ nextCursor: result.responseMetaData?.nextCursor ?? null
769
+ }) }]
770
+ };
771
+ } catch (err) {
772
+ return {
773
+ content: [{ type: "text", text: mcpErrorHint(err) }],
774
+ isError: true
775
+ };
776
+ }
777
+ }
778
+ );
779
+ }
780
+
781
+ // src/mcp/tools/calendar.ts
782
+ import { z as z5 } from "zod";
783
+
784
+ // src/api/calendar.ts
785
+ import { randomUUID } from "crypto";
786
+
787
+ // src/auth/oauth-user.ts
788
+ import { createServer } from "http";
789
+ import { URL as URL2 } from "url";
345
790
  var AUTH_URL2 = "https://auth.worksmobile.com/oauth2/v2.0/authorize";
346
791
  var TOKEN_URL = "https://auth.worksmobile.com/oauth2/v2.0/token";
347
792
  var REDIRECT_PORT = 9876;
@@ -356,14 +801,14 @@ function buildAuthorizeUrl(clientId, scope, state) {
356
801
  });
357
802
  return `${AUTH_URL2}?${params.toString()}`;
358
803
  }
359
- function waitForAuthCode() {
360
- return new Promise((resolve, reject) => {
804
+ function waitForAuthCode(expectedState) {
805
+ return new Promise((resolve3, reject) => {
361
806
  const timeout = setTimeout(() => {
362
807
  server.close();
363
808
  reject(new AuthError("OAuth login timed out (120s). Try again."));
364
809
  }, 12e4);
365
810
  const server = createServer((req, res) => {
366
- const url = new URL(req.url ?? "/", `http://localhost:${REDIRECT_PORT}`);
811
+ const url = new URL2(req.url ?? "/", `http://localhost:${REDIRECT_PORT}`);
367
812
  if (url.pathname !== "/callback") {
368
813
  res.writeHead(404);
369
814
  res.end("Not found");
@@ -371,6 +816,12 @@ function waitForAuthCode() {
371
816
  }
372
817
  const code = url.searchParams.get("code");
373
818
  const error = url.searchParams.get("error");
819
+ const state = url.searchParams.get("state");
820
+ if (state !== expectedState) {
821
+ res.writeHead(403, { "Content-Type": "text/html; charset=utf-8" });
822
+ res.end("<h2>\uBCF4\uC548 \uC624\uB958</h2><p>state \uBD88\uC77C\uCE58. \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uB85C\uADF8\uC778\uC744 \uB2E4\uC2DC \uC9C4\uD589\uD558\uC138\uC694.</p>");
823
+ return;
824
+ }
374
825
  if (error) {
375
826
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
376
827
  res.end("<h2>\uB85C\uADF8\uC778 \uC2E4\uD328</h2><p>\uC774 \uCC3D\uC744 \uB2EB\uC544\uB3C4 \uB429\uB2C8\uB2E4.</p>");
@@ -391,9 +842,9 @@ function waitForAuthCode() {
391
842
  res.end("<h2>\uB85C\uADF8\uC778 \uC131\uACF5!</h2><p>\uC774 \uCC3D\uC744 \uB2EB\uACE0 \uD130\uBBF8\uB110\uB85C \uB3CC\uC544\uAC00\uC138\uC694.</p>");
392
843
  clearTimeout(timeout);
393
844
  server.close();
394
- resolve(code);
845
+ resolve3(code);
395
846
  });
396
- server.listen(REDIRECT_PORT, () => {
847
+ server.listen(REDIRECT_PORT, "127.0.0.1", () => {
397
848
  });
398
849
  server.on("error", (err) => {
399
850
  clearTimeout(timeout);
@@ -401,6 +852,17 @@ function waitForAuthCode() {
401
852
  });
402
853
  });
403
854
  }
855
+ async function safeErrorSummary(res) {
856
+ const text = await res.text();
857
+ try {
858
+ const body = JSON.parse(text);
859
+ if (body.error || body.error_description) {
860
+ return [body.error, body.error_description].filter(Boolean).join(": ");
861
+ }
862
+ } catch {
863
+ }
864
+ return text.length > 200 ? text.slice(0, 200) + "..." : text;
865
+ }
404
866
  async function exchangeCodeForToken(code, clientId, clientSecret) {
405
867
  const body = new URLSearchParams({
406
868
  grant_type: "authorization_code",
@@ -415,8 +877,7 @@ async function exchangeCodeForToken(code, clientId, clientSecret) {
415
877
  body: body.toString()
416
878
  });
417
879
  if (!res.ok) {
418
- const text = await res.text();
419
- throw new AuthError(`Token exchange failed (${res.status}): ${text}`);
880
+ throw new AuthError(`Token exchange failed (${res.status}): ${await safeErrorSummary(res)}`);
420
881
  }
421
882
  const data = await res.json();
422
883
  return {
@@ -440,8 +901,7 @@ async function refreshUserToken(refreshToken2, profile = "default") {
440
901
  body: body.toString()
441
902
  });
442
903
  if (!res.ok) {
443
- const text = await res.text();
444
- throw new AuthError(`Token refresh failed (${res.status}): ${text}`);
904
+ throw new AuthError(`Token refresh failed (${res.status}): ${await safeErrorSummary(res)}`);
445
905
  }
446
906
  const data = await res.json();
447
907
  return {
@@ -451,13 +911,31 @@ async function refreshUserToken(refreshToken2, profile = "default") {
451
911
  scope: data.scope
452
912
  };
453
913
  }
454
- function startOAuthCallbackServer(clientId, clientSecret) {
455
- return waitForAuthCode().then(
914
+ function startOAuthCallbackServer(clientId, clientSecret, expectedState) {
915
+ return waitForAuthCode(expectedState).then(
456
916
  (code) => exchangeCodeForToken(code, clientId, clientSecret)
457
917
  );
458
918
  }
919
+ async function revokeToken(token, clientId, clientSecret) {
920
+ try {
921
+ const res = await fetch("https://auth.worksmobile.com/oauth2/v2.0/revoke", {
922
+ method: "POST",
923
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
924
+ body: new URLSearchParams({
925
+ token,
926
+ client_id: clientId,
927
+ client_secret: clientSecret
928
+ }).toString()
929
+ });
930
+ if (!res.ok && process.env["NWORKS_VERBOSE"] === "1") {
931
+ console.error(`[nworks] Token revoke returned ${res.status}`);
932
+ }
933
+ } catch {
934
+ }
935
+ }
459
936
 
460
937
  // src/auth/token-user.ts
938
+ var inFlightRefresh = /* @__PURE__ */ new Map();
461
939
  async function getValidUserToken(profile = "default") {
462
940
  const cached = await loadUserToken(profile);
463
941
  if (!cached) {
@@ -468,22 +946,106 @@ async function getValidUserToken(profile = "default") {
468
946
  if (cached.expiresAt > Date.now() / 1e3 + 300) {
469
947
  return cached.accessToken;
470
948
  }
949
+ return refreshValidUserToken(profile);
950
+ }
951
+ function refreshValidUserToken(profile = "default") {
952
+ const existing = inFlightRefresh.get(profile);
953
+ if (existing) return existing;
954
+ const run = doRefresh(profile);
955
+ inFlightRefresh.set(profile, run);
956
+ void run.finally(() => inFlightRefresh.delete(profile)).catch(() => {
957
+ });
958
+ return run;
959
+ }
960
+ async function doRefresh(profile) {
961
+ const cached = await loadUserToken(profile);
962
+ if (!cached) {
963
+ throw new AuthError(
964
+ "User OAuth token not found. Run `nworks login --user` first."
965
+ );
966
+ }
471
967
  const refreshed = await refreshUserToken(cached.refreshToken, profile);
472
968
  await saveUserToken(refreshed, profile);
473
969
  return refreshed.accessToken;
474
970
  }
475
971
 
476
- // src/api/calendar.ts
477
- var BASE_URL2 = "https://www.worksapis.com/v1.0";
478
- async function authedFetch(url, init, profile) {
479
- const token = await getValidUserToken(profile);
480
- const headers = new Headers(init.headers);
481
- headers.set("Authorization", `Bearer ${token}`);
482
- return fetch(url, { ...init, headers });
972
+ // src/api/user-client.ts
973
+ var MAX_RETRIES2 = 3;
974
+ var MAX_RETRY_AFTER_SECONDS = 60;
975
+ var MAX_CONCURRENT_PER_HOST = 5;
976
+ function sleep2(ms) {
977
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
978
+ }
979
+ var HostSemaphore = class {
980
+ constructor(max) {
981
+ this.max = max;
982
+ }
983
+ active = 0;
984
+ waiters = [];
985
+ async acquire() {
986
+ if (this.active < this.max) {
987
+ this.active++;
988
+ return;
989
+ }
990
+ await new Promise((resolve3) => this.waiters.push(resolve3));
991
+ }
992
+ release() {
993
+ const next = this.waiters.shift();
994
+ if (next) {
995
+ next();
996
+ } else {
997
+ this.active--;
998
+ }
999
+ }
1000
+ };
1001
+ var semaphores = /* @__PURE__ */ new Map();
1002
+ function semaphoreFor(host) {
1003
+ let semaphore = semaphores.get(host);
1004
+ if (!semaphore) {
1005
+ semaphore = new HostSemaphore(MAX_CONCURRENT_PER_HOST);
1006
+ semaphores.set(host, semaphore);
1007
+ }
1008
+ return semaphore;
1009
+ }
1010
+ async function userFetch(url, init, profile) {
1011
+ let refreshedOn401 = false;
1012
+ for (let attempt = 0; ; attempt++) {
1013
+ const res = await fetchOnce(url, init, profile);
1014
+ if (res.status === 401 && !refreshedOn401) {
1015
+ refreshedOn401 = true;
1016
+ await refreshValidUserToken(profile);
1017
+ continue;
1018
+ }
1019
+ if (res.status === 429 && attempt < MAX_RETRIES2) {
1020
+ await sleep2(retryAfterMs(res));
1021
+ continue;
1022
+ }
1023
+ return res;
1024
+ }
1025
+ }
1026
+ async function fetchOnce(url, init, profile) {
1027
+ const host = new URL(url).host;
1028
+ const semaphore = semaphoreFor(host);
1029
+ await semaphore.acquire();
1030
+ try {
1031
+ const token = await getValidUserToken(profile);
1032
+ const headers = new Headers(init.headers);
1033
+ headers.set("Authorization", `Bearer ${token}`);
1034
+ return await fetch(url, { ...init, headers });
1035
+ } finally {
1036
+ semaphore.release();
1037
+ }
1038
+ }
1039
+ function retryAfterMs(res) {
1040
+ const raw = parseInt(res.headers.get("Retry-After") ?? "5", 10);
1041
+ const seconds = Math.min(Number.isNaN(raw) ? 5 : raw, MAX_RETRY_AFTER_SECONDS);
1042
+ return seconds * 1e3;
483
1043
  }
484
- async function handleError(res) {
1044
+ async function handleUserApiError(res) {
485
1045
  if (res.status === 401) {
486
- throw new AuthError("User token expired. Run `nworks login --user --scope calendar` again.");
1046
+ throw new AuthError(
1047
+ "User token expired or revoked. Run `nworks login --user` again."
1048
+ );
487
1049
  }
488
1050
  let code = "UNKNOWN";
489
1051
  let description = `HTTP ${res.status}`;
@@ -495,6 +1057,9 @@ async function handleError(res) {
495
1057
  }
496
1058
  throw new ApiError(code, description, res.status);
497
1059
  }
1060
+
1061
+ // src/api/calendar.ts
1062
+ var BASE_URL2 = "https://www.worksapis.com/v1.0";
498
1063
  function generateEventId() {
499
1064
  return `event-${randomUUID()}`;
500
1065
  }
@@ -508,12 +1073,12 @@ function normalizeDateTime(dt) {
508
1073
  async function listEvents(fromDateTime, untilDateTime, userId = "me", profile = "default") {
509
1074
  const from = encodeURIComponent(fromDateTime);
510
1075
  const until = encodeURIComponent(untilDateTime);
511
- const url = `${BASE_URL2}/users/${userId}/calendar/events?fromDateTime=${from}&untilDateTime=${until}`;
1076
+ const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events?fromDateTime=${from}&untilDateTime=${until}`;
512
1077
  if (process.env["NWORKS_VERBOSE"] === "1") {
513
1078
  console.error(`[nworks] GET ${url}`);
514
1079
  }
515
- const res = await authedFetch(url, { method: "GET" }, profile);
516
- if (!res.ok) return handleError(res);
1080
+ const res = await userFetch(url, { method: "GET" }, profile);
1081
+ if (!res.ok) return handleUserApiError(res);
517
1082
  const data = await res.json();
518
1083
  return { events: data.events ?? [] };
519
1084
  }
@@ -545,12 +1110,12 @@ async function createEvent(opts) {
545
1110
  eventComponents: [eventComponent],
546
1111
  sendNotification: opts.sendNotification ?? false
547
1112
  };
548
- const url = `${BASE_URL2}/users/${userId}/calendar/events`;
1113
+ const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events`;
549
1114
  if (process.env["NWORKS_VERBOSE"] === "1") {
550
1115
  console.error(`[nworks] POST ${url}`);
551
- console.error(`[nworks] Body: ${JSON.stringify(body, null, 2)}`);
1116
+ console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
552
1117
  }
553
- const res = await authedFetch(
1118
+ const res = await userFetch(
554
1119
  url,
555
1120
  {
556
1121
  method: "POST",
@@ -562,16 +1127,16 @@ async function createEvent(opts) {
562
1127
  if (res.status === 201) {
563
1128
  return await res.json();
564
1129
  }
565
- if (!res.ok) return handleError(res);
1130
+ if (!res.ok) return handleUserApiError(res);
566
1131
  return await res.json();
567
1132
  }
568
1133
  async function getEvent(eventId, userId = "me", profile = "default") {
569
- const url = `${BASE_URL2}/users/${userId}/calendar/events/${eventId}`;
1134
+ const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events/${sanitizePathSegment(eventId)}`;
570
1135
  if (process.env["NWORKS_VERBOSE"] === "1") {
571
1136
  console.error(`[nworks] GET ${url}`);
572
1137
  }
573
- const res = await authedFetch(url, { method: "GET" }, profile);
574
- if (!res.ok) return handleError(res);
1138
+ const res = await userFetch(url, { method: "GET" }, profile);
1139
+ if (!res.ok) return handleUserApiError(res);
575
1140
  const data = await res.json();
576
1141
  const event = data.eventComponents[0];
577
1142
  if (!event) throw new ApiError("NOT_FOUND", "Event not found", 404);
@@ -609,12 +1174,12 @@ async function updateEvent(opts) {
609
1174
  eventComponents: [eventComponent],
610
1175
  sendNotification: opts.sendNotification ?? false
611
1176
  };
612
- const url = `${BASE_URL2}/users/${userId}/calendar/events/${opts.eventId}`;
1177
+ const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events/${sanitizePathSegment(opts.eventId)}`;
613
1178
  if (process.env["NWORKS_VERBOSE"] === "1") {
614
1179
  console.error(`[nworks] PUT ${url}`);
615
- console.error(`[nworks] Body: ${JSON.stringify(body, null, 2)}`);
1180
+ console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
616
1181
  }
617
- const res = await authedFetch(
1182
+ const res = await userFetch(
618
1183
  url,
619
1184
  {
620
1185
  method: "PUT",
@@ -623,47 +1188,203 @@ async function updateEvent(opts) {
623
1188
  },
624
1189
  profile
625
1190
  );
626
- if (!res.ok) return handleError(res);
1191
+ if (!res.ok) return handleUserApiError(res);
627
1192
  }
628
1193
  async function deleteEvent(eventId, userId = "me", sendNotification = false, profile = "default") {
629
1194
  const params = new URLSearchParams();
630
1195
  params.set("sendNotification", String(sendNotification));
631
- const url = `${BASE_URL2}/users/${userId}/calendar/events/${eventId}?${params.toString()}`;
1196
+ const url = `${BASE_URL2}/users/${sanitizePathSegment(userId)}/calendar/events/${sanitizePathSegment(eventId)}?${params.toString()}`;
632
1197
  if (process.env["NWORKS_VERBOSE"] === "1") {
633
1198
  console.error(`[nworks] DELETE ${url}`);
634
1199
  }
635
- const res = await authedFetch(url, { method: "DELETE" }, profile);
1200
+ const res = await userFetch(url, { method: "DELETE" }, profile);
636
1201
  if (res.status === 204) return;
637
- if (!res.ok) return handleError(res);
1202
+ if (!res.ok) return handleUserApiError(res);
1203
+ }
1204
+
1205
+ // src/mcp/tools/calendar.ts
1206
+ function registerCalendarTools(server) {
1207
+ defineTool(
1208
+ server,
1209
+ "nworks_calendar_list",
1210
+ "\uC0AC\uC6A9\uC790\uC758 \uCE98\uB9B0\uB354 \uC77C\uC815/\uC2A4\uCF00\uC904\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uC624\uB298 \uC77C\uC815 \uC54C\uB824\uC918', '\uC774\uBC88 \uC8FC \uC2A4\uCF00\uC904 \uD655\uC778' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (calendar.read scope). \uBBF8\uB85C\uADF8\uC778 \uC2DC nworks_login_user\uB85C \uB85C\uADF8\uC778 \uD544\uC694",
1211
+ {
1212
+ fromDateTime: z5.string().describe("\uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss+09:00)"),
1213
+ untilDateTime: z5.string().describe("\uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss+09:00)"),
1214
+ userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
1215
+ },
1216
+ async ({ fromDateTime, untilDateTime, userId }) => {
1217
+ try {
1218
+ const result = await listEvents(
1219
+ fromDateTime,
1220
+ untilDateTime,
1221
+ userId ?? "me"
1222
+ );
1223
+ const events = result.events.flatMap(
1224
+ (e) => e.eventComponents.map((c) => ({
1225
+ eventId: c.eventId,
1226
+ summary: c.summary,
1227
+ start: c.start.dateTime ?? c.start.date ?? "",
1228
+ end: c.end.dateTime ?? c.end.date ?? "",
1229
+ location: c.location ?? ""
1230
+ }))
1231
+ );
1232
+ return {
1233
+ content: [{ type: "text", text: JSON.stringify({ events, count: events.length }) }]
1234
+ };
1235
+ } catch (err) {
1236
+ return {
1237
+ content: [{ type: "text", text: mcpErrorHint(err, "calendar.list") }],
1238
+ isError: true
1239
+ };
1240
+ }
1241
+ }
1242
+ );
1243
+ defineTool(
1244
+ server,
1245
+ "nworks_calendar_create",
1246
+ "\uCE98\uB9B0\uB354 \uC77C\uC815\uC744 \uC0C8\uB85C \uB9CC\uB4ED\uB2C8\uB2E4. '\uD68C\uC758 \uC7A1\uC544\uC918', '\uC77C\uC815 \uB4F1\uB85D\uD574\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (calendar + calendar.read scope)",
1247
+ {
1248
+ summary: z5.string().describe("\uC77C\uC815 \uC81C\uBAA9"),
1249
+ start: z5.string().describe("\uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
1250
+ end: z5.string().describe("\uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
1251
+ timeZone: z5.string().optional().describe("\uD0C0\uC784\uC874 (\uAE30\uBCF8: Asia/Seoul)"),
1252
+ description: z5.string().optional().describe("\uC77C\uC815 \uC124\uBA85"),
1253
+ location: z5.string().optional().describe("\uC7A5\uC18C"),
1254
+ attendees: z5.array(z5.object({ email: z5.string(), displayName: z5.string().optional() })).optional().describe("\uCC38\uC11D\uC790 \uBAA9\uB85D"),
1255
+ sendNotification: z5.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
1256
+ userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
1257
+ },
1258
+ async ({
1259
+ summary,
1260
+ start,
1261
+ end,
1262
+ timeZone,
1263
+ description,
1264
+ location,
1265
+ attendees,
1266
+ sendNotification,
1267
+ userId
1268
+ }) => {
1269
+ try {
1270
+ const result = await createEvent({
1271
+ summary,
1272
+ start,
1273
+ end,
1274
+ timeZone,
1275
+ description,
1276
+ location,
1277
+ attendees,
1278
+ sendNotification,
1279
+ userId: userId ?? "me"
1280
+ });
1281
+ const event = result.eventComponents?.[0];
1282
+ return {
1283
+ content: [{ type: "text", text: JSON.stringify({ success: true, eventId: event?.eventId, summary: event?.summary, start: event?.start, end: event?.end }) }]
1284
+ };
1285
+ } catch (err) {
1286
+ return {
1287
+ content: [{ type: "text", text: mcpErrorHint(err, "calendar.create") }],
1288
+ isError: true
1289
+ };
1290
+ }
1291
+ }
1292
+ );
1293
+ defineTool(
1294
+ server,
1295
+ "nworks_calendar_update",
1296
+ "\uAE30\uC874 \uCE98\uB9B0\uB354 \uC77C\uC815\uC744 \uC218\uC815\uD569\uB2C8\uB2E4. '\uC77C\uC815 \uC2DC\uAC04 \uBCC0\uACBD\uD574\uC918', '\uD68C\uC758 \uC81C\uBAA9 \uBC14\uAFD4\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (calendar + calendar.read scope). eventId\uB294 nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5",
1297
+ {
1298
+ eventId: z5.string().describe("\uC77C\uC815 ID (nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
1299
+ summary: z5.string().optional().describe("\uC0C8 \uC81C\uBAA9"),
1300
+ start: z5.string().optional().describe("\uC0C8 \uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
1301
+ end: z5.string().optional().describe("\uC0C8 \uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
1302
+ timeZone: z5.string().optional().describe("\uD0C0\uC784\uC874 (\uAE30\uBCF8: Asia/Seoul)"),
1303
+ description: z5.string().optional().describe("\uC0C8 \uC124\uBA85"),
1304
+ location: z5.string().optional().describe("\uC0C8 \uC7A5\uC18C"),
1305
+ sendNotification: z5.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
1306
+ userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
1307
+ },
1308
+ async ({
1309
+ eventId,
1310
+ summary,
1311
+ start,
1312
+ end,
1313
+ timeZone,
1314
+ description,
1315
+ location,
1316
+ sendNotification,
1317
+ userId
1318
+ }) => {
1319
+ try {
1320
+ await updateEvent({
1321
+ eventId,
1322
+ summary,
1323
+ start,
1324
+ end,
1325
+ timeZone,
1326
+ description,
1327
+ location,
1328
+ sendNotification,
1329
+ userId: userId ?? "me"
1330
+ });
1331
+ return {
1332
+ content: [{ type: "text", text: JSON.stringify({ success: true, eventId, message: "\uC77C\uC815\uC774 \uC218\uC815\uB418\uC5C8\uC2B5\uB2C8\uB2E4" }) }]
1333
+ };
1334
+ } catch (err) {
1335
+ return {
1336
+ content: [{ type: "text", text: mcpErrorHint(err, "calendar.update") }],
1337
+ isError: true
1338
+ };
1339
+ }
1340
+ }
1341
+ );
1342
+ defineTool(
1343
+ server,
1344
+ "nworks_calendar_delete",
1345
+ "\uCE98\uB9B0\uB354 \uC77C\uC815\uC744 \uC0AD\uC81C\uD569\uB2C8\uB2E4. '\uC77C\uC815 \uCDE8\uC18C\uD574\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (calendar + calendar.read scope). eventId\uB294 nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5",
1346
+ {
1347
+ eventId: z5.string().describe("\uC0AD\uC81C\uD560 \uC77C\uC815 ID (nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
1348
+ sendNotification: z5.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
1349
+ userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
1350
+ },
1351
+ async ({ eventId, sendNotification, userId }) => {
1352
+ try {
1353
+ await deleteEvent(
1354
+ eventId,
1355
+ userId ?? "me",
1356
+ sendNotification ?? false
1357
+ );
1358
+ return {
1359
+ content: [{ type: "text", text: JSON.stringify({ success: true, eventId, message: "\uC77C\uC815\uC774 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4" }) }]
1360
+ };
1361
+ } catch (err) {
1362
+ return {
1363
+ content: [{ type: "text", text: mcpErrorHint(err, "calendar.delete") }],
1364
+ isError: true
1365
+ };
1366
+ }
1367
+ }
1368
+ );
638
1369
  }
639
1370
 
1371
+ // src/mcp/tools/drive.ts
1372
+ import { z as z6 } from "zod";
1373
+
640
1374
  // src/api/drive.ts
641
- import { readFile as readFile3, stat } from "fs/promises";
1375
+ import { readFile as readFile3, stat as stat2 } from "fs/promises";
642
1376
  import { basename } from "path";
643
1377
  var BASE_URL3 = "https://www.worksapis.com/v1.0";
644
- async function authedFetch2(url, init, profile) {
645
- const token = await getValidUserToken(profile);
646
- const headers = new Headers(init.headers);
647
- headers.set("Authorization", `Bearer ${token}`);
648
- return fetch(url, { ...init, headers });
649
- }
650
- async function handleError2(res) {
651
- if (res.status === 401) {
652
- throw new AuthError("User token expired. Run `nworks login --user --scope file` again.");
653
- }
654
- let code = "UNKNOWN";
655
- let description = `HTTP ${res.status}`;
656
- try {
657
- const body = await res.json();
658
- code = body.code ?? code;
659
- description = body.description ?? description;
660
- } catch {
661
- }
662
- throw new ApiError(code, description, res.status);
663
- }
1378
+ var MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
1379
+ var ALLOWED_HOSTS = [
1380
+ "storage.worksmobile.com",
1381
+ "apis-storage.worksmobile.com",
1382
+ "www.worksapis.com",
1383
+ "worksapis.com"
1384
+ ];
664
1385
  async function listFiles(userId = "me", folderId, count = 20, cursor, profile = "default") {
665
- const base = `${BASE_URL3}/users/${userId}/drive/files`;
666
- const path = folderId ? `${base}/${folderId}/children` : base;
1386
+ const base = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files`;
1387
+ const path = folderId ? `${base}/${sanitizePathSegment(folderId)}/children` : base;
667
1388
  const params = new URLSearchParams();
668
1389
  params.set("count", String(count));
669
1390
  if (cursor) params.set("cursor", cursor);
@@ -671,21 +1392,25 @@ async function listFiles(userId = "me", folderId, count = 20, cursor, profile =
671
1392
  if (process.env["NWORKS_VERBOSE"] === "1") {
672
1393
  console.error(`[nworks] GET ${url}`);
673
1394
  }
674
- const res = await authedFetch2(url, { method: "GET" }, profile);
675
- if (!res.ok) return handleError2(res);
1395
+ const res = await userFetch(url, { method: "GET" }, profile);
1396
+ if (!res.ok) return handleUserApiError(res);
676
1397
  const data = await res.json();
677
1398
  return { files: data.files ?? [], responseMetaData: data.responseMetaData };
678
1399
  }
679
1400
  async function uploadFile(localPath, userId = "me", folderId, overwrite = false, profile = "default") {
680
1401
  const fileName = basename(localPath);
681
- const fileStat = await stat(localPath);
1402
+ const safeName = sanitizeFileName(fileName);
1403
+ const fileStat = await stat2(localPath);
682
1404
  const fileSize = fileStat.size;
683
- const base = `${BASE_URL3}/users/${userId}/drive/files`;
684
- const createUrl = folderId ? `${base}/${folderId}` : base;
1405
+ if (fileSize > MAX_UPLOAD_SIZE) {
1406
+ throw new ApiError("FILE_TOO_LARGE", `File size (${fileSize} bytes) exceeds maximum allowed (${MAX_UPLOAD_SIZE} bytes)`, 413);
1407
+ }
1408
+ const base = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files`;
1409
+ const createUrl = folderId ? `${base}/${sanitizePathSegment(folderId)}` : base;
685
1410
  if (process.env["NWORKS_VERBOSE"] === "1") {
686
1411
  console.error(`[nworks] POST ${createUrl} (create upload URL)`);
687
1412
  }
688
- const createRes = await authedFetch2(
1413
+ const createRes = await userFetch(
689
1414
  createUrl,
690
1415
  {
691
1416
  method: "POST",
@@ -694,13 +1419,14 @@ async function uploadFile(localPath, userId = "me", folderId, overwrite = false,
694
1419
  },
695
1420
  profile
696
1421
  );
697
- if (!createRes.ok) return handleError2(createRes);
1422
+ if (!createRes.ok) return handleUserApiError(createRes);
698
1423
  const { uploadUrl } = await createRes.json();
1424
+ validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
699
1425
  const fileBuffer = await readFile3(localPath);
700
1426
  const boundary = `----nworks${Date.now()}`;
701
1427
  const header = Buffer.from(
702
1428
  `--${boundary}\r
703
- Content-Disposition: form-data; name="Filedata"; filename="${fileName}"\r
1429
+ Content-Disposition: form-data; name="Filedata"; filename="${safeName}"\r
704
1430
  Content-Type: application/octet-stream\r
705
1431
  \r
706
1432
  `
@@ -712,7 +1438,7 @@ Content-Type: application/octet-stream\r
712
1438
  if (process.env["NWORKS_VERBOSE"] === "1") {
713
1439
  console.error(`[nworks] POST ${uploadUrl} (upload content, ${fileSize} bytes)`);
714
1440
  }
715
- const uploadRes = await authedFetch2(
1441
+ const uploadRes = await userFetch(
716
1442
  uploadUrl,
717
1443
  {
718
1444
  method: "POST",
@@ -721,17 +1447,20 @@ Content-Type: application/octet-stream\r
721
1447
  },
722
1448
  profile
723
1449
  );
724
- if (!uploadRes.ok) return handleError2(uploadRes);
1450
+ if (!uploadRes.ok) return handleUserApiError(uploadRes);
725
1451
  return await uploadRes.json();
726
1452
  }
727
1453
  async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overwrite = false, profile = "default") {
728
1454
  const fileSize = fileBuffer.length;
729
- const base = `${BASE_URL3}/users/${userId}/drive/files`;
730
- const createUrl = folderId ? `${base}/${folderId}` : base;
1455
+ if (fileSize > MAX_UPLOAD_SIZE) {
1456
+ throw new ApiError("FILE_TOO_LARGE", `File size (${fileSize} bytes) exceeds maximum allowed (${MAX_UPLOAD_SIZE} bytes)`, 413);
1457
+ }
1458
+ const base = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files`;
1459
+ const createUrl = folderId ? `${base}/${sanitizePathSegment(folderId)}` : base;
731
1460
  if (process.env["NWORKS_VERBOSE"] === "1") {
732
1461
  console.error(`[nworks] POST ${createUrl} (create upload URL for buffer)`);
733
1462
  }
734
- const createRes = await authedFetch2(
1463
+ const createRes = await userFetch(
735
1464
  createUrl,
736
1465
  {
737
1466
  method: "POST",
@@ -740,12 +1469,13 @@ async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overw
740
1469
  },
741
1470
  profile
742
1471
  );
743
- if (!createRes.ok) return handleError2(createRes);
1472
+ if (!createRes.ok) return handleUserApiError(createRes);
744
1473
  const { uploadUrl } = await createRes.json();
1474
+ validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
745
1475
  const boundary = `----nworks${Date.now()}`;
746
1476
  const header = Buffer.from(
747
1477
  `--${boundary}\r
748
- Content-Disposition: form-data; name="Filedata"; filename="${fileName}"\r
1478
+ Content-Disposition: form-data; name="Filedata"; filename="${sanitizeFileName(fileName)}"\r
749
1479
  Content-Type: application/octet-stream\r
750
1480
  \r
751
1481
  `
@@ -757,7 +1487,7 @@ Content-Type: application/octet-stream\r
757
1487
  if (process.env["NWORKS_VERBOSE"] === "1") {
758
1488
  console.error(`[nworks] POST ${uploadUrl} (upload buffer, ${fileSize} bytes)`);
759
1489
  }
760
- const uploadRes = await authedFetch2(
1490
+ const uploadRes = await userFetch(
761
1491
  uploadUrl,
762
1492
  {
763
1493
  method: "POST",
@@ -766,32 +1496,29 @@ Content-Type: application/octet-stream\r
766
1496
  },
767
1497
  profile
768
1498
  );
769
- if (!uploadRes.ok) return handleError2(uploadRes);
1499
+ if (!uploadRes.ok) return handleUserApiError(uploadRes);
770
1500
  return await uploadRes.json();
771
1501
  }
772
- async function downloadFile(fileId, userId = "me", profile = "default") {
773
- const url = `${BASE_URL3}/users/${userId}/drive/files/${fileId}/download`;
1502
+ async function downloadViaRedirect(url, profile) {
774
1503
  if (process.env["NWORKS_VERBOSE"] === "1") {
775
1504
  console.error(`[nworks] GET ${url} (get download URL)`);
776
1505
  }
777
- const redirectRes = await authedFetch2(
1506
+ const redirectRes = await userFetch(
778
1507
  url,
779
1508
  { method: "GET", redirect: "manual" },
780
1509
  profile
781
1510
  );
782
- if (redirectRes.status === 401) {
783
- throw new AuthError("User token expired. Run `nworks login --user --scope file` again.");
784
- }
785
1511
  const location = redirectRes.headers.get("location");
786
1512
  if (!location) {
787
- if (!redirectRes.ok) return handleError2(redirectRes);
1513
+ if (!redirectRes.ok) return handleUserApiError(redirectRes);
788
1514
  throw new ApiError("NO_REDIRECT", "No download URL returned", redirectRes.status);
789
1515
  }
1516
+ const safeLocation = validateRedirectUrl(location, ALLOWED_HOSTS);
790
1517
  if (process.env["NWORKS_VERBOSE"] === "1") {
791
- console.error(`[nworks] GET ${location} (download content)`);
1518
+ console.error(`[nworks] GET ${safeLocation} (download content)`);
792
1519
  }
793
- const downloadRes = await authedFetch2(location, { method: "GET" }, profile);
794
- if (!downloadRes.ok) return handleError2(downloadRes);
1520
+ const downloadRes = await userFetch(safeLocation, { method: "GET" }, profile);
1521
+ if (!downloadRes.ok) return handleUserApiError(downloadRes);
795
1522
  const arrayBuffer = await downloadRes.arrayBuffer();
796
1523
  const buffer = Buffer.from(arrayBuffer);
797
1524
  const disposition = downloadRes.headers.get("content-disposition");
@@ -804,946 +1531,233 @@ async function downloadFile(fileId, userId = "me", profile = "default") {
804
1531
  }
805
1532
  return { buffer, fileName };
806
1533
  }
807
-
808
- // src/api/mail.ts
809
- var BASE_URL4 = "https://www.worksapis.com/v1.0";
810
- async function authedFetch3(url, init, profile) {
811
- const token = await getValidUserToken(profile);
812
- const headers = new Headers(init.headers);
813
- headers.set("Authorization", `Bearer ${token}`);
814
- return fetch(url, { ...init, headers });
815
- }
816
- async function handleError3(res) {
817
- if (res.status === 401) {
818
- throw new AuthError("User token expired. Run `nworks login --user --scope mail` again.");
819
- }
820
- let code = "UNKNOWN";
821
- let description = `HTTP ${res.status}`;
822
- try {
823
- const body = await res.json();
824
- code = body.code ?? code;
825
- description = body.description ?? description;
826
- } catch {
827
- }
828
- throw new ApiError(code, description, res.status);
829
- }
830
- async function sendMail(opts) {
831
- const userId = opts.userId ?? "me";
832
- const profile = opts.profile ?? "default";
833
- const url = `${BASE_URL4}/users/${userId}/mail`;
834
- if (process.env["NWORKS_VERBOSE"] === "1") {
835
- console.error(`[nworks] POST ${url}`);
836
- }
837
- const body = {
838
- to: opts.to,
839
- subject: opts.subject
840
- };
841
- if (opts.body !== void 0) body.body = opts.body;
842
- if (opts.cc) body.cc = opts.cc;
843
- if (opts.bcc) body.bcc = opts.bcc;
844
- if (opts.contentType) body.contentType = opts.contentType;
845
- const res = await authedFetch3(
846
- url,
847
- {
848
- method: "POST",
849
- headers: { "Content-Type": "application/json" },
850
- body: JSON.stringify(body)
851
- },
852
- profile
853
- );
854
- if (res.status === 202) return;
855
- if (!res.ok) return handleError3(res);
1534
+ async function downloadFile(fileId, userId = "me", profile = "default") {
1535
+ const url = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files/${sanitizePathSegment(fileId)}/download`;
1536
+ return downloadViaRedirect(url, profile);
856
1537
  }
857
- async function listMails(folderId = 0, userId = "me", count = 30, cursor, isUnread, profile = "default") {
1538
+ async function listSharedDrives(count = 20, cursor, profile = "default") {
858
1539
  const params = new URLSearchParams();
859
1540
  params.set("count", String(count));
860
1541
  if (cursor) params.set("cursor", cursor);
861
- if (isUnread) params.set("isUnread", "true");
862
- const url = `${BASE_URL4}/users/${userId}/mail/mailfolders/${folderId}/children?${params.toString()}`;
1542
+ const url = `${BASE_URL3}/sharedrives?${params.toString()}`;
863
1543
  if (process.env["NWORKS_VERBOSE"] === "1") {
864
1544
  console.error(`[nworks] GET ${url}`);
865
1545
  }
866
- const res = await authedFetch3(url, { method: "GET" }, profile);
867
- if (!res.ok) return handleError3(res);
1546
+ const res = await userFetch(url, { method: "GET" }, profile);
1547
+ if (!res.ok) return handleUserApiError(res);
868
1548
  const data = await res.json();
869
- return {
870
- mails: data.mails ?? [],
871
- unreadCount: data.unreadCount,
872
- folderName: data.folderName,
873
- totalCount: data.totalCount,
874
- responseMetaData: data.responseMetaData
875
- };
1549
+ return { sharedDrives: data.sharedrives ?? [], responseMetaData: data.responseMetaData };
876
1550
  }
877
- async function readMail(mailId, userId = "me", profile = "default") {
878
- const url = `${BASE_URL4}/users/${userId}/mail/${mailId}`;
1551
+ async function listSharedDriveFiles(sharedriveId, folderId, count = 20, cursor, profile = "default") {
1552
+ const base = `${BASE_URL3}/sharedrives/${sanitizePathSegment(sharedriveId)}/files`;
1553
+ const path = folderId ? `${base}/${sanitizePathSegment(folderId)}/children` : base;
1554
+ const params = new URLSearchParams();
1555
+ params.set("count", String(count));
1556
+ if (cursor) params.set("cursor", cursor);
1557
+ const url = `${path}?${params.toString()}`;
879
1558
  if (process.env["NWORKS_VERBOSE"] === "1") {
880
1559
  console.error(`[nworks] GET ${url}`);
881
1560
  }
882
- const res = await authedFetch3(url, { method: "GET" }, profile);
883
- if (!res.ok) return handleError3(res);
884
- return await res.json();
1561
+ const res = await userFetch(url, { method: "GET" }, profile);
1562
+ if (!res.ok) return handleUserApiError(res);
1563
+ const data = await res.json();
1564
+ return { files: data.files ?? [], responseMetaData: data.responseMetaData };
1565
+ }
1566
+ async function downloadSharedDriveFile(sharedriveId, fileId, profile = "default") {
1567
+ const url = `${BASE_URL3}/sharedrives/${sanitizePathSegment(sharedriveId)}/files/${sanitizePathSegment(fileId)}/download`;
1568
+ return downloadViaRedirect(url, profile);
885
1569
  }
886
1570
 
887
- // src/api/task.ts
888
- var BASE_URL5 = "https://www.worksapis.com/v1.0";
889
- async function authedFetch4(url, init, profile) {
890
- const token = await getValidUserToken(profile);
891
- const headers = new Headers(init.headers);
892
- headers.set("Authorization", `Bearer ${token}`);
893
- return fetch(url, { ...init, headers });
894
- }
895
- async function handleError4(res) {
896
- if (res.status === 401) {
897
- throw new AuthError("User token expired. Run `nworks login --user --scope task` again.");
898
- }
899
- let code = "UNKNOWN";
900
- let description = `HTTP ${res.status}`;
901
- try {
902
- const body = await res.json();
903
- code = body.code ?? code;
904
- description = body.description ?? description;
905
- } catch {
906
- }
907
- throw new ApiError(code, description, res.status);
908
- }
909
- async function resolveUserId(userId, profile) {
910
- if (userId !== "me") return userId;
911
- const url = `${BASE_URL5}/users/me`;
912
- const res = await authedFetch4(url, { method: "GET" }, profile);
913
- if (!res.ok) return handleError4(res);
914
- const data = await res.json();
915
- if (process.env["NWORKS_VERBOSE"] === "1") {
916
- console.error(`[nworks] Resolved "me" \u2192 ${data.userId}`);
917
- }
918
- return data.userId;
919
- }
920
- async function listTasks(categoryId = "default", userId = "me", count = 50, cursor, status = "ALL", profile = "default") {
921
- const params = new URLSearchParams();
922
- params.set("categoryId", categoryId);
923
- params.set("count", String(count));
924
- params.set("status", status);
925
- if (cursor) params.set("cursor", cursor);
926
- const url = `${BASE_URL5}/users/${userId}/tasks?${params.toString()}`;
927
- if (process.env["NWORKS_VERBOSE"] === "1") {
928
- console.error(`[nworks] GET ${url}`);
929
- }
930
- const res = await authedFetch4(url, { method: "GET" }, profile);
931
- if (!res.ok) return handleError4(res);
932
- const data = await res.json();
933
- return { tasks: data.tasks ?? [], responseMetaData: data.responseMetaData };
934
- }
935
- async function createTask(opts) {
936
- const userId = opts.userId ?? "me";
937
- const profile = opts.profile ?? "default";
938
- const resolvedUserId = await resolveUserId(userId, profile);
939
- const assignorId = opts.assignorId ?? resolvedUserId;
940
- const assigneeIds = opts.assigneeIds ?? [resolvedUserId];
941
- const body = {
942
- assignorId,
943
- assignees: assigneeIds.map((id) => ({ assigneeId: id, status: "TODO" })),
944
- title: opts.title,
945
- content: opts.content ?? "",
946
- completionCondition: "ANY_ONE"
947
- };
948
- if (opts.dueDate) body.dueDate = opts.dueDate;
949
- if (opts.categoryId) body.categoryId = opts.categoryId;
950
- const url = `${BASE_URL5}/users/${userId}/tasks`;
951
- if (process.env["NWORKS_VERBOSE"] === "1") {
952
- console.error(`[nworks] POST ${url}`);
953
- console.error(`[nworks] Body: ${JSON.stringify(body, null, 2)}`);
954
- }
955
- const res = await authedFetch4(
956
- url,
957
- {
958
- method: "POST",
959
- headers: { "Content-Type": "application/json" },
960
- body: JSON.stringify(body)
961
- },
962
- profile
963
- );
964
- if (res.status === 201) {
965
- return await res.json();
966
- }
967
- if (!res.ok) return handleError4(res);
968
- return await res.json();
969
- }
970
- async function updateTask(opts) {
971
- const profile = opts.profile ?? "default";
972
- const body = {};
973
- if (opts.title !== void 0) body.title = opts.title;
974
- if (opts.content !== void 0) body.content = opts.content;
975
- if (opts.dueDate !== void 0) body.dueDate = opts.dueDate;
976
- const url = `${BASE_URL5}/tasks/${opts.taskId}`;
977
- if (process.env["NWORKS_VERBOSE"] === "1") {
978
- console.error(`[nworks] PATCH ${url}`);
979
- }
980
- const res = await authedFetch4(
981
- url,
982
- {
983
- method: "PATCH",
984
- headers: { "Content-Type": "application/json" },
985
- body: JSON.stringify(body)
986
- },
987
- profile
988
- );
989
- if (!res.ok) return handleError4(res);
990
- return await res.json();
991
- }
992
- async function completeTask(taskId, profile = "default") {
993
- const url = `${BASE_URL5}/tasks/${taskId}/complete`;
994
- if (process.env["NWORKS_VERBOSE"] === "1") {
995
- console.error(`[nworks] POST ${url}`);
996
- }
997
- const res = await authedFetch4(
998
- url,
999
- { method: "POST" },
1000
- profile
1001
- );
1002
- if (res.status === 204) return;
1003
- if (!res.ok) return handleError4(res);
1004
- }
1005
- async function incompleteTask(taskId, profile = "default") {
1006
- const url = `${BASE_URL5}/tasks/${taskId}/incomplete`;
1007
- if (process.env["NWORKS_VERBOSE"] === "1") {
1008
- console.error(`[nworks] POST ${url}`);
1009
- }
1010
- const res = await authedFetch4(
1011
- url,
1012
- { method: "POST" },
1013
- profile
1014
- );
1015
- if (res.status === 204) return;
1016
- if (!res.ok) return handleError4(res);
1017
- }
1018
- async function deleteTask(taskId, profile = "default") {
1019
- const url = `${BASE_URL5}/tasks/${taskId}`;
1020
- if (process.env["NWORKS_VERBOSE"] === "1") {
1021
- console.error(`[nworks] DELETE ${url}`);
1022
- }
1023
- const res = await authedFetch4(
1024
- url,
1025
- { method: "DELETE" },
1026
- profile
1027
- );
1028
- if (res.status === 204) return;
1029
- if (!res.ok) return handleError4(res);
1030
- }
1031
-
1032
- // src/api/board.ts
1033
- var BASE_URL6 = "https://www.worksapis.com/v1.0";
1034
- async function authedFetch5(url, init, profile) {
1035
- const token = await getValidUserToken(profile);
1036
- const headers = new Headers(init.headers);
1037
- headers.set("Authorization", `Bearer ${token}`);
1038
- return fetch(url, { ...init, headers });
1039
- }
1040
- async function handleError5(res) {
1041
- if (res.status === 401) {
1042
- throw new AuthError("User token expired. Run `nworks login --user --scope board` again.");
1043
- }
1044
- let code = "UNKNOWN";
1045
- let description = `HTTP ${res.status}`;
1046
- try {
1047
- const body = await res.json();
1048
- code = body.code ?? code;
1049
- description = body.description ?? description;
1050
- } catch {
1051
- }
1052
- throw new ApiError(code, description, res.status);
1053
- }
1054
- function safeParseJson(text) {
1055
- const safe = text.replace(
1056
- /"((?:board|post|domain|user)Id)"\s*:\s*(\d{16,})/g,
1057
- '"$1":"$2"'
1058
- );
1059
- return JSON.parse(safe);
1060
- }
1061
- async function listBoards(count = 20, cursor, profile = "default") {
1062
- const params = new URLSearchParams();
1063
- params.set("count", String(count));
1064
- if (cursor) params.set("cursor", cursor);
1065
- const url = `${BASE_URL6}/boards?${params.toString()}`;
1066
- if (process.env["NWORKS_VERBOSE"] === "1") {
1067
- console.error(`[nworks] GET ${url}`);
1068
- }
1069
- const res = await authedFetch5(url, { method: "GET" }, profile);
1070
- if (!res.ok) return handleError5(res);
1071
- const text = await res.text();
1072
- if (process.env["NWORKS_VERBOSE"] === "1") {
1073
- console.error(`[nworks] Response: ${text}`);
1074
- }
1075
- const data = safeParseJson(text);
1076
- return { boards: data.boards ?? [], responseMetaData: data.responseMetaData };
1077
- }
1078
- async function listPosts(boardId, count = 20, cursor, profile = "default") {
1079
- const params = new URLSearchParams();
1080
- params.set("count", String(count));
1081
- if (cursor) params.set("cursor", cursor);
1082
- const url = `${BASE_URL6}/boards/${boardId}/posts?${params.toString()}`;
1083
- if (process.env["NWORKS_VERBOSE"] === "1") {
1084
- console.error(`[nworks] GET ${url}`);
1085
- }
1086
- const res = await authedFetch5(url, { method: "GET" }, profile);
1087
- if (!res.ok) return handleError5(res);
1088
- const text = await res.text();
1089
- if (process.env["NWORKS_VERBOSE"] === "1") {
1090
- console.error(`[nworks] Response: ${text}`);
1091
- }
1092
- const data = safeParseJson(text);
1093
- return { posts: data.posts ?? [], responseMetaData: data.responseMetaData };
1094
- }
1095
- async function readPost(boardId, postId, profile = "default") {
1096
- const url = `${BASE_URL6}/boards/${boardId}/posts/${postId}`;
1097
- if (process.env["NWORKS_VERBOSE"] === "1") {
1098
- console.error(`[nworks] GET ${url}`);
1099
- }
1100
- const res = await authedFetch5(url, { method: "GET" }, profile);
1101
- if (!res.ok) return handleError5(res);
1102
- const text = await res.text();
1103
- if (process.env["NWORKS_VERBOSE"] === "1") {
1104
- console.error(`[nworks] Response: ${text}`);
1105
- }
1106
- return safeParseJson(text);
1107
- }
1108
- async function createPost(opts) {
1109
- const profile = opts.profile ?? "default";
1110
- const body = {
1111
- title: opts.title,
1112
- body: opts.body ?? ""
1113
- };
1114
- if (opts.enableComment !== void 0) body.enableComment = opts.enableComment;
1115
- if (opts.sendNotifications !== void 0) body.sendNotifications = opts.sendNotifications;
1116
- const url = `${BASE_URL6}/boards/${opts.boardId}/posts`;
1117
- if (process.env["NWORKS_VERBOSE"] === "1") {
1118
- console.error(`[nworks] POST ${url}`);
1119
- console.error(`[nworks] Body: ${JSON.stringify(body, null, 2)}`);
1120
- }
1121
- const res = await authedFetch5(
1122
- url,
1123
- {
1124
- method: "POST",
1125
- headers: { "Content-Type": "application/json" },
1126
- body: JSON.stringify(body)
1127
- },
1128
- profile
1129
- );
1130
- if (res.status === 201 || res.ok) {
1131
- const text = await res.text();
1132
- if (process.env["NWORKS_VERBOSE"] === "1") {
1133
- console.error(`[nworks] Response: ${text}`);
1134
- }
1135
- return safeParseJson(text);
1136
- }
1137
- return handleError5(res);
1138
- }
1139
-
1140
- // src/commands/doctor.ts
1141
- import { Command } from "commander";
1142
- import { existsSync as existsSync2 } from "fs";
1143
- import { readFile as readFile4 } from "fs/promises";
1144
-
1145
- // src/output/format.ts
1146
- function output(data, opts = {}) {
1147
- const useJson = opts.json || !process.stdout.isTTY;
1148
- if (useJson) {
1149
- console.log(JSON.stringify(data, null, 2));
1150
- return;
1151
- }
1152
- if (Array.isArray(data)) {
1153
- printTable(data);
1154
- } else if (typeof data === "object" && data !== null) {
1155
- const record = data;
1156
- for (const value of Object.values(record)) {
1157
- if (Array.isArray(value) && value.length > 0) {
1158
- printTable(value);
1159
- return;
1160
- }
1161
- }
1162
- for (const [key, value] of Object.entries(record)) {
1163
- console.log(` ${key}: ${String(value)}`);
1164
- }
1165
- } else {
1166
- console.log(String(data));
1167
- }
1168
- }
1169
- function printTable(rows) {
1170
- if (rows.length === 0) {
1171
- console.log(" (no data)");
1172
- return;
1173
- }
1174
- const first = rows[0];
1175
- const keys = Object.keys(first);
1176
- const widths = {};
1177
- for (const key of keys) {
1178
- widths[key] = key.length;
1179
- }
1180
- for (const row of rows) {
1181
- const record = row;
1182
- for (const key of keys) {
1183
- const len = String(record[key] ?? "").length;
1184
- if (len > (widths[key] ?? 0)) {
1185
- widths[key] = len;
1186
- }
1187
- }
1188
- }
1189
- const header = keys.map((k) => k.padEnd(widths[k] ?? 0)).join(" ");
1190
- const separator = keys.map((k) => "\u2500".repeat(widths[k] ?? 0)).join("\u2500\u2500");
1191
- console.log(` ${header}`);
1192
- console.log(` ${separator}`);
1193
- for (const row of rows) {
1194
- const record = row;
1195
- const line = keys.map((k) => String(record[k] ?? "").padEnd(widths[k] ?? 0)).join(" ");
1196
- console.log(` ${line}`);
1197
- }
1198
- }
1199
- function errorOutput(error, opts = {}) {
1200
- const payload = {
1201
- success: false,
1202
- error: { code: error.code ?? "ERROR", message: error.message, hint: error.hint }
1203
- };
1204
- if (opts.json || !process.stderr.isTTY) {
1205
- console.error(JSON.stringify(payload, null, 2));
1206
- } else {
1207
- console.error(` Error: ${error.message}`);
1208
- if (error.code) {
1209
- console.error(` Code: ${error.code}`);
1210
- }
1211
- if (error.hint) {
1212
- console.error(` \u2192 ${error.hint}`);
1213
- }
1214
- }
1215
- }
1216
-
1217
- // src/utils/error-hints.ts
1218
- var REQUIRED_SCOPES = {
1219
- calendar: "calendar",
1220
- "calendar.list": "calendar.read",
1221
- "calendar.create": "calendar calendar.read",
1222
- "calendar.update": "calendar calendar.read",
1223
- "calendar.delete": "calendar calendar.read",
1224
- mail: "mail",
1225
- "mail.send": "mail",
1226
- "mail.list": "mail.read",
1227
- "mail.read": "mail.read",
1228
- task: "task",
1229
- "task.list": "task.read",
1230
- "task.create": "task user.read",
1231
- "task.update": "task user.read",
1232
- "task.delete": "task user.read",
1233
- drive: "file",
1234
- "drive.list": "file.read",
1235
- "drive.upload": "file",
1236
- "drive.download": "file.read",
1237
- board: "board",
1238
- "board.list": "board.read",
1239
- "board.posts": "board.read",
1240
- "board.read": "board.read",
1241
- "board.create": "board"
1242
- };
1243
- var ERROR_HINTS_CLI = {
1244
- FORBIDDEN: "\uAD8C\uD55C\uC774 \uBD80\uC871\uD569\uB2C8\uB2E4. Developer Console\uC5D0\uC11C OAuth Scope\uB97C \uD655\uC778\uD558\uC138\uC694.",
1245
- ACCESS_DENIED: "\uC811\uADFC\uC774 \uAC70\uBD80\uB410\uC2B5\uB2C8\uB2E4. Admin\uC5D0\uC11C Bot\uC744 \uCD94\uAC00\uD588\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.",
1246
- SERVICE_ACCOUNT_NOT_ALLOWED: "\uC11C\uBE44\uC2A4 \uACC4\uC815\uC73C\uB85C\uB294 \uC774 API\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `nworks login --user`\uB85C User OAuth \uB85C\uADF8\uC778\uD558\uC138\uC694.",
1247
- UNAUTHORIZED: "\uC778\uC99D\uC774 \uB9CC\uB8CC\uB410\uC2B5\uB2C8\uB2E4. `nworks login`\uC73C\uB85C \uB2E4\uC2DC \uB85C\uADF8\uC778\uD558\uC138\uC694."
1248
- };
1249
- var ERROR_HINTS_MCP = {
1250
- FORBIDDEN: "\uAD8C\uD55C\uC774 \uBD80\uC871\uD569\uB2C8\uB2E4. Developer Console\uC5D0\uC11C OAuth Scope\uB97C \uD655\uC778\uD558\uC138\uC694.",
1251
- ACCESS_DENIED: "\uC811\uADFC\uC774 \uAC70\uBD80\uB410\uC2B5\uB2C8\uB2E4. Admin\uC5D0\uC11C Bot\uC744 \uCD94\uAC00\uD588\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.",
1252
- SERVICE_ACCOUNT_NOT_ALLOWED: "\uC11C\uBE44\uC2A4 \uACC4\uC815\uC73C\uB85C\uB294 \uC774 API\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. nworks_login_user tool\uB85C User OAuth \uB85C\uADF8\uC778\uC744 \uBA3C\uC800 \uD574\uC8FC\uC138\uC694.",
1253
- UNAUTHORIZED: "\uC778\uC99D\uC774 \uB9CC\uB8CC\uB410\uC2B5\uB2C8\uB2E4. nworks_login_user tool\uB85C \uB2E4\uC2DC \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uD558\uC138\uC694. \uADF8\uB798\uB3C4 \uC548 \uB418\uBA74 nworks_setup tool\uB85C \uC7AC\uC124\uC815 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694."
1254
- };
1255
- function cliErrorHint(err, area) {
1256
- if (err instanceof ApiError) {
1257
- const hint = ERROR_HINTS_CLI[err.code];
1258
- const scopeHint = area ? buildScopeHint(area, "cli") : "";
1259
- if (hint) {
1260
- return `[${err.code}] ${err.message}
1261
- \u2192 ${hint}${scopeHint}`;
1262
- }
1263
- return `[${err.code}] ${err.message}${scopeHint}`;
1264
- }
1265
- if (err instanceof AuthError) {
1266
- return `${err.message}
1267
- \u2192 ${ERROR_HINTS_CLI["UNAUTHORIZED"]}`;
1268
- }
1269
- return err.message;
1270
- }
1271
- function mcpErrorHint(err, area) {
1272
- if (err instanceof ApiError) {
1273
- const hint = ERROR_HINTS_MCP[err.code];
1274
- const scopeHint = area ? buildScopeHint(area, "mcp") : "";
1275
- if (hint) {
1276
- return `Error: [${err.code}] ${err.message}
1277
-
1278
- [\uC548\uB0B4] ${hint}${scopeHint}`;
1279
- }
1280
- return `Error: [${err.code}] ${err.message}${scopeHint}`;
1281
- }
1282
- if (err instanceof AuthError) {
1283
- return `Error: ${err.message}
1284
-
1285
- [\uC548\uB0B4] \uC778\uC99D \uC815\uBCF4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. nworks_setup tool\uC744 \uBA3C\uC800 \uD638\uCD9C\uD558\uC138\uC694 (Client ID \uD544\uC694, Client Secret\uC740 \uD658\uACBD\uBCC0\uC218 NWORKS_CLIENT_SECRET\uC5D0\uC11C \uC77D\uC74C). \uD658\uACBD\uBCC0\uC218\uAC00 \uC5C6\uC73C\uBA74 \uC0AC\uC6A9\uC790\uC5D0\uAC8C MCP \uC124\uC815 \uD30C\uC77C\uC758 env \uD544\uB4DC\uC5D0 NWORKS_CLIENT_SECRET \uCD94\uAC00\uB97C \uC548\uB0B4\uD558\uC138\uC694.`;
1286
- }
1287
- return `Error: ${err.message}`;
1288
- }
1289
- function buildScopeHint(area, mode) {
1290
- const scope = REQUIRED_SCOPES[area];
1291
- if (!scope) return "";
1292
- const scopes = scope.split(" ").join(", ");
1293
- if (mode === "cli") {
1294
- return `
1295
- \u2192 \uC774 \uBA85\uB839\uC5B4\uB294 ${scopes} scope\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. \`nworks login --user --scope "${scope}"\`\uB97C \uC2E4\uD589\uD558\uC138\uC694.`;
1296
- }
1297
- return `
1298
- \u2192 \uC774 API\uB294 ${scopes} scope\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. nworks_login_user tool\uB85C \uB85C\uADF8\uC778\uD558\uC138\uC694 (scope\uB97C \uC9C0\uC815\uD558\uC9C0 \uC54A\uC73C\uBA74 \uC804\uCCB4 \uAD8C\uD55C\uC774 \uC790\uB3D9 \uD3EC\uD568\uB429\uB2C8\uB2E4).`;
1299
- }
1300
-
1301
- // src/output/cli-error.ts
1302
- function cliError(err, opts = {}, area) {
1303
- const error = err;
1304
- if (error instanceof ApiError) {
1305
- errorOutput(
1306
- {
1307
- code: error.code,
1308
- message: error.message,
1309
- hint: cliErrorHint(err, area).split("\n").slice(1).map((l) => l.replace(/^\s+→\s*/, "")).join(" ")
1310
- },
1311
- opts
1312
- );
1313
- } else if (error instanceof AuthError) {
1314
- errorOutput(
1315
- {
1316
- code: "AUTH_ERROR",
1317
- message: error.message,
1318
- hint: "\uC778\uC99D\uC774 \uB9CC\uB8CC\uB410\uC2B5\uB2C8\uB2E4. `nworks login`\uC73C\uB85C \uB2E4\uC2DC \uB85C\uADF8\uC778\uD558\uC138\uC694."
1319
- },
1320
- opts
1321
- );
1322
- } else {
1323
- errorOutput({ message: error.message }, opts);
1324
- }
1325
- process.exitCode = 1;
1326
- }
1327
-
1328
- // src/commands/doctor.ts
1329
- async function runChecks(profile) {
1330
- const results = [];
1331
- let creds;
1332
- try {
1333
- creds = await loadCredentials(profile);
1334
- results.push({ check: "credentials", status: "OK", detail: `clientId: ${creds.clientId}` });
1335
- } catch {
1336
- results.push({ check: "credentials", status: "FAIL", detail: "\uC778\uC99D \uC815\uBCF4 \uC5C6\uC74C. CLI: `nworks login --user` / MCP: nworks_setup tool \uC0AC\uC6A9 (\uD658\uACBD\uBCC0\uC218 NWORKS_CLIENT_SECRET \uD544\uC694)" });
1337
- return results;
1338
- }
1339
- if (hasServiceAccountCreds(creds)) {
1340
- results.push({ check: "serviceAccount", status: "OK", detail: creds.serviceAccount });
1341
- } else {
1342
- results.push({ check: "serviceAccount", status: "SKIP", detail: "\uBBF8\uC124\uC815 (\uBD07 \uBA54\uC2DC\uC9C0 \uC0AC\uC6A9 \uC2DC \uD544\uC694)" });
1343
- }
1344
- if (creds.privateKeyPath) {
1345
- if (existsSync2(creds.privateKeyPath)) {
1346
- try {
1347
- await readFile4(creds.privateKeyPath, "utf-8");
1348
- results.push({ check: "privateKey", status: "OK", detail: creds.privateKeyPath });
1349
- } catch {
1350
- results.push({ check: "privateKey", status: "FAIL", detail: `\uC77D\uAE30 \uBD88\uAC00: ${creds.privateKeyPath}` });
1351
- }
1352
- } else {
1353
- results.push({ check: "privateKey", status: "FAIL", detail: `\uD30C\uC77C \uC5C6\uC74C: ${creds.privateKeyPath}` });
1354
- }
1355
- } else {
1356
- results.push({ check: "privateKey", status: "SKIP", detail: "\uBBF8\uC124\uC815" });
1357
- }
1358
- if (creds.botId) {
1359
- results.push({ check: "botId", status: "OK", detail: creds.botId });
1360
- } else {
1361
- results.push({ check: "botId", status: "SKIP", detail: "\uBBF8\uC124\uC815 (\uBA54\uC2DC\uC9C0 \uC804\uC1A1 \uC2DC \uD544\uC694)" });
1362
- }
1363
- const token = await loadToken(profile);
1364
- if (token) {
1365
- const valid = token.expiresAt > Date.now() / 1e3;
1366
- results.push({
1367
- check: "serviceToken",
1368
- status: valid ? "OK" : "EXPIRED",
1369
- detail: valid ? `\uB9CC\uB8CC: ${new Date(token.expiresAt * 1e3).toISOString()}` : `\uB9CC\uB8CC\uB428: ${new Date(token.expiresAt * 1e3).toISOString()}`
1370
- });
1371
- } else {
1372
- results.push({ check: "serviceToken", status: "SKIP", detail: "\uD1A0\uD070 \uC5C6\uC74C" });
1373
- }
1374
- const userToken = await loadUserToken(profile);
1375
- if (userToken) {
1376
- const valid = userToken.expiresAt > Date.now() / 1e3;
1377
- results.push({
1378
- check: "userOAuth",
1379
- status: valid ? "OK" : "EXPIRED",
1380
- detail: valid ? `scope: ${userToken.scope} | \uB9CC\uB8CC: ${new Date(userToken.expiresAt * 1e3).toISOString()}` : `\uB9CC\uB8CC\uB428 | scope: ${userToken.scope}`
1381
- });
1382
- } else {
1383
- results.push({ check: "userOAuth", status: "SKIP", detail: "\uD1A0\uD070 \uC5C6\uC74C. `nworks login --user` \uD544\uC694" });
1384
- }
1385
- if (hasServiceAccountCreds(creds) && token && token.expiresAt > Date.now() / 1e3) {
1386
- try {
1387
- const res = await fetch("https://www.worksapis.com/v1.0/users/me", {
1388
- headers: { Authorization: `Bearer ${token.accessToken}` }
1389
- });
1390
- if (res.ok) {
1391
- results.push({ check: "apiConnection", status: "OK", detail: "NAVER WORKS API \uC5F0\uACB0 \uC131\uACF5" });
1392
- } else {
1393
- results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
1394
- }
1395
- } catch (e) {
1396
- results.push({ check: "apiConnection", status: "FAIL", detail: `\uC5F0\uACB0 \uC2E4\uD328: ${e.message}` });
1397
- }
1398
- } else if (userToken && userToken.expiresAt > Date.now() / 1e3) {
1399
- try {
1400
- const res = await fetch("https://www.worksapis.com/v1.0/users/me", {
1401
- headers: { Authorization: `Bearer ${userToken.accessToken}` }
1402
- });
1403
- if (res.ok) {
1404
- results.push({ check: "apiConnection", status: "OK", detail: "NAVER WORKS API \uC5F0\uACB0 \uC131\uACF5" });
1405
- } else {
1406
- results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
1407
- }
1408
- } catch (e) {
1409
- results.push({ check: "apiConnection", status: "FAIL", detail: `\uC5F0\uACB0 \uC2E4\uD328: ${e.message}` });
1410
- }
1411
- } else {
1412
- results.push({ check: "apiConnection", status: "SKIP", detail: "\uC720\uD6A8\uD55C \uD1A0\uD070 \uC5C6\uC74C \u2014 API \uD14C\uC2A4\uD2B8 \uAC74\uB108\uB700" });
1413
- }
1414
- return results;
1415
- }
1416
- var doctorCommand = new Command("doctor").description("Check nworks configuration and connectivity").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
1417
- try {
1418
- const results = await runChecks(opts.profile);
1419
- if (opts.json || !process.stdout.isTTY) {
1420
- output(results, opts);
1421
- } else {
1422
- console.log("\n nworks doctor\n");
1423
- for (const r of results) {
1424
- const icon = r.status === "OK" ? "\u2705" : r.status === "SKIP" ? "\u2796" : "\u274C";
1425
- console.log(` ${icon} ${r.check.padEnd(16)} ${r.detail}`);
1426
- }
1427
- console.log();
1428
- }
1429
- } catch (err) {
1430
- cliError(err, opts);
1431
- }
1432
- });
1433
-
1434
- // src/mcp/tools.ts
1435
- function registerTools(server) {
1436
- server.tool(
1437
- "nworks_setup",
1438
- `NAVER WORKS API \uC778\uC99D \uC815\uBCF4\uB97C \uC124\uC815\uD569\uB2C8\uB2E4.
1439
-
1440
- \u25A0 \uC0AC\uC804 \uC900\uBE44 (\uC0AC\uC6A9\uC790\uAC00 \uC9C1\uC811 \uD574\uC57C \uD568):
1441
- 1. https://dev.worksmobile.com \uC5D0\uC11C \uC571 \uC0DD\uC131 \uD6C4 Client ID\uC640 Client Secret\uC744 \uBC1C\uAE09\uBC1B\uC2B5\uB2C8\uB2E4.
1442
- 2. MCP \uC124\uC815 \uD30C\uC77C(\uC608: claude_desktop_config.json)\uC758 nworks \uC11C\uBC84\uC5D0 env \uD544\uB4DC\uB97C \uCD94\uAC00\uD569\uB2C8\uB2E4:
1443
- { "env": { "NWORKS_CLIENT_SECRET": "<\uBC1C\uAE09\uBC1B\uC740 Client Secret>" } }
1444
- 3. MCP \uD074\uB77C\uC774\uC5B8\uD2B8(\uC608: Claude Desktop)\uB97C \uC7AC\uC2DC\uC791\uD569\uB2C8\uB2E4.
1445
-
1446
- \u25A0 \uC774 tool\uC758 \uC5ED\uD560:
1447
- - clientId(\uD544\uC218)\uC640 serviceAccount, botId, domainId(\uC120\uD0DD)\uB97C \uD30C\uB77C\uBBF8\uD130\uB85C \uBC1B\uC544 \uC800\uC7A5\uD569\uB2C8\uB2E4.
1448
- - Client Secret\uC740 \uBCF4\uC548\uC744 \uC704\uD574 \uD30C\uB77C\uBBF8\uD130\uB85C \uBC1B\uC9C0 \uC54A\uC73C\uBA70, \uD658\uACBD\uBCC0\uC218 NWORKS_CLIENT_SECRET\uC5D0\uC11C \uC790\uB3D9\uC73C\uB85C \uC77D\uC2B5\uB2C8\uB2E4.
1449
- - Service Account \uC0AC\uC6A9 \uC2DC \uD658\uACBD\uBCC0\uC218 NWORKS_PRIVATE_KEY_PATH\uB3C4 \uD544\uC694\uD569\uB2C8\uB2E4.
1450
-
1451
- \u25A0 \uC124\uC815 \uD6C4 \uB2E4\uC74C \uB2E8\uACC4:
1452
- - \uCE98\uB9B0\uB354/\uBA54\uC77C/\uB4DC\uB77C\uC774\uBE0C/\uD560\uC77C/\uAC8C\uC2DC\uD310 \u2192 nworks_login_user tool\uB85C \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778 \uD544\uC694
1453
- - \uBA54\uC2DC\uC9C0/\uAD6C\uC131\uC6D0\uC870\uD68C \u2192 Service Account \uC778\uC99D (serviceAccount + botId + NWORKS_PRIVATE_KEY_PATH)
1454
-
1455
- \u25A0 \uD658\uACBD\uBCC0\uC218 NWORKS_CLIENT_SECRET\uC774 \uC5C6\uC73C\uBA74 \uC774 tool\uC740 \uC2E4\uD328\uD569\uB2C8\uB2E4. \uC2E4\uD328 \uC2DC \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC704 \uC0AC\uC804 \uC900\uBE44 \uB2E8\uACC4\uB97C \uC548\uB0B4\uD558\uC138\uC694.
1456
-
1457
- OAuth Redirect URI: http://localhost:9876/callback`,
1458
- {
1459
- clientId: z.string().describe("Client ID (Developer Console\uC5D0\uC11C \uBC1C\uAE09)"),
1460
- serviceAccount: z.string().optional().describe("Service Account ID (\uC608: xxxxx.serviceaccount@domain)"),
1461
- botId: z.string().optional().describe("Bot ID (\uBA54\uC2DC\uC9C0 \uC804\uC1A1 \uC2DC \uD544\uC694)"),
1462
- domainId: z.string().optional().describe("Domain ID")
1463
- },
1464
- async ({ clientId, serviceAccount, botId, domainId }) => {
1465
- try {
1466
- const resolvedSecret = process.env["NWORKS_CLIENT_SECRET"];
1467
- if (!resolvedSecret) {
1468
- return {
1469
- content: [{ type: "text", text: JSON.stringify({
1470
- error: true,
1471
- message: "\uD658\uACBD\uBCC0\uC218 NWORKS_CLIENT_SECRET\uC774 \uC124\uC815\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.",
1472
- userAction: [
1473
- "1. https://dev.worksmobile.com \uC5D0\uC11C \uC571\uC758 Client Secret\uC744 \uD655\uC778\uD569\uB2C8\uB2E4.",
1474
- "2. MCP \uC124\uC815 \uD30C\uC77C(\uC608: claude_desktop_config.json)\uC744 \uC5F4\uACE0, nworks \uC11C\uBC84 \uC124\uC815\uC5D0 \uB2E4\uC74C\uC744 \uCD94\uAC00\uD569\uB2C8\uB2E4:",
1475
- ' "env": { "NWORKS_CLIENT_SECRET": "<Client Secret>" }',
1476
- "3. MCP \uD074\uB77C\uC774\uC5B8\uD2B8(\uC608: Claude Desktop)\uB97C \uC7AC\uC2DC\uC791\uD569\uB2C8\uB2E4.",
1477
- "4. \uC7AC\uC2DC\uC791 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694."
1478
- ],
1479
- configExample: '{ "mcpServers": { "nworks": { "command": "npx", "args": ["-y", "nworks", "mcp"], "env": { "NWORKS_CLIENT_SECRET": "<Client Secret>" } } } }'
1480
- }) }],
1481
- isError: true
1482
- };
1483
- }
1484
- const resolvedPrivateKeyPath = process.env["NWORKS_PRIVATE_KEY_PATH"];
1485
- await saveCredentials({
1486
- clientId,
1487
- clientSecret: resolvedSecret,
1488
- serviceAccount,
1489
- privateKeyPath: resolvedPrivateKeyPath,
1490
- botId,
1491
- domainId
1492
- });
1493
- const nextSteps = [];
1494
- if (serviceAccount && resolvedPrivateKeyPath && botId) {
1495
- nextSteps.push("Service Account \uC778\uC99D \uC900\uBE44 \uC644\uB8CC \u2014 \uBD07 \uBA54\uC2DC\uC9C0 \uB4F1 \uBC14\uB85C \uC0AC\uC6A9 \uAC00\uB2A5");
1496
- } else if (serviceAccount && botId && !resolvedPrivateKeyPath) {
1497
- nextSteps.push('NWORKS_PRIVATE_KEY_PATH \uD658\uACBD\uBCC0\uC218\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. Service Account \uC778\uC99D\uC5D0\uB294 Private Key \uD30C\uC77C \uACBD\uB85C\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4. \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uC548\uB0B4\uD558\uC138\uC694: (1) Developer Console(https://dev.worksmobile.com)\uC5D0\uC11C Private Key\uB97C \uB2E4\uC6B4\uB85C\uB4DC (2) MCP \uC124\uC815 \uD30C\uC77C\uC758 env\uC5D0 NWORKS_PRIVATE_KEY_PATH\uB97C \uCD94\uAC00 (\uC608: "NWORKS_PRIVATE_KEY_PATH": "C:/keys/private.key") (3) MCP \uD074\uB77C\uC774\uC5B8\uD2B8 \uC7AC\uC2DC\uC791');
1498
- }
1499
- nextSteps.push("User OAuth\uAC00 \uD544\uC694\uD55C API\uB294 nworks_login_user tool\uB85C \uBE0C\uB77C\uC6B0\uC800 \uB85C\uADF8\uC778\uC744 \uC9C4\uD589\uD558\uC138\uC694");
1500
- const mask = (s) => s.length <= 4 ? "****" : `****${s.slice(-Math.min(4, Math.floor(s.length / 3)))}`;
1501
- return {
1502
- content: [
1503
- {
1504
- type: "text",
1505
- text: JSON.stringify({
1506
- success: true,
1507
- message: "\uC778\uC99D \uC815\uBCF4\uAC00 \uC800\uC7A5\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
1508
- nextSteps,
1509
- clientId,
1510
- clientSecret: `${mask(resolvedSecret)} (\uD658\uACBD\uBCC0\uC218)`,
1511
- serviceAccount: serviceAccount ?? null,
1512
- privateKeyPath: resolvedPrivateKeyPath ? `${mask(resolvedPrivateKeyPath)} (\uD658\uACBD\uBCC0\uC218)` : null,
1513
- botId: botId ?? null
1514
- })
1515
- }
1516
- ]
1517
- };
1518
- } catch (err) {
1519
- return {
1520
- content: [{ type: "text", text: mcpErrorHint(err) }],
1521
- isError: true
1522
- };
1523
- }
1524
- }
1525
- );
1526
- server.tool(
1527
- "nworks_message_send",
1528
- "NAVER WORKS \uBA54\uC2DC\uC9C0\uB97C \uC804\uC1A1\uD569\uB2C8\uB2E4 (\uBD07\uC774 \uC0AC\uC6A9\uC790 \uB610\uB294 \uCC44\uB110\uC5D0 \uBC1C\uC1A1). Service Account \uC778\uC99D \uC0AC\uC6A9 (nworks_setup\uC5D0\uC11C serviceAccount, botId \uC124\uC815 + \uD658\uACBD\uBCC0\uC218 NWORKS_PRIVATE_KEY_PATH \uD544\uC694. User OAuth \uBD88\uD544\uC694)",
1571
+ // src/mcp/tools/drive.ts
1572
+ function registerDriveTools(server) {
1573
+ defineTool(
1574
+ server,
1575
+ "nworks_drive_list",
1576
+ "NAVER WORKS \uB4DC\uB77C\uC774\uBE0C\uC758 \uD30C\uC77C/\uD3F4\uB354 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uB4DC\uB77C\uC774\uBE0C \uD30C\uC77C \uBCF4\uC5EC\uC918', '\uB0B4 \uD30C\uC77C \uBAA9\uB85D' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (file.read scope)",
1529
1577
  {
1530
- to: z.string().optional().describe("\uC218\uC2E0\uC790 userId (channel\uACFC \uD0DD 1). nworks_directory_members\uB85C userId \uC870\uD68C \uAC00\uB2A5"),
1531
- channel: z.string().optional().describe("\uCC44\uB110 channelId (to\uC640 \uD0DD 1). nworks_message_members\uB85C \uCC44\uB110 \uAD6C\uC131\uC6D0 \uD655\uC778 \uAC00\uB2A5"),
1532
- text: z.string().describe("\uBA54\uC2DC\uC9C0 \uBCF8\uBB38"),
1533
- type: z.enum(["text", "button", "list"]).optional().describe("\uBA54\uC2DC\uC9C0 \uD0C0\uC785 (\uAE30\uBCF8: text)"),
1534
- actions: z.string().optional().describe("\uBC84\uD2BC \uC561\uC158 JSON (type=button\uC77C \uB54C)"),
1535
- elements: z.string().optional().describe("\uB9AC\uC2A4\uD2B8 \uD56D\uBAA9 JSON (type=list\uC77C \uB54C)")
1578
+ userId: z6.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
1579
+ folderId: z6.string().optional().describe("\uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
1580
+ count: z6.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 200)"),
1581
+ cursor: z6.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
1536
1582
  },
1537
- async ({ to, channel, text, type, actions, elements }) => {
1583
+ async ({ userId, folderId, count, cursor }) => {
1538
1584
  try {
1539
- const result = await send({
1540
- to,
1541
- channel,
1542
- text,
1543
- type,
1544
- actions,
1545
- elements
1546
- });
1585
+ const result = await listFiles(
1586
+ userId ?? "me",
1587
+ folderId,
1588
+ count ?? 20,
1589
+ cursor
1590
+ );
1591
+ const files = result.files.map((f) => ({
1592
+ fileId: f.fileId,
1593
+ name: f.fileName,
1594
+ type: f.fileType,
1595
+ size: f.fileSize,
1596
+ modified: f.modifiedTime,
1597
+ path: f.filePath
1598
+ }));
1547
1599
  return {
1548
- content: [{ type: "text", text: JSON.stringify(result) }]
1600
+ content: [{ type: "text", text: JSON.stringify({ files, count: files.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
1549
1601
  };
1550
1602
  } catch (err) {
1551
1603
  return {
1552
- content: [{ type: "text", text: mcpErrorHint(err) }],
1604
+ content: [{ type: "text", text: mcpErrorHint(err, "drive.list") }],
1553
1605
  isError: true
1554
1606
  };
1555
1607
  }
1556
1608
  }
1557
1609
  );
1558
- server.tool(
1559
- "nworks_message_members",
1560
- "\uD2B9\uC815 \uCC44\uB110\uC758 \uAD6C\uC131\uC6D0 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uC774 \uCC44\uB110\uC5D0 \uB204\uAC00 \uC788\uC5B4?' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. Service Account \uC778\uC99D \uC0AC\uC6A9 (nworks_setup \uD544\uC694)",
1610
+ defineTool(
1611
+ server,
1612
+ "nworks_drive_upload",
1613
+ "\uD30C\uC77C\uC744 \uB4DC\uB77C\uC774\uBE0C\uC5D0 \uC5C5\uB85C\uB4DC\uD569\uB2C8\uB2E4 (User OAuth file scope \uD544\uC694). content(base64)\uC640 fileName\uC73C\uB85C \uC804\uB2EC\uD558\uAC70\uB098, filePath\uB85C \uB85C\uCEEC \uD30C\uC77C \uACBD\uB85C\uB97C \uC9C0\uC815\uD569\uB2C8\uB2E4. MCP \uD074\uB77C\uC774\uC5B8\uD2B8\uC5D0\uC11C\uB294 content+fileName \uBC29\uC2DD\uC744 \uAD8C\uC7A5\uD569\uB2C8\uB2E4.",
1561
1614
  {
1562
- channel: z.string().describe("\uCC44\uB110 channelId")
1615
+ content: z6.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uD30C\uC77C \uB0B4\uC6A9 (base64 \uC778\uCF54\uB529). filePath \uB300\uC2E0 \uC0AC\uC6A9"),
1616
+ fileName: z6.string().optional().describe("\uD30C\uC77C\uBA85 (content \uC0AC\uC6A9 \uC2DC \uD544\uC218)"),
1617
+ filePath: z6.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uB85C\uCEEC \uD30C\uC77C \uACBD\uB85C (content \uB300\uC2E0 \uC0AC\uC6A9, \uB85C\uCEEC \uD658\uACBD\uC5D0\uC11C\uB9CC \uB3D9\uC791)"),
1618
+ userId: z6.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
1619
+ folderId: z6.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
1620
+ overwrite: z6.boolean().optional().describe("\uB3D9\uC77C \uD30C\uC77C\uBA85 \uB36E\uC5B4\uC4F0\uAE30 (\uAE30\uBCF8: false)")
1563
1621
  },
1564
- async ({ channel }) => {
1565
- try {
1566
- const result = await listMembers(channel);
1567
- return {
1568
- content: [{ type: "text", text: JSON.stringify(result) }]
1569
- };
1570
- } catch (err) {
1571
- return {
1572
- content: [{ type: "text", text: mcpErrorHint(err) }],
1573
- isError: true
1574
- };
1575
- }
1576
- }
1577
- );
1578
- server.tool(
1579
- "nworks_directory_members",
1580
- "NAVER WORKS \uC870\uC9C1 \uAD6C\uC131\uC6D0(\uC9C1\uC6D0) \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uAD6C\uC131\uC6D0 \uBAA9\uB85D \uBCF4\uC5EC\uC918', '\uD300\uC6D0 \uCC3E\uC544\uC918', '\uB204\uAD6C\uD55C\uD14C \uBA54\uC2DC\uC9C0 \uBCF4\uB0BC\uC9C0 userId \uCC3E\uAE30' \uB4F1\uC5D0 \uC0AC\uC6A9. Service Account \uC778\uC99D \uC0AC\uC6A9 (nworks_setup \uD544\uC694). \uBA54\uC2DC\uC9C0 \uC804\uC1A1 \uC2DC \uC218\uC2E0\uC790 userId\uB97C \uC5EC\uAE30\uC11C \uC870\uD68C \uAC00\uB2A5",
1581
- {},
1582
- async () => {
1622
+ async ({ content, fileName, filePath, userId, folderId, overwrite }) => {
1583
1623
  try {
1584
- const result = await listUsers();
1624
+ let result;
1625
+ if (content && fileName) {
1626
+ const buffer = Buffer.from(content, "base64");
1627
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1628
+ console.error(`[nworks] MCP upload: fileName=${fileName}, bufferSize=${buffer.length}`);
1629
+ }
1630
+ result = await uploadBuffer(
1631
+ buffer,
1632
+ fileName,
1633
+ userId ?? "me",
1634
+ folderId,
1635
+ overwrite ?? false
1636
+ );
1637
+ } else if (filePath) {
1638
+ const safePath = validateLocalPath(filePath);
1639
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1640
+ console.error(`[nworks] MCP upload: filePath=${safePath}`);
1641
+ }
1642
+ result = await uploadFile(
1643
+ safePath,
1644
+ userId ?? "me",
1645
+ folderId,
1646
+ overwrite ?? false
1647
+ );
1648
+ } else {
1649
+ return {
1650
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: "content+fileName \uB610\uB294 filePath \uC911 \uD558\uB098\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4. MCP \uD074\uB77C\uC774\uC5B8\uD2B8\uC5D0\uC11C\uB294 \uD30C\uC77C \uB0B4\uC6A9\uC744 base64\uB85C \uC778\uCF54\uB529\uD558\uC5EC content \uD30C\uB77C\uBBF8\uD130\uC5D0 \uC804\uB2EC\uD558\uACE0, fileName\uC5D0 \uD30C\uC77C\uBA85\uC744 \uC9C0\uC815\uD558\uC138\uC694." }) }],
1651
+ isError: true
1652
+ };
1653
+ }
1585
1654
  return {
1586
- content: [{ type: "text", text: JSON.stringify(result) }]
1655
+ content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
1587
1656
  };
1588
1657
  } catch (err) {
1658
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1659
+ console.error(`[nworks] drive upload error: ${err.stack}`);
1660
+ }
1589
1661
  return {
1590
- content: [{ type: "text", text: mcpErrorHint(err) }],
1662
+ content: [{ type: "text", text: mcpErrorHint(err, "drive.upload") }],
1591
1663
  isError: true
1592
1664
  };
1593
1665
  }
1594
1666
  }
1595
1667
  );
1596
- server.tool(
1597
- "nworks_calendar_list",
1598
- "\uC0AC\uC6A9\uC790\uC758 \uCE98\uB9B0\uB354 \uC77C\uC815/\uC2A4\uCF00\uC904\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uC624\uB298 \uC77C\uC815 \uC54C\uB824\uC918', '\uC774\uBC88 \uC8FC \uC2A4\uCF00\uC904 \uD655\uC778' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (calendar.read scope). \uBBF8\uB85C\uADF8\uC778 \uC2DC nworks_login_user\uB85C \uB85C\uADF8\uC778 \uD544\uC694",
1668
+ defineTool(
1669
+ server,
1670
+ "nworks_drive_download",
1671
+ "\uB4DC\uB77C\uC774\uBE0C \uD30C\uC77C\uC744 \uB2E4\uC6B4\uB85C\uB4DC\uD569\uB2C8\uB2E4. User OAuth \uC778\uC99D \uD544\uC694 (file.read scope). outputDir\uC744 \uC9C0\uC815\uD558\uBA74 \uB85C\uCEEC\uC5D0 \uD30C\uC77C\uB85C \uC800\uC7A5\uD558\uACE0, \uBBF8\uC9C0\uC815 \uC2DC \uD30C\uC77C \uB0B4\uC6A9\uC744 \uC9C1\uC811 \uBC18\uD658\uD569\uB2C8\uB2E4 (\uD14D\uC2A4\uD2B8\uB294 text, \uBC14\uC774\uB108\uB9AC\uB294 base64). 5MB \uCD08\uACFC \uD30C\uC77C\uC740 \uBC18\uB4DC\uC2DC outputDir\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.",
1599
1672
  {
1600
- fromDateTime: z.string().describe("\uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss+09:00)"),
1601
- untilDateTime: z.string().describe("\uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss+09:00)"),
1602
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
1673
+ fileId: z6.string().describe("\uB2E4\uC6B4\uB85C\uB4DC\uD560 \uD30C\uC77C ID (nworks_drive_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
1674
+ outputDir: z6.string().optional().describe("\uC800\uC7A5 \uB514\uB809\uD1A0\uB9AC (\uC9C0\uC815 \uC2DC \uD30C\uC77C\uB85C \uC800\uC7A5, \uBBF8\uC9C0\uC815 \uC2DC \uB0B4\uC6A9\uC744 \uC9C1\uC811 \uBC18\uD658)"),
1675
+ outputName: z6.string().optional().describe("\uC800\uC7A5 \uD30C\uC77C\uBA85 (\uBBF8\uC9C0\uC815 \uC2DC \uC6D0\uBCF8 \uD30C\uC77C\uBA85)"),
1676
+ userId: z6.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
1603
1677
  },
1604
- async ({ fromDateTime, untilDateTime, userId }) => {
1678
+ async ({ fileId, outputDir, outputName, userId }) => {
1605
1679
  try {
1606
- const result = await listEvents(
1607
- fromDateTime,
1608
- untilDateTime,
1680
+ const result = await downloadFile(
1681
+ fileId,
1609
1682
  userId ?? "me"
1610
1683
  );
1611
- const events = result.events.flatMap(
1612
- (e) => e.eventComponents.map((c) => ({
1613
- eventId: c.eventId,
1614
- summary: c.summary,
1615
- start: c.start.dateTime ?? c.start.date ?? "",
1616
- end: c.end.dateTime ?? c.end.date ?? "",
1617
- location: c.location ?? ""
1618
- }))
1619
- );
1620
- return {
1621
- content: [{ type: "text", text: JSON.stringify({ events, count: events.length }) }]
1622
- };
1623
- } catch (err) {
1624
- return {
1625
- content: [{ type: "text", text: mcpErrorHint(err, "calendar.list") }],
1626
- isError: true
1627
- };
1628
- }
1629
- }
1630
- );
1631
- server.tool(
1632
- "nworks_calendar_create",
1633
- "\uCE98\uB9B0\uB354 \uC77C\uC815\uC744 \uC0C8\uB85C \uB9CC\uB4ED\uB2C8\uB2E4. '\uD68C\uC758 \uC7A1\uC544\uC918', '\uC77C\uC815 \uB4F1\uB85D\uD574\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (calendar + calendar.read scope)",
1634
- {
1635
- summary: z.string().describe("\uC77C\uC815 \uC81C\uBAA9"),
1636
- start: z.string().describe("\uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
1637
- end: z.string().describe("\uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
1638
- timeZone: z.string().optional().describe("\uD0C0\uC784\uC874 (\uAE30\uBCF8: Asia/Seoul)"),
1639
- description: z.string().optional().describe("\uC77C\uC815 \uC124\uBA85"),
1640
- location: z.string().optional().describe("\uC7A5\uC18C"),
1641
- attendees: z.array(z.object({ email: z.string(), displayName: z.string().optional() })).optional().describe("\uCC38\uC11D\uC790 \uBAA9\uB85D"),
1642
- sendNotification: z.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
1643
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
1644
- },
1645
- async ({ summary, start, end, timeZone, description, location, attendees, sendNotification, userId }) => {
1646
- try {
1647
- const result = await createEvent({
1648
- summary,
1649
- start,
1650
- end,
1651
- timeZone,
1652
- description,
1653
- location,
1654
- attendees,
1655
- sendNotification,
1656
- userId: userId ?? "me"
1657
- });
1658
- const event = result.eventComponents?.[0];
1659
- return {
1660
- content: [{ type: "text", text: JSON.stringify({ success: true, eventId: event?.eventId, summary: event?.summary, start: event?.start, end: event?.end }) }]
1661
- };
1662
- } catch (err) {
1663
- return {
1664
- content: [{ type: "text", text: mcpErrorHint(err, "calendar.create") }],
1665
- isError: true
1666
- };
1667
- }
1668
- }
1669
- );
1670
- server.tool(
1671
- "nworks_calendar_update",
1672
- "\uAE30\uC874 \uCE98\uB9B0\uB354 \uC77C\uC815\uC744 \uC218\uC815\uD569\uB2C8\uB2E4. '\uC77C\uC815 \uC2DC\uAC04 \uBCC0\uACBD\uD574\uC918', '\uD68C\uC758 \uC81C\uBAA9 \uBC14\uAFD4\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (calendar + calendar.read scope). eventId\uB294 nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5",
1673
- {
1674
- eventId: z.string().describe("\uC77C\uC815 ID (nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
1675
- summary: z.string().optional().describe("\uC0C8 \uC81C\uBAA9"),
1676
- start: z.string().optional().describe("\uC0C8 \uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
1677
- end: z.string().optional().describe("\uC0C8 \uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
1678
- timeZone: z.string().optional().describe("\uD0C0\uC784\uC874 (\uAE30\uBCF8: Asia/Seoul)"),
1679
- description: z.string().optional().describe("\uC0C8 \uC124\uBA85"),
1680
- location: z.string().optional().describe("\uC0C8 \uC7A5\uC18C"),
1681
- sendNotification: z.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
1682
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
1683
- },
1684
- async ({ eventId, summary, start, end, timeZone, description, location, sendNotification, userId }) => {
1685
- try {
1686
- await updateEvent({
1687
- eventId,
1688
- summary,
1689
- start,
1690
- end,
1691
- timeZone,
1692
- description,
1693
- location,
1694
- sendNotification,
1695
- userId: userId ?? "me"
1696
- });
1684
+ const fileName = outputName ?? result.fileName ?? fileId;
1685
+ if (outputDir) {
1686
+ const { writeFile: writeFile2 } = await import("fs/promises");
1687
+ const { join: join3 } = await import("path");
1688
+ const safeDir = validateLocalPath(outputDir);
1689
+ const safeName = sanitizeFileName(fileName);
1690
+ const outPath = join3(safeDir, safeName);
1691
+ validateLocalPath(outPath, safeDir);
1692
+ await writeFile2(outPath, result.buffer);
1693
+ return {
1694
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
1695
+ };
1696
+ }
1697
+ const MAX_INLINE_SIZE = 5 * 1024 * 1024;
1698
+ if (result.buffer.length > MAX_INLINE_SIZE) {
1699
+ const sizeMB = (result.buffer.length / (1024 * 1024)).toFixed(1);
1700
+ return {
1701
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: `\uD30C\uC77C\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4 (${sizeMB}MB). outputDir\uB97C \uC9C0\uC815\uD574\uC11C \uB85C\uCEEC\uC5D0 \uC800\uC7A5\uD558\uC138\uC694.`, fileName, size: result.buffer.length }) }],
1702
+ isError: true
1703
+ };
1704
+ }
1705
+ const textExtensions = /\.(txt|md|csv|json|xml|html|htm|css|js|ts|jsx|tsx|yaml|yml|toml|ini|cfg|conf|log|sh|bash|zsh|py|rb|java|go|rs|c|cpp|h|hpp|sql|graphql|env|gitignore|dockerignore|editorconfig)$/i;
1706
+ const isText = textExtensions.test(fileName);
1707
+ if (isText) {
1708
+ const text = result.buffer.toString("utf-8");
1709
+ return {
1710
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "text", content: text }) }]
1711
+ };
1712
+ }
1713
+ const base64 = result.buffer.toString("base64");
1697
1714
  return {
1698
- content: [{ type: "text", text: JSON.stringify({ success: true, eventId, message: "\uC77C\uC815\uC774 \uC218\uC815\uB418\uC5C8\uC2B5\uB2C8\uB2E4" }) }]
1715
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "base64", content: base64 }) }]
1699
1716
  };
1700
1717
  } catch (err) {
1701
1718
  return {
1702
- content: [{ type: "text", text: mcpErrorHint(err, "calendar.update") }],
1719
+ content: [{ type: "text", text: mcpErrorHint(err, "drive.download") }],
1703
1720
  isError: true
1704
1721
  };
1705
1722
  }
1706
1723
  }
1707
1724
  );
1708
- server.tool(
1709
- "nworks_calendar_delete",
1710
- "\uCE98\uB9B0\uB354 \uC77C\uC815\uC744 \uC0AD\uC81C\uD569\uB2C8\uB2E4. '\uC77C\uC815 \uCDE8\uC18C\uD574\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (calendar + calendar.read scope). eventId\uB294 nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5",
1725
+ defineTool(
1726
+ server,
1727
+ "nworks_sharedrive_list",
1728
+ "NAVER WORKS \uACF5\uC720 \uB4DC\uB77C\uC774\uBE0C \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uACF5\uC720 \uB4DC\uB77C\uC774\uBE0C \uBB50 \uC788\uC5B4?', '\uD300 \uACF5\uC720 \uB4DC\uB77C\uC774\uBE0C \uCC3E\uC544\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (file.read scope)",
1711
1729
  {
1712
- eventId: z.string().describe("\uC0AD\uC81C\uD560 \uC77C\uC815 ID (nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
1713
- sendNotification: z.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
1714
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
1730
+ count: z6.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 200)"),
1731
+ cursor: z6.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
1715
1732
  },
1716
- async ({ eventId, sendNotification, userId }) => {
1733
+ async ({ count, cursor }) => {
1717
1734
  try {
1718
- await deleteEvent(
1719
- eventId,
1720
- userId ?? "me",
1721
- sendNotification ?? false
1722
- );
1735
+ const result = await listSharedDrives(count ?? 20, cursor);
1723
1736
  return {
1724
- content: [{ type: "text", text: JSON.stringify({ success: true, eventId, message: "\uC77C\uC815\uC774 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4" }) }]
1737
+ content: [{ type: "text", text: JSON.stringify({ sharedDrives: result.sharedDrives, count: result.sharedDrives.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
1725
1738
  };
1726
1739
  } catch (err) {
1727
1740
  return {
1728
- content: [{ type: "text", text: mcpErrorHint(err, "calendar.delete") }],
1741
+ content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_list") }],
1729
1742
  isError: true
1730
1743
  };
1731
1744
  }
1732
1745
  }
1733
1746
  );
1734
- server.tool(
1735
- "nworks_drive_list",
1736
- "NAVER WORKS \uB4DC\uB77C\uC774\uBE0C\uC758 \uD30C\uC77C/\uD3F4\uB354 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uB4DC\uB77C\uC774\uBE0C \uD30C\uC77C \uBCF4\uC5EC\uC918', '\uB0B4 \uD30C\uC77C \uBAA9\uB85D' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (file.read scope)",
1747
+ defineTool(
1748
+ server,
1749
+ "nworks_sharedrive_files",
1750
+ "\uACF5\uC720 \uB4DC\uB77C\uC774\uBE0C\uC758 \uD30C\uC77C/\uD3F4\uB354 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. sharedriveId\uB294 nworks_sharedrive_list\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (file.read scope)",
1737
1751
  {
1738
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
1739
- folderId: z.string().optional().describe("\uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
1740
- count: z.number().optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 200)"),
1741
- cursor: z.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
1752
+ sharedriveId: z6.string().describe("\uACF5\uC720 \uB4DC\uB77C\uC774\uBE0C ID (nworks_sharedrive_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
1753
+ folderId: z6.string().optional().describe("\uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
1754
+ count: z6.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 200)"),
1755
+ cursor: z6.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
1742
1756
  },
1743
- async ({ userId, folderId, count, cursor }) => {
1757
+ async ({ sharedriveId, folderId, count, cursor }) => {
1744
1758
  try {
1745
- const result = await listFiles(
1746
- userId ?? "me",
1759
+ const result = await listSharedDriveFiles(
1760
+ sharedriveId,
1747
1761
  folderId,
1748
1762
  count ?? 20,
1749
1763
  cursor
@@ -1761,87 +1775,33 @@ OAuth Redirect URI: http://localhost:9876/callback`,
1761
1775
  };
1762
1776
  } catch (err) {
1763
1777
  return {
1764
- content: [{ type: "text", text: mcpErrorHint(err, "drive.list") }],
1765
- isError: true
1766
- };
1767
- }
1768
- }
1769
- );
1770
- server.tool(
1771
- "nworks_drive_upload",
1772
- "\uD30C\uC77C\uC744 \uB4DC\uB77C\uC774\uBE0C\uC5D0 \uC5C5\uB85C\uB4DC\uD569\uB2C8\uB2E4 (User OAuth file scope \uD544\uC694). content(base64)\uC640 fileName\uC73C\uB85C \uC804\uB2EC\uD558\uAC70\uB098, filePath\uB85C \uB85C\uCEEC \uD30C\uC77C \uACBD\uB85C\uB97C \uC9C0\uC815\uD569\uB2C8\uB2E4. MCP \uD074\uB77C\uC774\uC5B8\uD2B8\uC5D0\uC11C\uB294 content+fileName \uBC29\uC2DD\uC744 \uAD8C\uC7A5\uD569\uB2C8\uB2E4.",
1773
- {
1774
- content: z.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uD30C\uC77C \uB0B4\uC6A9 (base64 \uC778\uCF54\uB529). filePath \uB300\uC2E0 \uC0AC\uC6A9"),
1775
- fileName: z.string().optional().describe("\uD30C\uC77C\uBA85 (content \uC0AC\uC6A9 \uC2DC \uD544\uC218)"),
1776
- filePath: z.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uB85C\uCEEC \uD30C\uC77C \uACBD\uB85C (content \uB300\uC2E0 \uC0AC\uC6A9, \uB85C\uCEEC \uD658\uACBD\uC5D0\uC11C\uB9CC \uB3D9\uC791)"),
1777
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
1778
- folderId: z.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
1779
- overwrite: z.boolean().optional().describe("\uB3D9\uC77C \uD30C\uC77C\uBA85 \uB36E\uC5B4\uC4F0\uAE30 (\uAE30\uBCF8: false)")
1780
- },
1781
- async ({ content, fileName, filePath, userId, folderId, overwrite }) => {
1782
- try {
1783
- let result;
1784
- if (content && fileName) {
1785
- const buffer = Buffer.from(content, "base64");
1786
- if (process.env["NWORKS_VERBOSE"] === "1") {
1787
- console.error(`[nworks] MCP upload: fileName=${fileName}, bufferSize=${buffer.length}`);
1788
- }
1789
- result = await uploadBuffer(
1790
- buffer,
1791
- fileName,
1792
- userId ?? "me",
1793
- folderId,
1794
- overwrite ?? false
1795
- );
1796
- } else if (filePath) {
1797
- if (process.env["NWORKS_VERBOSE"] === "1") {
1798
- console.error(`[nworks] MCP upload: filePath=${filePath}`);
1799
- }
1800
- result = await uploadFile(
1801
- filePath,
1802
- userId ?? "me",
1803
- folderId,
1804
- overwrite ?? false
1805
- );
1806
- } else {
1807
- return {
1808
- content: [{ type: "text", text: JSON.stringify({ error: true, message: "content+fileName \uB610\uB294 filePath \uC911 \uD558\uB098\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4. MCP \uD074\uB77C\uC774\uC5B8\uD2B8\uC5D0\uC11C\uB294 \uD30C\uC77C \uB0B4\uC6A9\uC744 base64\uB85C \uC778\uCF54\uB529\uD558\uC5EC content \uD30C\uB77C\uBBF8\uD130\uC5D0 \uC804\uB2EC\uD558\uACE0, fileName\uC5D0 \uD30C\uC77C\uBA85\uC744 \uC9C0\uC815\uD558\uC138\uC694." }) }],
1809
- isError: true
1810
- };
1811
- }
1812
- return {
1813
- content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
1814
- };
1815
- } catch (err) {
1816
- const error = err;
1817
- const detail = process.env["NWORKS_VERBOSE"] === "1" ? ` | stack: ${error.stack}` : "";
1818
- return {
1819
- content: [{ type: "text", text: `${mcpErrorHint(err, "drive.upload")}${detail}` }],
1778
+ content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_files") }],
1820
1779
  isError: true
1821
1780
  };
1822
1781
  }
1823
1782
  }
1824
1783
  );
1825
- server.tool(
1826
- "nworks_drive_download",
1827
- "\uB4DC\uB77C\uC774\uBE0C \uD30C\uC77C\uC744 \uB2E4\uC6B4\uB85C\uB4DC\uD569\uB2C8\uB2E4. User OAuth \uC778\uC99D \uD544\uC694 (file.read scope). outputDir\uC744 \uC9C0\uC815\uD558\uBA74 \uB85C\uCEEC\uC5D0 \uD30C\uC77C\uB85C \uC800\uC7A5\uD558\uACE0, \uBBF8\uC9C0\uC815 \uC2DC \uD30C\uC77C \uB0B4\uC6A9\uC744 \uC9C1\uC811 \uBC18\uD658\uD569\uB2C8\uB2E4 (\uD14D\uC2A4\uD2B8\uB294 text, \uBC14\uC774\uB108\uB9AC\uB294 base64). 5MB \uCD08\uACFC \uD30C\uC77C\uC740 \uBC18\uB4DC\uC2DC outputDir\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.",
1784
+ defineTool(
1785
+ server,
1786
+ "nworks_sharedrive_download",
1787
+ "\uACF5\uC720 \uB4DC\uB77C\uC774\uBE0C \uD30C\uC77C\uC744 \uB2E4\uC6B4\uB85C\uB4DC\uD569\uB2C8\uB2E4. User OAuth \uC778\uC99D \uD544\uC694 (file.read scope). outputDir\uC744 \uC9C0\uC815\uD558\uBA74 \uB85C\uCEEC\uC5D0 \uD30C\uC77C\uB85C \uC800\uC7A5\uD558\uACE0, \uBBF8\uC9C0\uC815 \uC2DC \uD30C\uC77C \uB0B4\uC6A9\uC744 \uC9C1\uC811 \uBC18\uD658\uD569\uB2C8\uB2E4 (\uD14D\uC2A4\uD2B8\uB294 text, \uBC14\uC774\uB108\uB9AC\uB294 base64). 5MB \uCD08\uACFC \uD30C\uC77C\uC740 \uBC18\uB4DC\uC2DC outputDir\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.",
1828
1788
  {
1829
- fileId: z.string().describe("\uB2E4\uC6B4\uB85C\uB4DC\uD560 \uD30C\uC77C ID (nworks_drive_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
1830
- outputDir: z.string().optional().describe("\uC800\uC7A5 \uB514\uB809\uD1A0\uB9AC (\uC9C0\uC815 \uC2DC \uD30C\uC77C\uB85C \uC800\uC7A5, \uBBF8\uC9C0\uC815 \uC2DC \uB0B4\uC6A9\uC744 \uC9C1\uC811 \uBC18\uD658)"),
1831
- outputName: z.string().optional().describe("\uC800\uC7A5 \uD30C\uC77C\uBA85 (\uBBF8\uC9C0\uC815 \uC2DC \uC6D0\uBCF8 \uD30C\uC77C\uBA85)"),
1832
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
1789
+ sharedriveId: z6.string().describe("\uACF5\uC720 \uB4DC\uB77C\uC774\uBE0C ID (nworks_sharedrive_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
1790
+ fileId: z6.string().describe("\uB2E4\uC6B4\uB85C\uB4DC\uD560 \uD30C\uC77C ID (nworks_sharedrive_files\uB85C \uC870\uD68C \uAC00\uB2A5)"),
1791
+ outputDir: z6.string().optional().describe("\uC800\uC7A5 \uB514\uB809\uD1A0\uB9AC (\uC9C0\uC815 \uC2DC \uD30C\uC77C\uB85C \uC800\uC7A5, \uBBF8\uC9C0\uC815 \uC2DC \uB0B4\uC6A9\uC744 \uC9C1\uC811 \uBC18\uD658)"),
1792
+ outputName: z6.string().optional().describe("\uC800\uC7A5 \uD30C\uC77C\uBA85 (\uBBF8\uC9C0\uC815 \uC2DC \uC6D0\uBCF8 \uD30C\uC77C\uBA85)")
1833
1793
  },
1834
- async ({ fileId, outputDir, outputName, userId }) => {
1794
+ async ({ sharedriveId, fileId, outputDir, outputName }) => {
1835
1795
  try {
1836
- const result = await downloadFile(
1837
- fileId,
1838
- userId ?? "me"
1839
- );
1796
+ const result = await downloadSharedDriveFile(sharedriveId, fileId);
1840
1797
  const fileName = outputName ?? result.fileName ?? fileId;
1841
1798
  if (outputDir) {
1842
1799
  const { writeFile: writeFile2 } = await import("fs/promises");
1843
- const { join: join2 } = await import("path");
1844
- const outPath = join2(outputDir, fileName);
1800
+ const { join: join3 } = await import("path");
1801
+ const safeDir = validateLocalPath(outputDir);
1802
+ const safeName = sanitizeFileName(fileName);
1803
+ const outPath = join3(safeDir, safeName);
1804
+ validateLocalPath(outPath, safeDir);
1845
1805
  await writeFile2(outPath, result.buffer);
1846
1806
  return {
1847
1807
  content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
@@ -1869,23 +1829,102 @@ OAuth Redirect URI: http://localhost:9876/callback`,
1869
1829
  };
1870
1830
  } catch (err) {
1871
1831
  return {
1872
- content: [{ type: "text", text: mcpErrorHint(err, "drive.download") }],
1832
+ content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_download") }],
1873
1833
  isError: true
1874
1834
  };
1875
1835
  }
1876
1836
  }
1877
1837
  );
1878
- server.tool(
1838
+ }
1839
+
1840
+ // src/mcp/tools/mail.ts
1841
+ import { z as z7 } from "zod";
1842
+
1843
+ // src/api/mail.ts
1844
+ var BASE_URL4 = "https://www.worksapis.com/v1.0";
1845
+ async function sendMail(opts) {
1846
+ const userId = opts.userId ?? "me";
1847
+ const profile = opts.profile ?? "default";
1848
+ const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail`;
1849
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1850
+ console.error(`[nworks] POST ${url}`);
1851
+ }
1852
+ const body = {
1853
+ to: opts.to,
1854
+ subject: opts.subject
1855
+ };
1856
+ if (opts.body !== void 0) body.body = opts.body;
1857
+ if (opts.cc) body.cc = opts.cc;
1858
+ if (opts.bcc) body.bcc = opts.bcc;
1859
+ if (opts.contentType) body.contentType = opts.contentType;
1860
+ const res = await userFetch(
1861
+ url,
1862
+ {
1863
+ method: "POST",
1864
+ headers: { "Content-Type": "application/json" },
1865
+ body: JSON.stringify(body)
1866
+ },
1867
+ profile
1868
+ );
1869
+ if (res.status === 202) return;
1870
+ if (!res.ok) return handleUserApiError(res);
1871
+ }
1872
+ async function listMails(folderId = 0, userId = "me", count = 30, cursor, isUnread, profile = "default") {
1873
+ const params = new URLSearchParams();
1874
+ params.set("count", String(count));
1875
+ if (cursor) params.set("cursor", cursor);
1876
+ if (isUnread) params.set("isUnread", "true");
1877
+ const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail/mailfolders/${sanitizePathSegment(String(folderId))}/children?${params.toString()}`;
1878
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1879
+ console.error(`[nworks] GET ${url}`);
1880
+ }
1881
+ const res = await userFetch(url, { method: "GET" }, profile);
1882
+ if (!res.ok) return handleUserApiError(res);
1883
+ const data = await res.json();
1884
+ return {
1885
+ mails: data.mails ?? [],
1886
+ unreadCount: data.unreadCount,
1887
+ folderName: data.folderName,
1888
+ totalCount: data.totalCount,
1889
+ responseMetaData: data.responseMetaData
1890
+ };
1891
+ }
1892
+ async function readMail(mailId, userId = "me", profile = "default") {
1893
+ const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail/${sanitizePathSegment(String(mailId))}`;
1894
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1895
+ console.error(`[nworks] GET ${url}`);
1896
+ }
1897
+ const res = await userFetch(url, { method: "GET" }, profile);
1898
+ if (!res.ok) return handleUserApiError(res);
1899
+ return await res.json();
1900
+ }
1901
+ async function downloadAttachment(mailId, attachmentId, userId = "me", profile = "default") {
1902
+ const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail/${sanitizePathSegment(
1903
+ String(mailId)
1904
+ )}/attachments/${sanitizePathSegment(String(attachmentId))}`;
1905
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1906
+ console.error(`[nworks] GET ${url}`);
1907
+ }
1908
+ const res = await userFetch(url, { method: "GET" }, profile);
1909
+ if (!res.ok) return handleUserApiError(res);
1910
+ const data = await res.json();
1911
+ return { buffer: Buffer.from(data.data, "base64"), filename: data.filename };
1912
+ }
1913
+
1914
+ // src/mcp/tools/mail.ts
1915
+ function registerMailTools(server) {
1916
+ defineTool(
1917
+ server,
1879
1918
  "nworks_mail_send",
1880
1919
  "NAVER WORKS \uBA54\uC77C\uC744 \uC804\uC1A1\uD569\uB2C8\uB2E4. '\uBA54\uC77C \uBCF4\uB0B4\uC918', '\uC774\uBA54\uC77C \uC791\uC131\uD574\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. \uBE44\uB3D9\uAE30 \uC804\uC1A1(\uC131\uACF5 \uC2DC 202). User OAuth \uC778\uC99D \uD544\uC694 (mail scope)",
1881
1920
  {
1882
- to: z.string().describe("\uC218\uC2E0\uC790 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
1883
- subject: z.string().describe("\uBA54\uC77C \uC81C\uBAA9"),
1884
- body: z.string().optional().describe("\uBA54\uC77C \uBCF8\uBB38"),
1885
- cc: z.string().optional().describe("\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
1886
- bcc: z.string().optional().describe("\uC228\uC740\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
1887
- contentType: z.enum(["html", "text"]).optional().describe("\uBCF8\uBB38 \uD615\uC2DD (\uAE30\uBCF8: html)"),
1888
- userId: z.string().optional().describe("\uBC1C\uC2E0\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
1921
+ to: z7.string().describe("\uC218\uC2E0\uC790 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
1922
+ subject: z7.string().describe("\uBA54\uC77C \uC81C\uBAA9"),
1923
+ body: z7.string().optional().describe("\uBA54\uC77C \uBCF8\uBB38"),
1924
+ cc: z7.string().optional().describe("\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
1925
+ bcc: z7.string().optional().describe("\uC228\uC740\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
1926
+ contentType: z7.enum(["html", "text"]).optional().describe("\uBCF8\uBB38 \uD615\uC2DD (\uAE30\uBCF8: html)"),
1927
+ userId: z7.string().optional().describe("\uBC1C\uC2E0\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
1889
1928
  },
1890
1929
  async ({ to, subject, body, cc, bcc, contentType, userId }) => {
1891
1930
  try {
@@ -1909,15 +1948,16 @@ OAuth Redirect URI: http://localhost:9876/callback`,
1909
1948
  }
1910
1949
  }
1911
1950
  );
1912
- server.tool(
1951
+ defineTool(
1952
+ server,
1913
1953
  "nworks_mail_list",
1914
1954
  "\uBC1B\uC740 \uBA54\uC77C \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uBA54\uC77C \uD655\uC778\uD574\uC918', '\uBC1B\uC740\uD3B8\uC9C0\uD568 \uBCF4\uC5EC\uC918', '\uC548 \uC77D\uC740 \uBA54\uC77C \uC788\uC5B4?' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (mail.read scope)",
1915
1955
  {
1916
- folderId: z.number().optional().describe("\uBA54\uC77C \uD3F4\uB354 ID (\uAE30\uBCF8: 0 = \uBC1B\uC740\uD3B8\uC9C0\uD568)"),
1917
- count: z.number().optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 30, \uCD5C\uB300: 200)"),
1918
- cursor: z.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
1919
- isUnread: z.boolean().optional().describe("\uC77D\uC9C0 \uC54A\uC740 \uBA54\uC77C\uB9CC \uC870\uD68C"),
1920
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
1956
+ folderId: z7.number().int().min(0).optional().describe("\uBA54\uC77C \uD3F4\uB354 ID (\uAE30\uBCF8: 0 = \uBC1B\uC740\uD3B8\uC9C0\uD568)"),
1957
+ count: z7.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 30, \uCD5C\uB300: 200)"),
1958
+ cursor: z7.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
1959
+ isUnread: z7.boolean().optional().describe("\uC77D\uC9C0 \uC54A\uC740 \uBA54\uC77C\uB9CC \uC870\uD68C"),
1960
+ userId: z7.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
1921
1961
  },
1922
1962
  async ({ folderId, count, cursor, isUnread, userId }) => {
1923
1963
  try {
@@ -1947,12 +1987,13 @@ OAuth Redirect URI: http://localhost:9876/callback`,
1947
1987
  }
1948
1988
  }
1949
1989
  );
1950
- server.tool(
1990
+ defineTool(
1991
+ server,
1951
1992
  "nworks_mail_read",
1952
1993
  "\uD2B9\uC815 \uBA54\uC77C\uC758 \uC0C1\uC138 \uB0B4\uC6A9(\uBCF8\uBB38, \uCCA8\uBD80\uD30C\uC77C \uB4F1)\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uC774 \uBA54\uC77C \uB0B4\uC6A9 \uBCF4\uC5EC\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. mailId\uB294 nworks_mail_list\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (mail.read scope)",
1953
1994
  {
1954
- mailId: z.number().describe("\uBA54\uC77C ID (nworks_mail_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
1955
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
1995
+ mailId: z7.number().int().min(0).describe("\uBA54\uC77C ID (nworks_mail_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
1996
+ userId: z7.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
1956
1997
  },
1957
1998
  async ({ mailId, userId }) => {
1958
1999
  try {
@@ -1986,15 +2027,207 @@ OAuth Redirect URI: http://localhost:9876/callback`,
1986
2027
  }
1987
2028
  }
1988
2029
  );
1989
- server.tool(
2030
+ defineTool(
2031
+ server,
2032
+ "nworks_mail_download_attachment",
2033
+ "\uBA54\uC77C \uCCA8\uBD80\uD30C\uC77C\uC744 \uB2E4\uC6B4\uB85C\uB4DC\uD569\uB2C8\uB2E4. mailId\uC640 attachmentId\uB294 nworks_mail_read\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (mail.read scope). outputDir\uC744 \uC9C0\uC815\uD558\uBA74 \uB85C\uCEEC\uC5D0 \uD30C\uC77C\uB85C \uC800\uC7A5\uD558\uACE0, \uBBF8\uC9C0\uC815 \uC2DC \uD30C\uC77C \uB0B4\uC6A9\uC744 \uC9C1\uC811 \uBC18\uD658\uD569\uB2C8\uB2E4 (\uD14D\uC2A4\uD2B8\uB294 text, \uBC14\uC774\uB108\uB9AC\uB294 base64). 5MB \uCD08\uACFC \uD30C\uC77C\uC740 \uBC18\uB4DC\uC2DC outputDir\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.",
2034
+ {
2035
+ mailId: z7.number().int().min(0).describe("\uBA54\uC77C ID (nworks_mail_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2036
+ attachmentId: z7.number().int().min(0).describe("\uCCA8\uBD80\uD30C\uC77C ID (nworks_mail_read\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2037
+ outputDir: z7.string().optional().describe("\uC800\uC7A5 \uB514\uB809\uD1A0\uB9AC (\uC9C0\uC815 \uC2DC \uD30C\uC77C\uB85C \uC800\uC7A5, \uBBF8\uC9C0\uC815 \uC2DC \uB0B4\uC6A9\uC744 \uC9C1\uC811 \uBC18\uD658)"),
2038
+ outputName: z7.string().optional().describe("\uC800\uC7A5 \uD30C\uC77C\uBA85 (\uBBF8\uC9C0\uC815 \uC2DC \uC6D0\uBCF8 \uD30C\uC77C\uBA85)"),
2039
+ userId: z7.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2040
+ },
2041
+ async ({ mailId, attachmentId, outputDir, outputName, userId }) => {
2042
+ try {
2043
+ const result = await downloadAttachment(
2044
+ mailId,
2045
+ attachmentId,
2046
+ userId ?? "me"
2047
+ );
2048
+ const fileName = outputName ?? result.filename;
2049
+ if (outputDir) {
2050
+ const { writeFile: writeFile2 } = await import("fs/promises");
2051
+ const { join: join3 } = await import("path");
2052
+ const safeDir = validateLocalPath(outputDir);
2053
+ const safeName = sanitizeFileName(fileName);
2054
+ const outPath = join3(safeDir, safeName);
2055
+ validateLocalPath(outPath, safeDir);
2056
+ await writeFile2(outPath, result.buffer);
2057
+ return {
2058
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
2059
+ };
2060
+ }
2061
+ const MAX_INLINE_SIZE = 5 * 1024 * 1024;
2062
+ if (result.buffer.length > MAX_INLINE_SIZE) {
2063
+ const sizeMB = (result.buffer.length / (1024 * 1024)).toFixed(1);
2064
+ return {
2065
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: `\uD30C\uC77C\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4 (${sizeMB}MB). outputDir\uB97C \uC9C0\uC815\uD574\uC11C \uB85C\uCEEC\uC5D0 \uC800\uC7A5\uD558\uC138\uC694.`, fileName, size: result.buffer.length }) }],
2066
+ isError: true
2067
+ };
2068
+ }
2069
+ const textExtensions = /\.(txt|md|csv|json|xml|html|htm|css|js|ts|jsx|tsx|yaml|yml|toml|ini|cfg|conf|log|sh|bash|zsh|py|rb|java|go|rs|c|cpp|h|hpp|sql|graphql|env|gitignore|dockerignore|editorconfig)$/i;
2070
+ const isText = textExtensions.test(fileName);
2071
+ if (isText) {
2072
+ const text = result.buffer.toString("utf-8");
2073
+ return {
2074
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "text", content: text }) }]
2075
+ };
2076
+ }
2077
+ const base64 = result.buffer.toString("base64");
2078
+ return {
2079
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "base64", content: base64 }) }]
2080
+ };
2081
+ } catch (err) {
2082
+ return {
2083
+ content: [{ type: "text", text: mcpErrorHint(err, "mail.download_attachment") }],
2084
+ isError: true
2085
+ };
2086
+ }
2087
+ }
2088
+ );
2089
+ }
2090
+
2091
+ // src/mcp/tools/task.ts
2092
+ import { z as z8 } from "zod";
2093
+
2094
+ // src/api/task.ts
2095
+ var BASE_URL5 = "https://www.worksapis.com/v1.0";
2096
+ async function resolveUserId(userId, profile) {
2097
+ if (userId !== "me") return userId;
2098
+ const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}`;
2099
+ const res = await userFetch(url, { method: "GET" }, profile);
2100
+ if (!res.ok) return handleUserApiError(res);
2101
+ const data = await res.json();
2102
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2103
+ console.error(`[nworks] Resolved "me" \u2192 ${data.userId}`);
2104
+ }
2105
+ return data.userId;
2106
+ }
2107
+ async function listTasks(categoryId = "default", userId = "me", count = 50, cursor, status = "ALL", profile = "default") {
2108
+ const params = new URLSearchParams();
2109
+ params.set("categoryId", categoryId);
2110
+ params.set("count", String(count));
2111
+ params.set("status", status);
2112
+ if (cursor) params.set("cursor", cursor);
2113
+ const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}/tasks?${params.toString()}`;
2114
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2115
+ console.error(`[nworks] GET ${url}`);
2116
+ }
2117
+ const res = await userFetch(url, { method: "GET" }, profile);
2118
+ if (!res.ok) return handleUserApiError(res);
2119
+ const data = await res.json();
2120
+ return { tasks: data.tasks ?? [], responseMetaData: data.responseMetaData };
2121
+ }
2122
+ async function createTask(opts) {
2123
+ const userId = opts.userId ?? "me";
2124
+ const profile = opts.profile ?? "default";
2125
+ const resolvedUserId = await resolveUserId(userId, profile);
2126
+ const assignorId = opts.assignorId ?? resolvedUserId;
2127
+ const assigneeIds = opts.assigneeIds ?? [resolvedUserId];
2128
+ const body = {
2129
+ assignorId,
2130
+ assignees: assigneeIds.map((id) => ({ assigneeId: id, status: "TODO" })),
2131
+ title: opts.title,
2132
+ content: opts.content ?? "",
2133
+ completionCondition: "ANY_ONE"
2134
+ };
2135
+ if (opts.dueDate) body.dueDate = opts.dueDate;
2136
+ if (opts.categoryId) body.categoryId = opts.categoryId;
2137
+ const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}/tasks`;
2138
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2139
+ console.error(`[nworks] POST ${url}`);
2140
+ console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
2141
+ }
2142
+ const res = await userFetch(
2143
+ url,
2144
+ {
2145
+ method: "POST",
2146
+ headers: { "Content-Type": "application/json" },
2147
+ body: JSON.stringify(body)
2148
+ },
2149
+ profile
2150
+ );
2151
+ if (res.status === 201) {
2152
+ return await res.json();
2153
+ }
2154
+ if (!res.ok) return handleUserApiError(res);
2155
+ return await res.json();
2156
+ }
2157
+ async function updateTask(opts) {
2158
+ const profile = opts.profile ?? "default";
2159
+ const body = {};
2160
+ if (opts.title !== void 0) body.title = opts.title;
2161
+ if (opts.content !== void 0) body.content = opts.content;
2162
+ if (opts.dueDate !== void 0) body.dueDate = opts.dueDate;
2163
+ const url = `${BASE_URL5}/tasks/${sanitizePathSegment(opts.taskId)}`;
2164
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2165
+ console.error(`[nworks] PATCH ${url}`);
2166
+ }
2167
+ const res = await userFetch(
2168
+ url,
2169
+ {
2170
+ method: "PATCH",
2171
+ headers: { "Content-Type": "application/json" },
2172
+ body: JSON.stringify(body)
2173
+ },
2174
+ profile
2175
+ );
2176
+ if (!res.ok) return handleUserApiError(res);
2177
+ return await res.json();
2178
+ }
2179
+ async function completeTask(taskId, profile = "default") {
2180
+ const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}/complete`;
2181
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2182
+ console.error(`[nworks] POST ${url}`);
2183
+ }
2184
+ const res = await userFetch(
2185
+ url,
2186
+ { method: "POST" },
2187
+ profile
2188
+ );
2189
+ if (res.status === 204) return;
2190
+ if (!res.ok) return handleUserApiError(res);
2191
+ }
2192
+ async function incompleteTask(taskId, profile = "default") {
2193
+ const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}/incomplete`;
2194
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2195
+ console.error(`[nworks] POST ${url}`);
2196
+ }
2197
+ const res = await userFetch(
2198
+ url,
2199
+ { method: "POST" },
2200
+ profile
2201
+ );
2202
+ if (res.status === 204) return;
2203
+ if (!res.ok) return handleUserApiError(res);
2204
+ }
2205
+ async function deleteTask(taskId, profile = "default") {
2206
+ const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}`;
2207
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2208
+ console.error(`[nworks] DELETE ${url}`);
2209
+ }
2210
+ const res = await userFetch(
2211
+ url,
2212
+ { method: "DELETE" },
2213
+ profile
2214
+ );
2215
+ if (res.status === 204) return;
2216
+ if (!res.ok) return handleUserApiError(res);
2217
+ }
2218
+
2219
+ // src/mcp/tools/task.ts
2220
+ function registerTaskTools(server) {
2221
+ defineTool(
2222
+ server,
1990
2223
  "nworks_task_list",
1991
2224
  "\uD560 \uC77C(TODO) \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uD560 \uC77C \uD655\uC778\uD574\uC918', 'TODO \uBAA9\uB85D \uBCF4\uC5EC\uC918', '\uB0A8\uC740 \uC5C5\uBB34 \uBB50 \uC788\uC5B4?' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (task.read scope)",
1992
2225
  {
1993
- categoryId: z.string().optional().describe("\uCE74\uD14C\uACE0\uB9AC ID (\uAE30\uBCF8: default)"),
1994
- status: z.enum(["TODO", "ALL"]).optional().describe("\uD544\uD130: TODO \uB610\uB294 ALL (\uAE30\uBCF8: ALL)"),
1995
- count: z.number().optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 50, \uCD5C\uB300: 100)"),
1996
- cursor: z.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
1997
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2226
+ categoryId: z8.string().optional().describe("\uCE74\uD14C\uACE0\uB9AC ID (\uAE30\uBCF8: default)"),
2227
+ status: z8.enum(["TODO", "ALL"]).optional().describe("\uD544\uD130: TODO \uB610\uB294 ALL (\uAE30\uBCF8: ALL)"),
2228
+ count: z8.number().int().min(1).max(100).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 50, \uCD5C\uB300: 100)"),
2229
+ cursor: z8.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
2230
+ userId: z8.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
1998
2231
  },
1999
2232
  async ({ categoryId, status, count, cursor, userId }) => {
2000
2233
  try {
@@ -2024,16 +2257,17 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2024
2257
  }
2025
2258
  }
2026
2259
  );
2027
- server.tool(
2260
+ defineTool(
2261
+ server,
2028
2262
  "nworks_task_create",
2029
2263
  "\uD560 \uC77C(TODO)\uC744 \uC0C8\uB85C \uB9CC\uB4ED\uB2C8\uB2E4. '\uD560 \uC77C \uCD94\uAC00\uD574\uC918', 'TODO \uB4F1\uB85D\uD574\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. \uAE30\uBCF8\uC801\uC73C\uB85C \uC790\uAE30 \uC790\uC2E0\uC5D0\uAC8C \uD560\uB2F9. User OAuth \uC778\uC99D \uD544\uC694 (task + user.read scope)",
2030
2264
  {
2031
- title: z.string().describe("\uD560 \uC77C \uC81C\uBAA9"),
2032
- content: z.string().optional().describe("\uD560 \uC77C \uB0B4\uC6A9"),
2033
- dueDate: z.string().optional().describe("\uB9C8\uAC10\uC77C (YYYY-MM-DD)"),
2034
- categoryId: z.string().optional().describe("\uCE74\uD14C\uACE0\uB9AC ID"),
2035
- assigneeIds: z.array(z.string()).optional().describe("\uB2F4\uB2F9\uC790 user ID \uBAA9\uB85D (\uBBF8\uC9C0\uC815 \uC2DC \uC790\uAE30 \uC790\uC2E0)"),
2036
- userId: z.string().optional().describe("\uC0DD\uC131\uC790 user ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2265
+ title: z8.string().describe("\uD560 \uC77C \uC81C\uBAA9"),
2266
+ content: z8.string().optional().describe("\uD560 \uC77C \uB0B4\uC6A9"),
2267
+ dueDate: z8.string().optional().describe("\uB9C8\uAC10\uC77C (YYYY-MM-DD)"),
2268
+ categoryId: z8.string().optional().describe("\uCE74\uD14C\uACE0\uB9AC ID"),
2269
+ assigneeIds: z8.array(z8.string()).optional().describe("\uB2F4\uB2F9\uC790 user ID \uBAA9\uB85D (\uBBF8\uC9C0\uC815 \uC2DC \uC790\uAE30 \uC790\uC2E0)"),
2270
+ userId: z8.string().optional().describe("\uC0DD\uC131\uC790 user ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2037
2271
  },
2038
2272
  async ({ title, content, dueDate, categoryId, assigneeIds, userId }) => {
2039
2273
  try {
@@ -2056,15 +2290,16 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2056
2290
  }
2057
2291
  }
2058
2292
  );
2059
- server.tool(
2293
+ defineTool(
2294
+ server,
2060
2295
  "nworks_task_update",
2061
2296
  "\uD560 \uC77C\uC744 \uC218\uC815\uD558\uAC70\uB098 \uC644\uB8CC \uCC98\uB9AC\uD569\uB2C8\uB2E4. '\uD560 \uC77C \uC644\uB8CC \uCC98\uB9AC\uD574\uC918', '\uB9C8\uAC10\uC77C \uBCC0\uACBD\uD574\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. taskId\uB294 nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (task + user.read scope)",
2062
2297
  {
2063
- taskId: z.string().describe("\uD560 \uC77C ID (nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2064
- status: z.enum(["done", "todo"]).optional().describe("\uC0C1\uD0DC \uBCC0\uACBD: done(\uC644\uB8CC) \uB610\uB294 todo(\uBBF8\uC644\uB8CC)"),
2065
- title: z.string().optional().describe("\uC0C8 \uC81C\uBAA9"),
2066
- content: z.string().optional().describe("\uC0C8 \uB0B4\uC6A9"),
2067
- dueDate: z.string().optional().describe("\uC0C8 \uB9C8\uAC10\uC77C (YYYY-MM-DD)")
2298
+ taskId: z8.string().describe("\uD560 \uC77C ID (nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2299
+ status: z8.enum(["done", "todo"]).optional().describe("\uC0C1\uD0DC \uBCC0\uACBD: done(\uC644\uB8CC) \uB610\uB294 todo(\uBBF8\uC644\uB8CC)"),
2300
+ title: z8.string().optional().describe("\uC0C8 \uC81C\uBAA9"),
2301
+ content: z8.string().optional().describe("\uC0C8 \uB0B4\uC6A9"),
2302
+ dueDate: z8.string().optional().describe("\uC0C8 \uB9C8\uAC10\uC77C (YYYY-MM-DD)")
2068
2303
  },
2069
2304
  async ({ taskId, status, title, content, dueDate }) => {
2070
2305
  try {
@@ -2098,11 +2333,12 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2098
2333
  }
2099
2334
  }
2100
2335
  );
2101
- server.tool(
2336
+ defineTool(
2337
+ server,
2102
2338
  "nworks_task_delete",
2103
2339
  "\uD560 \uC77C\uC744 \uC0AD\uC81C\uD569\uB2C8\uB2E4. taskId\uB294 nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (task + user.read scope)",
2104
2340
  {
2105
- taskId: z.string().describe("\uC0AD\uC81C\uD560 \uD560 \uC77C ID (nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
2341
+ taskId: z8.string().describe("\uC0AD\uC81C\uD560 \uD560 \uC77C ID (nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
2106
2342
  },
2107
2343
  async ({ taskId }) => {
2108
2344
  try {
@@ -2118,12 +2354,108 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2118
2354
  }
2119
2355
  }
2120
2356
  );
2121
- server.tool(
2357
+ }
2358
+
2359
+ // src/mcp/tools/board.ts
2360
+ import { z as z9 } from "zod";
2361
+
2362
+ // src/api/board.ts
2363
+ var BASE_URL6 = "https://www.worksapis.com/v1.0";
2364
+ function safeParseJson(text) {
2365
+ const safe = text.replace(
2366
+ /"((?:board|post|domain|user)Id)"\s*:\s*(\d{16,})/g,
2367
+ '"$1":"$2"'
2368
+ );
2369
+ return JSON.parse(safe);
2370
+ }
2371
+ async function listBoards(count = 20, cursor, profile = "default") {
2372
+ const params = new URLSearchParams();
2373
+ params.set("count", String(count));
2374
+ if (cursor) params.set("cursor", cursor);
2375
+ const url = `${BASE_URL6}/boards?${params.toString()}`;
2376
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2377
+ console.error(`[nworks] GET ${url}`);
2378
+ }
2379
+ const res = await userFetch(url, { method: "GET" }, profile);
2380
+ if (!res.ok) return handleUserApiError(res);
2381
+ const text = await res.text();
2382
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2383
+ console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
2384
+ }
2385
+ const data = safeParseJson(text);
2386
+ return { boards: data.boards ?? [], responseMetaData: data.responseMetaData };
2387
+ }
2388
+ async function listPosts(boardId, count = 20, cursor, profile = "default") {
2389
+ const params = new URLSearchParams();
2390
+ params.set("count", String(count));
2391
+ if (cursor) params.set("cursor", cursor);
2392
+ const url = `${BASE_URL6}/boards/${sanitizePathSegment(boardId)}/posts?${params.toString()}`;
2393
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2394
+ console.error(`[nworks] GET ${url}`);
2395
+ }
2396
+ const res = await userFetch(url, { method: "GET" }, profile);
2397
+ if (!res.ok) return handleUserApiError(res);
2398
+ const text = await res.text();
2399
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2400
+ console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
2401
+ }
2402
+ const data = safeParseJson(text);
2403
+ return { posts: data.posts ?? [], responseMetaData: data.responseMetaData };
2404
+ }
2405
+ async function readPost(boardId, postId, profile = "default") {
2406
+ const url = `${BASE_URL6}/boards/${sanitizePathSegment(boardId)}/posts/${sanitizePathSegment(postId)}`;
2407
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2408
+ console.error(`[nworks] GET ${url}`);
2409
+ }
2410
+ const res = await userFetch(url, { method: "GET" }, profile);
2411
+ if (!res.ok) return handleUserApiError(res);
2412
+ const text = await res.text();
2413
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2414
+ console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
2415
+ }
2416
+ return safeParseJson(text);
2417
+ }
2418
+ async function createPost(opts) {
2419
+ const profile = opts.profile ?? "default";
2420
+ const body = {
2421
+ title: opts.title,
2422
+ body: opts.body ?? ""
2423
+ };
2424
+ if (opts.enableComment !== void 0) body.enableComment = opts.enableComment;
2425
+ if (opts.sendNotifications !== void 0) body.sendNotifications = opts.sendNotifications;
2426
+ const url = `${BASE_URL6}/boards/${sanitizePathSegment(opts.boardId)}/posts`;
2427
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2428
+ console.error(`[nworks] POST ${url}`);
2429
+ console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
2430
+ }
2431
+ const res = await userFetch(
2432
+ url,
2433
+ {
2434
+ method: "POST",
2435
+ headers: { "Content-Type": "application/json" },
2436
+ body: JSON.stringify(body)
2437
+ },
2438
+ profile
2439
+ );
2440
+ if (res.status === 201 || res.ok) {
2441
+ const text = await res.text();
2442
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2443
+ console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
2444
+ }
2445
+ return safeParseJson(text);
2446
+ }
2447
+ return handleUserApiError(res);
2448
+ }
2449
+
2450
+ // src/mcp/tools/board.ts
2451
+ function registerBoardTools(server) {
2452
+ defineTool(
2453
+ server,
2122
2454
  "nworks_board_list",
2123
2455
  "NAVER WORKS \uAC8C\uC2DC\uD310 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uAC8C\uC2DC\uD310 \uBB50 \uC788\uC5B4?', '\uACF5\uC9C0\uC0AC\uD56D \uAC8C\uC2DC\uD310 \uCC3E\uC544\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (board.read scope)",
2124
2456
  {
2125
- count: z.number().optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20)"),
2126
- cursor: z.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
2457
+ count: z9.number().int().min(1).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20)"),
2458
+ cursor: z9.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
2127
2459
  },
2128
2460
  async ({ count, cursor }) => {
2129
2461
  try {
@@ -2144,13 +2476,14 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2144
2476
  }
2145
2477
  }
2146
2478
  );
2147
- server.tool(
2479
+ defineTool(
2480
+ server,
2148
2481
  "nworks_board_posts",
2149
2482
  "\uAC8C\uC2DC\uD310\uC758 \uAE00 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uAC8C\uC2DC\uD310 \uAE00 \uBCF4\uC5EC\uC918', '\uACF5\uC9C0\uC0AC\uD56D \uD655\uC778' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. boardId\uB294 nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (board.read scope)",
2150
2483
  {
2151
- boardId: z.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2152
- count: z.number().optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 40)"),
2153
- cursor: z.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
2484
+ boardId: z9.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2485
+ count: z9.number().int().min(1).max(40).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 40)"),
2486
+ cursor: z9.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
2154
2487
  },
2155
2488
  async ({ boardId, count, cursor }) => {
2156
2489
  try {
@@ -2164,107 +2497,409 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2164
2497
  createdTime: p.createdTime ?? ""
2165
2498
  }));
2166
2499
  return {
2167
- content: [{ type: "text", text: JSON.stringify({ posts, count: posts.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
2500
+ content: [{ type: "text", text: JSON.stringify({ posts, count: posts.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
2501
+ };
2502
+ } catch (err) {
2503
+ return {
2504
+ content: [{ type: "text", text: mcpErrorHint(err, "board.posts") }],
2505
+ isError: true
2506
+ };
2507
+ }
2508
+ }
2509
+ );
2510
+ defineTool(
2511
+ server,
2512
+ "nworks_board_read",
2513
+ "\uAC8C\uC2DC\uD310 \uAE00\uC758 \uC0C1\uC138 \uB0B4\uC6A9\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. postId\uB294 nworks_board_posts\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (board.read scope)",
2514
+ {
2515
+ boardId: z9.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2516
+ postId: z9.string().describe("\uAE00 ID (nworks_board_posts\uB85C \uC870\uD68C \uAC00\uB2A5)")
2517
+ },
2518
+ async ({ boardId, postId }) => {
2519
+ try {
2520
+ const post = await readPost(boardId, postId);
2521
+ return {
2522
+ content: [{ type: "text", text: JSON.stringify({
2523
+ postId: post.postId,
2524
+ boardId: post.boardId,
2525
+ title: post.title,
2526
+ body: post.body ?? "",
2527
+ userName: post.userName ?? "",
2528
+ readCount: post.readCount ?? 0,
2529
+ commentCount: post.commentCount ?? 0,
2530
+ createdTime: post.createdTime ?? "",
2531
+ updatedTime: post.updatedTime ?? ""
2532
+ }) }]
2533
+ };
2534
+ } catch (err) {
2535
+ return {
2536
+ content: [{ type: "text", text: mcpErrorHint(err, "board.read") }],
2537
+ isError: true
2538
+ };
2539
+ }
2540
+ }
2541
+ );
2542
+ defineTool(
2543
+ server,
2544
+ "nworks_board_create",
2545
+ "\uAC8C\uC2DC\uD310\uC5D0 \uAE00\uC744 \uC791\uC131\uD569\uB2C8\uB2E4. '\uAC8C\uC2DC\uD310\uC5D0 \uAE00 \uC62C\uB824\uC918', '\uACF5\uC9C0 \uC791\uC131\uD574\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. boardId\uB294 nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (board scope)",
2546
+ {
2547
+ boardId: z9.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2548
+ title: z9.string().describe("\uAE00 \uC81C\uBAA9"),
2549
+ body: z9.string().optional().describe("\uAE00 \uBCF8\uBB38"),
2550
+ enableComment: z9.boolean().optional().describe("\uB313\uAE00 \uD5C8\uC6A9 (\uAE30\uBCF8: true)"),
2551
+ sendNotifications: z9.boolean().optional().describe("\uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)")
2552
+ },
2553
+ async ({ boardId, title, body, enableComment, sendNotifications }) => {
2554
+ try {
2555
+ const post = await createPost({
2556
+ boardId,
2557
+ title,
2558
+ body,
2559
+ enableComment,
2560
+ sendNotifications
2561
+ });
2562
+ return {
2563
+ content: [{ type: "text", text: JSON.stringify({ success: true, postId: post.postId, boardId: post.boardId, title: post.title }) }]
2564
+ };
2565
+ } catch (err) {
2566
+ return {
2567
+ content: [{ type: "text", text: mcpErrorHint(err, "board.create") }],
2568
+ isError: true
2569
+ };
2570
+ }
2571
+ }
2572
+ );
2573
+ }
2574
+
2575
+ // src/mcp/tools/contact.ts
2576
+ import { z as z10 } from "zod";
2577
+
2578
+ // src/api/contact.ts
2579
+ var BASE_URL7 = "https://www.worksapis.com/v1.0";
2580
+ async function listContacts(opts = {}) {
2581
+ const userId = opts.userId ?? "me";
2582
+ const profile = opts.profile ?? "default";
2583
+ const params = new URLSearchParams();
2584
+ params.set("count", String(opts.count ?? 20));
2585
+ if (opts.cursor) params.set("cursor", opts.cursor);
2586
+ if (opts.accessibleRange) params.set("accessibleRange", opts.accessibleRange);
2587
+ if (opts.contactTagId) params.set("contactTagId", opts.contactTagId);
2588
+ if (opts.email) params.set("email", opts.email);
2589
+ if (opts.telephone) params.set("telephone", opts.telephone);
2590
+ if (opts.orderBy) params.set("orderBy", opts.orderBy);
2591
+ if (opts.searchDateType) params.set("searchDateType", opts.searchDateType);
2592
+ if (opts.startDateTime) params.set("startDateTime", opts.startDateTime);
2593
+ if (opts.endDateTime) params.set("endDateTime", opts.endDateTime);
2594
+ const url = `${BASE_URL7}/users/${sanitizePathSegment(userId)}/contacts?${params.toString()}`;
2595
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2596
+ console.error(`[nworks] GET ${url}`);
2597
+ }
2598
+ const res = await userFetch(url, { method: "GET" }, profile);
2599
+ if (!res.ok) return handleUserApiError(res);
2600
+ const data = await res.json();
2601
+ return { contacts: data.contacts ?? [], responseMetaData: data.responseMetaData };
2602
+ }
2603
+ async function getContact(contactId, profile = "default") {
2604
+ const url = `${BASE_URL7}/contacts/${sanitizePathSegment(contactId)}`;
2605
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2606
+ console.error(`[nworks] GET ${url}`);
2607
+ }
2608
+ const res = await userFetch(url, { method: "GET" }, profile);
2609
+ if (!res.ok) return handleUserApiError(res);
2610
+ return await res.json();
2611
+ }
2612
+ async function createContact(payload, profile = "default") {
2613
+ const url = `${BASE_URL7}/contacts`;
2614
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2615
+ console.error(`[nworks] POST ${url}`);
2616
+ }
2617
+ const res = await userFetch(
2618
+ url,
2619
+ {
2620
+ method: "POST",
2621
+ headers: { "Content-Type": "application/json" },
2622
+ body: JSON.stringify(payload)
2623
+ },
2624
+ profile
2625
+ );
2626
+ if (!res.ok) return handleUserApiError(res);
2627
+ return await res.json();
2628
+ }
2629
+ async function updateContact(contactId, payload, partial = true, profile = "default") {
2630
+ const url = `${BASE_URL7}/contacts/${sanitizePathSegment(contactId)}`;
2631
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2632
+ console.error(`[nworks] ${partial ? "PATCH" : "PUT"} ${url}`);
2633
+ }
2634
+ const res = await userFetch(
2635
+ url,
2636
+ {
2637
+ method: partial ? "PATCH" : "PUT",
2638
+ headers: { "Content-Type": "application/json" },
2639
+ body: JSON.stringify(payload)
2640
+ },
2641
+ profile
2642
+ );
2643
+ if (!res.ok) return handleUserApiError(res);
2644
+ return await res.json();
2645
+ }
2646
+ async function deleteContact(contactId, profile = "default") {
2647
+ const url = `${BASE_URL7}/contacts/${sanitizePathSegment(contactId)}`;
2648
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2649
+ console.error(`[nworks] DELETE ${url}`);
2650
+ }
2651
+ const res = await userFetch(url, { method: "DELETE" }, profile);
2652
+ if (res.status === 204) return;
2653
+ if (!res.ok) return handleUserApiError(res);
2654
+ }
2655
+ async function listContactTags(opts = {}) {
2656
+ const userId = opts.userId ?? "me";
2657
+ const profile = opts.profile ?? "default";
2658
+ const params = new URLSearchParams();
2659
+ params.set("count", String(opts.count ?? 20));
2660
+ if (opts.cursor) params.set("cursor", opts.cursor);
2661
+ const url = `${BASE_URL7}/users/${sanitizePathSegment(userId)}/contact-tags?${params.toString()}`;
2662
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2663
+ console.error(`[nworks] GET ${url}`);
2664
+ }
2665
+ const res = await userFetch(url, { method: "GET" }, profile);
2666
+ if (!res.ok) return handleUserApiError(res);
2667
+ const data = await res.json();
2668
+ return { contactTags: data.contactTags ?? [], responseMetaData: data.responseMetaData };
2669
+ }
2670
+
2671
+ // src/mcp/tools/contact.ts
2672
+ function registerContactTools(server) {
2673
+ defineTool(
2674
+ server,
2675
+ "nworks_contact_list",
2676
+ "NAVER WORKS \uAC1C\uC778 \uC5F0\uB77D\uCC98 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uC5F0\uB77D\uCC98 \uBAA9\uB85D \uBCF4\uC5EC\uC918', '\uB0B4 \uBA85\uD568\uCCA9 \uD655\uC778' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (contact.read scope)",
2677
+ {
2678
+ userId: z10.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
2679
+ count: z10.number().int().min(1).max(500).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 500)"),
2680
+ cursor: z10.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
2681
+ contactTagId: z10.string().optional().describe("\uC5F0\uB77D\uCC98 \uD0DC\uADF8 ID\uB85C \uD544\uD130\uB9C1 (nworks_contact_list_tags\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2682
+ email: z10.string().optional().describe("\uC774\uBA54\uC77C\uB85C \uD544\uD130\uB9C1"),
2683
+ telephone: z10.string().optional().describe("\uC804\uD654\uBC88\uD638\uB85C \uD544\uD130\uB9C1")
2684
+ },
2685
+ async ({ userId, count, cursor, contactTagId, email, telephone }) => {
2686
+ try {
2687
+ const result = await listContacts({
2688
+ userId: userId ?? "me",
2689
+ count: count ?? 20,
2690
+ cursor,
2691
+ contactTagId,
2692
+ email,
2693
+ telephone
2694
+ });
2695
+ return {
2696
+ content: [{ type: "text", text: JSON.stringify({ contacts: result.contacts, count: result.contacts.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
2697
+ };
2698
+ } catch (err) {
2699
+ return {
2700
+ content: [{ type: "text", text: mcpErrorHint(err, "contact.list") }],
2701
+ isError: true
2702
+ };
2703
+ }
2704
+ }
2705
+ );
2706
+ defineTool(
2707
+ server,
2708
+ "nworks_contact_get",
2709
+ "\uD2B9\uC815 \uC5F0\uB77D\uCC98\uC758 \uC0C1\uC138 \uC815\uBCF4\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. contactId\uB294 nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (contact.read scope)",
2710
+ {
2711
+ contactId: z10.string().describe("\uC5F0\uB77D\uCC98 ID (nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
2712
+ },
2713
+ async ({ contactId }) => {
2714
+ try {
2715
+ const result = await getContact(contactId);
2716
+ return {
2717
+ content: [{ type: "text", text: JSON.stringify(result) }]
2718
+ };
2719
+ } catch (err) {
2720
+ return {
2721
+ content: [{ type: "text", text: mcpErrorHint(err, "contact.get") }],
2722
+ isError: true
2723
+ };
2724
+ }
2725
+ }
2726
+ );
2727
+ defineTool(
2728
+ server,
2729
+ "nworks_contact_create",
2730
+ `\uC0C8 \uC5F0\uB77D\uCC98\uB97C \uB9CC\uB4ED\uB2C8\uB2E4. '\uC5F0\uB77D\uCC98 \uCD94\uAC00\uD574\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. payload\uB294 NAVER WORKS \uC5F0\uB77D\uCC98 API \uC2A4\uD399\uC5D0 \uB9DE\uB294 JSON \uAC1D\uCCB4\uB97C \uADF8\uB300\uB85C \uC804\uB2EC\uD569\uB2C8\uB2E4. contactName\uACFC permission\uC740 \uD544\uC218\uC774\uBA70, permission.accessibleMembers\uC5D0\uB294 \uCD5C\uC18C 1\uBA85\uC774 \uC788\uC5B4\uC57C \uD569\uB2C8\uB2E4(\uBCF4\uD1B5 \uBCF8\uC778). \uC608: {"contactName":{"lastName":"\uAE40","firstName":"\uCCA0\uC218"},"emails":[{"email":"a@b.com","primary":true}],"permission":{"accessibleRange":"MEMBER","isCoEditing":false,"accessibleMembers":[{"id":"<userId>","type":"USER"}]}}. User OAuth \uC778\uC99D \uD544\uC694 (contact scope)`,
2731
+ {
2732
+ payload: z10.record(z10.string(), z10.unknown()).describe("\uC5F0\uB77D\uCC98 \uD544\uB4DC. \uD544\uC218: contactName, permission(accessibleRange\xB7isCoEditing\xB7accessibleMembers \uCD5C\uC18C 1\uBA85). \uC120\uD0DD: emails[], telephones[], organizations[], memo")
2733
+ },
2734
+ async ({ payload }) => {
2735
+ try {
2736
+ const result = await createContact(payload);
2737
+ return {
2738
+ content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
2739
+ };
2740
+ } catch (err) {
2741
+ return {
2742
+ content: [{ type: "text", text: mcpErrorHint(err, "contact.create") }],
2743
+ isError: true
2744
+ };
2745
+ }
2746
+ }
2747
+ );
2748
+ defineTool(
2749
+ server,
2750
+ "nworks_contact_update",
2751
+ "\uAE30\uC874 \uC5F0\uB77D\uCC98\uB97C \uC218\uC815\uD569\uB2C8\uB2E4. contactId\uB294 nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5. payload\uB294 \uC218\uC815\uD560 \uD544\uB4DC\uB9CC \uB2F4\uC740 JSON \uAC1D\uCCB4\uC785\uB2C8\uB2E4. User OAuth \uC778\uC99D \uD544\uC694 (contact scope)",
2752
+ {
2753
+ contactId: z10.string().describe("\uC5F0\uB77D\uCC98 ID (nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2754
+ payload: z10.record(z10.string(), z10.unknown()).describe("\uC218\uC815\uD560 \uD544\uB4DC (name, email, tel, company \uB4F1)"),
2755
+ replace: z10.boolean().optional().describe("\uC804\uCCB4 \uAD50\uCCB4(PUT) \uC5EC\uBD80 (\uAE30\uBCF8: false, \uBD80\uBD84 \uC218\uC815 PATCH)")
2756
+ },
2757
+ async ({ contactId, payload, replace }) => {
2758
+ try {
2759
+ const result = await updateContact(contactId, payload, !(replace ?? false));
2760
+ return {
2761
+ content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
2168
2762
  };
2169
2763
  } catch (err) {
2170
2764
  return {
2171
- content: [{ type: "text", text: mcpErrorHint(err, "board.posts") }],
2765
+ content: [{ type: "text", text: mcpErrorHint(err, "contact.update") }],
2172
2766
  isError: true
2173
2767
  };
2174
2768
  }
2175
2769
  }
2176
2770
  );
2177
- server.tool(
2178
- "nworks_board_read",
2179
- "\uAC8C\uC2DC\uD310 \uAE00\uC758 \uC0C1\uC138 \uB0B4\uC6A9\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. postId\uB294 nworks_board_posts\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (board.read scope)",
2771
+ defineTool(
2772
+ server,
2773
+ "nworks_contact_delete",
2774
+ "\uC5F0\uB77D\uCC98\uB97C \uC0AD\uC81C\uD569\uB2C8\uB2E4. contactId\uB294 nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (contact scope)",
2180
2775
  {
2181
- boardId: z.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2182
- postId: z.string().describe("\uAE00 ID (nworks_board_posts\uB85C \uC870\uD68C \uAC00\uB2A5)")
2776
+ contactId: z10.string().describe("\uC0AD\uC81C\uD560 \uC5F0\uB77D\uCC98 ID (nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
2183
2777
  },
2184
- async ({ boardId, postId }) => {
2778
+ async ({ contactId }) => {
2185
2779
  try {
2186
- const post = await readPost(boardId, postId);
2780
+ await deleteContact(contactId);
2187
2781
  return {
2188
- content: [{ type: "text", text: JSON.stringify({
2189
- postId: post.postId,
2190
- boardId: post.boardId,
2191
- title: post.title,
2192
- body: post.body ?? "",
2193
- userName: post.userName ?? "",
2194
- readCount: post.readCount ?? 0,
2195
- commentCount: post.commentCount ?? 0,
2196
- createdTime: post.createdTime ?? "",
2197
- updatedTime: post.updatedTime ?? ""
2198
- }) }]
2782
+ content: [{ type: "text", text: JSON.stringify({ success: true, contactId, message: "\uC5F0\uB77D\uCC98\uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4" }) }]
2199
2783
  };
2200
2784
  } catch (err) {
2201
2785
  return {
2202
- content: [{ type: "text", text: mcpErrorHint(err, "board.read") }],
2786
+ content: [{ type: "text", text: mcpErrorHint(err, "contact.delete") }],
2203
2787
  isError: true
2204
2788
  };
2205
2789
  }
2206
2790
  }
2207
2791
  );
2208
- server.tool(
2209
- "nworks_board_create",
2210
- "\uAC8C\uC2DC\uD310\uC5D0 \uAE00\uC744 \uC791\uC131\uD569\uB2C8\uB2E4. '\uAC8C\uC2DC\uD310\uC5D0 \uAE00 \uC62C\uB824\uC918', '\uACF5\uC9C0 \uC791\uC131\uD574\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. boardId\uB294 nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5. User OAuth \uC778\uC99D \uD544\uC694 (board scope)",
2792
+ defineTool(
2793
+ server,
2794
+ "nworks_contact_list_tags",
2795
+ "\uC5F0\uB77D\uCC98 \uD0DC\uADF8(\uADF8\uB8F9) \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. '\uC5F0\uB77D\uCC98 \uD0DC\uADF8 \uBAA9\uB85D \uBCF4\uC5EC\uC918' \uB4F1\uC758 \uC694\uCCAD\uC5D0 \uC0AC\uC6A9. User OAuth \uC778\uC99D \uD544\uC694 (contact.read scope)",
2211
2796
  {
2212
- boardId: z.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2213
- title: z.string().describe("\uAE00 \uC81C\uBAA9"),
2214
- body: z.string().optional().describe("\uAE00 \uBCF8\uBB38"),
2215
- enableComment: z.boolean().optional().describe("\uB313\uAE00 \uD5C8\uC6A9 (\uAE30\uBCF8: true)"),
2216
- sendNotifications: z.boolean().optional().describe("\uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)")
2797
+ userId: z10.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
2798
+ count: z10.number().int().min(1).max(500).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 500)"),
2799
+ cursor: z10.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
2217
2800
  },
2218
- async ({ boardId, title, body, enableComment, sendNotifications }) => {
2801
+ async ({ userId, count, cursor }) => {
2219
2802
  try {
2220
- const post = await createPost({
2221
- boardId,
2222
- title,
2223
- body,
2224
- enableComment,
2225
- sendNotifications
2226
- });
2803
+ const result = await listContactTags({ userId: userId ?? "me", count: count ?? 20, cursor });
2227
2804
  return {
2228
- content: [{ type: "text", text: JSON.stringify({ success: true, postId: post.postId, boardId: post.boardId, title: post.title }) }]
2805
+ content: [{ type: "text", text: JSON.stringify({ contactTags: result.contactTags, count: result.contactTags.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
2229
2806
  };
2230
2807
  } catch (err) {
2231
2808
  return {
2232
- content: [{ type: "text", text: mcpErrorHint(err, "board.create") }],
2809
+ content: [{ type: "text", text: mcpErrorHint(err, "contact.list_tags") }],
2233
2810
  isError: true
2234
2811
  };
2235
2812
  }
2236
2813
  }
2237
2814
  );
2238
- server.tool(
2815
+ }
2816
+
2817
+ // src/mcp/tools/auth.ts
2818
+ import { z as z11 } from "zod";
2819
+
2820
+ // src/auth/scopes.ts
2821
+ var READ_SCOPES = [
2822
+ "calendar.read",
2823
+ "file.read",
2824
+ "mail.read",
2825
+ "task.read",
2826
+ "user.read",
2827
+ "board.read",
2828
+ "contact.read"
2829
+ ];
2830
+ var ALL_SCOPES = [
2831
+ "calendar",
2832
+ "calendar.read",
2833
+ "file",
2834
+ "file.read",
2835
+ "mail",
2836
+ "mail.read",
2837
+ "task",
2838
+ "task.read",
2839
+ "user.read",
2840
+ "board",
2841
+ "board.read",
2842
+ "contact",
2843
+ "contact.read"
2844
+ ];
2845
+ var SCOPE_DEPENDENCIES = {
2846
+ calendar: ["calendar.read"],
2847
+ file: ["file.read"],
2848
+ mail: ["mail.read"],
2849
+ task: ["task.read", "user.read"],
2850
+ "task.read": ["user.read"],
2851
+ board: ["board.read"],
2852
+ contact: ["contact.read"]
2853
+ };
2854
+ var SCOPE_PRESETS = {
2855
+ readonly: READ_SCOPES,
2856
+ // default를 all로 두는 이유: 현재 제품의 "한 번 로그인으로 전체 기능" 약속을 보존한다.
2857
+ // 읽기 전용을 기본값으로 하면 신규 사용자가 쓰기 기능(일정 생성 등)을 쓸 때 재로그인해야 한다.
2858
+ // 안전을 최우선하려면 readonly로 바꾸면 되지만, 삭제류 도구는 이미 destructiveHint
2859
+ // annotation으로 클라이언트 승인 게이트가 걸린다.
2860
+ default: ALL_SCOPES,
2861
+ all: ALL_SCOPES
2862
+ };
2863
+ function isPreset(value) {
2864
+ return value === "readonly" || value === "default" || value === "all";
2865
+ }
2866
+ function expandScopeDependencies(scopes) {
2867
+ const expanded = new Set(scopes);
2868
+ for (const scope of scopes) {
2869
+ const deps = SCOPE_DEPENDENCIES[scope];
2870
+ if (deps) deps.forEach((d) => expanded.add(d));
2871
+ }
2872
+ return [...expanded];
2873
+ }
2874
+ function resolveScopes(input) {
2875
+ if (!input) return SCOPE_PRESETS.default;
2876
+ if (isPreset(input)) return SCOPE_PRESETS[input];
2877
+ return expandScopeDependencies(input.split(" ").filter(Boolean));
2878
+ }
2879
+ function mergeScopes(existing, requested) {
2880
+ return [.../* @__PURE__ */ new Set([...existing, ...requested])].join(" ");
2881
+ }
2882
+
2883
+ // src/mcp/tools/auth.ts
2884
+ function registerAuthTools(server) {
2885
+ defineTool(
2886
+ server,
2239
2887
  "nworks_login_user",
2240
- "User OAuth \uB85C\uADF8\uC778\uC744 \uC2DC\uC791\uD569\uB2C8\uB2E4. \uBC18\uD658\uB41C URL\uC744 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5F4\uC5B4 NAVER WORKS\uC5D0 \uB85C\uADF8\uC778\uD558\uC138\uC694. \uB85C\uADF8\uC778 \uC644\uB8CC \uD6C4 \uC790\uB3D9\uC73C\uB85C \uD1A0\uD070\uC774 \uC800\uC7A5\uB429\uB2C8\uB2E4. \uC911\uC694: scope\uB97C \uC9C0\uC815\uD558\uC9C0 \uB9C8\uC138\uC694. \uAE30\uBCF8\uAC12\uC774 \uBAA8\uB4E0 API(\uCE98\uB9B0\uB354, \uBA54\uC77C, \uD560\uC77C, \uB4DC\uB77C\uC774\uBE0C, \uAC8C\uC2DC\uD310)\uB97C \uD3EC\uD568\uD558\uBBC0\uB85C \uD55C \uBC88 \uB85C\uADF8\uC778\uC73C\uB85C \uC804\uCCB4 \uAE30\uB2A5\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4. scope\uB97C \uC881\uAC8C \uC9C0\uC815\uD558\uBA74 \uB2E4\uB978 \uAE30\uB2A5 \uC0AC\uC6A9 \uC2DC \uC7AC\uB85C\uADF8\uC778\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.",
2888
+ "User OAuth \uB85C\uADF8\uC778\uC744 \uC2DC\uC791\uD569\uB2C8\uB2E4. \uBC18\uD658\uB41C URL\uC744 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5F4\uC5B4 NAVER WORKS\uC5D0 \uB85C\uADF8\uC778\uD558\uC138\uC694. \uB85C\uADF8\uC778 \uC644\uB8CC \uD6C4 \uC790\uB3D9\uC73C\uB85C \uD1A0\uD070\uC774 \uC800\uC7A5\uB429\uB2C8\uB2E4. \uC911\uC694: preset\uC744 \uC9C0\uC815\uD558\uC9C0 \uB9C8\uC138\uC694. \uAE30\uBCF8\uAC12(default=all)\uC774 \uBAA8\uB4E0 API(\uCE98\uB9B0\uB354, \uBA54\uC77C, \uD560\uC77C, \uB4DC\uB77C\uC774\uBE0C, \uAC8C\uC2DC\uD310)\uB97C \uD3EC\uD568\uD558\uBBC0\uB85C \uD55C \uBC88 \uB85C\uADF8\uC778\uC73C\uB85C \uC804\uCCB4 \uAE30\uB2A5\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4. \uC77D\uAE30 \uC804\uC6A9\uC73C\uB85C \uC81C\uD55C\uD558\uB824\uBA74 preset='readonly'\uB97C \uC9C0\uC815\uD558\uC138\uC694(\uC774 \uACBD\uC6B0 \uC0DD\uC131/\uC218\uC815/\uC0AD\uC81C \uB3C4\uAD6C\uB294 \uC7AC\uB85C\uADF8\uC778 \uD544\uC694).",
2241
2889
  {
2242
- scope: z.string().optional().describe("\uC9C0\uC815\uD558\uC9C0 \uB9C8\uC138\uC694 (\uAE30\uBCF8\uAC12\uC774 \uC804\uCCB4 scope \uD3EC\uD568). \uD2B9\uC218\uD55C \uACBD\uC6B0\uC5D0\uB9CC \uC0AC\uC6A9")
2890
+ preset: z11.enum(["readonly", "default", "all"]).optional().describe("scope \uD504\uB9AC\uC14B. \uAE30\uBCF8 default(=all, \uC804\uCCB4 \uAE30\uB2A5). readonly\uB294 \uC77D\uAE30 \uC804\uC6A9."),
2891
+ scope: z11.string().optional().describe("\uACE0\uAE09: \uACF5\uBC31 \uAD6C\uBD84 raw scope \uC9C1\uC811 \uC9C0\uC815(preset \uB300\uC2E0). \uD2B9\uC218\uD55C \uACBD\uC6B0\uC5D0\uB9CC \uC0AC\uC6A9")
2243
2892
  },
2244
- async ({ scope }) => {
2245
- const DEFAULT_SCOPE = "calendar calendar.read file file.read mail mail.read task task.read user.read board board.read";
2893
+ async ({ preset, scope }) => {
2246
2894
  try {
2247
2895
  const creds = await loadCredentials();
2248
- const SCOPE_DEPS = {
2249
- calendar: ["calendar.read"],
2250
- task: ["user.read"],
2251
- "task.read": ["user.read"]
2252
- };
2253
- const expandScopes = (scopes) => {
2254
- const expanded = new Set(scopes);
2255
- for (const s of scopes) {
2256
- const deps = SCOPE_DEPS[s];
2257
- if (deps) deps.forEach((d) => expanded.add(d));
2258
- }
2259
- return [...expanded];
2260
- };
2261
2896
  const existingToken = await loadUserToken();
2262
2897
  const existingScopes = existingToken?.scope?.split(" ").filter(Boolean) ?? [];
2263
- const requestedScopes = expandScopes((scope ?? DEFAULT_SCOPE).split(" ").filter(Boolean));
2264
- const mergedScopes = [.../* @__PURE__ */ new Set([...existingScopes, ...requestedScopes])].join(" ");
2265
- const state = Math.random().toString(36).substring(2);
2898
+ const requestedScopes = resolveScopes(preset ?? scope);
2899
+ const mergedScopes = mergeScopes(existingScopes, requestedScopes);
2900
+ const state = generateSecureState();
2266
2901
  const authorizeUrl = buildAuthorizeUrl(creds.clientId, mergedScopes, state);
2267
- startOAuthCallbackServer(creds.clientId, creds.clientSecret).then(
2902
+ startOAuthCallbackServer(creds.clientId, creds.clientSecret, state).then(
2268
2903
  (token) => saveUserToken({
2269
2904
  accessToken: token.accessToken,
2270
2905
  refreshToken: token.refreshToken,
@@ -2298,7 +2933,8 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2298
2933
  }
2299
2934
  }
2300
2935
  );
2301
- server.tool(
2936
+ defineTool(
2937
+ server,
2302
2938
  "nworks_whoami",
2303
2939
  "\uD604\uC7AC \uC778\uC99D\uB41C NAVER WORKS \uACC4\uC815 \uC815\uBCF4\uC640 \uD1A0\uD070 \uC720\uD6A8 \uC0C1\uD0DC\uB97C \uD655\uC778\uD569\uB2C8\uB2E4. \uC778\uC99D \uBB38\uC81C \uC9C4\uB2E8 \uC2DC \uBA3C\uC800 \uD638\uCD9C",
2304
2940
  {},
@@ -2309,9 +2945,10 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2309
2945
  const userToken = await loadUserToken();
2310
2946
  const isValid = token ? token.expiresAt > Date.now() / 1e3 : false;
2311
2947
  const userTokenValid = userToken ? userToken.expiresAt > Date.now() / 1e3 : false;
2948
+ const mask2 = (s) => s.length <= 4 ? "****" : `****${s.slice(-4)}`;
2312
2949
  const info = {
2313
2950
  serviceAccount: creds.serviceAccount ?? null,
2314
- clientId: creds.clientId,
2951
+ clientId: mask2(creds.clientId),
2315
2952
  botId: creds.botId ?? null,
2316
2953
  tokenValid: isValid,
2317
2954
  userOAuth: userToken ? { valid: userTokenValid, scope: userToken.scope } : null
@@ -2327,12 +2964,25 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2327
2964
  }
2328
2965
  }
2329
2966
  );
2330
- server.tool(
2967
+ defineTool(
2968
+ server,
2331
2969
  "nworks_logout",
2332
2970
  "\uC800\uC7A5\uB41C NAVER WORKS \uC778\uC99D \uC815\uBCF4\uC640 \uD1A0\uD070\uC744 \uBAA8\uB450 \uC0AD\uC81C\uD569\uB2C8\uB2E4",
2333
2971
  {},
2334
2972
  async () => {
2335
2973
  try {
2974
+ try {
2975
+ const creds = await loadCredentials();
2976
+ const token = await loadToken();
2977
+ const userToken = await loadUserToken();
2978
+ if (token?.accessToken) {
2979
+ await revokeToken(token.accessToken, creds.clientId, creds.clientSecret);
2980
+ }
2981
+ if (userToken?.refreshToken) {
2982
+ await revokeToken(userToken.refreshToken, creds.clientId, creds.clientSecret);
2983
+ }
2984
+ } catch {
2985
+ }
2336
2986
  await clearCredentials();
2337
2987
  return {
2338
2988
  content: [
@@ -2353,15 +3003,263 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2353
3003
  }
2354
3004
  }
2355
3005
  );
2356
- server.tool(
3006
+ }
3007
+
3008
+ // src/commands/doctor.ts
3009
+ import { Command } from "commander";
3010
+ import { existsSync as existsSync2 } from "fs";
3011
+ import { readFile as readFile4 } from "fs/promises";
3012
+ import { resolve as resolve2, join as join2, sep as sep2 } from "path";
3013
+
3014
+ // src/output/format.ts
3015
+ function output(data, opts = {}) {
3016
+ const useJson = opts.json || !process.stdout.isTTY;
3017
+ if (useJson) {
3018
+ console.log(JSON.stringify(data, null, 2));
3019
+ return;
3020
+ }
3021
+ if (Array.isArray(data)) {
3022
+ printTable(data);
3023
+ } else if (typeof data === "object" && data !== null) {
3024
+ const record = data;
3025
+ for (const value of Object.values(record)) {
3026
+ if (Array.isArray(value) && value.length > 0) {
3027
+ printTable(value);
3028
+ return;
3029
+ }
3030
+ }
3031
+ for (const [key, value] of Object.entries(record)) {
3032
+ console.log(` ${key}: ${String(value)}`);
3033
+ }
3034
+ } else {
3035
+ console.log(String(data));
3036
+ }
3037
+ }
3038
+ function printTable(rows) {
3039
+ if (rows.length === 0) {
3040
+ console.log(" (no data)");
3041
+ return;
3042
+ }
3043
+ const first = rows[0];
3044
+ const keys = Object.keys(first);
3045
+ const widths = {};
3046
+ for (const key of keys) {
3047
+ widths[key] = key.length;
3048
+ }
3049
+ for (const row of rows) {
3050
+ const record = row;
3051
+ for (const key of keys) {
3052
+ const len = String(record[key] ?? "").length;
3053
+ if (len > (widths[key] ?? 0)) {
3054
+ widths[key] = len;
3055
+ }
3056
+ }
3057
+ }
3058
+ const header = keys.map((k) => k.padEnd(widths[k] ?? 0)).join(" ");
3059
+ const separator = keys.map((k) => "\u2500".repeat(widths[k] ?? 0)).join("\u2500\u2500");
3060
+ console.log(` ${header}`);
3061
+ console.log(` ${separator}`);
3062
+ for (const row of rows) {
3063
+ const record = row;
3064
+ const line = keys.map((k) => String(record[k] ?? "").padEnd(widths[k] ?? 0)).join(" ");
3065
+ console.log(` ${line}`);
3066
+ }
3067
+ }
3068
+ function errorOutput(error, opts = {}) {
3069
+ const payload = {
3070
+ success: false,
3071
+ error: { code: error.code ?? "ERROR", message: error.message, hint: error.hint }
3072
+ };
3073
+ if (opts.json || !process.stderr.isTTY) {
3074
+ console.error(JSON.stringify(payload, null, 2));
3075
+ } else {
3076
+ console.error(` Error: ${error.message}`);
3077
+ if (error.code) {
3078
+ console.error(` Code: ${error.code}`);
3079
+ }
3080
+ if (error.hint) {
3081
+ console.error(` \u2192 ${error.hint}`);
3082
+ }
3083
+ }
3084
+ }
3085
+
3086
+ // src/output/cli-error.ts
3087
+ function cliError(err, opts = {}, area) {
3088
+ const error = err;
3089
+ if (error instanceof ApiError) {
3090
+ errorOutput(
3091
+ {
3092
+ code: error.code,
3093
+ message: error.message,
3094
+ hint: cliErrorHint(err, area).split("\n").slice(1).map((l) => l.replace(/^\s+→\s*/, "")).join(" ")
3095
+ },
3096
+ opts
3097
+ );
3098
+ } else if (error instanceof AuthError) {
3099
+ errorOutput(
3100
+ {
3101
+ code: "AUTH_ERROR",
3102
+ message: error.message,
3103
+ hint: "\uC778\uC99D\uC774 \uB9CC\uB8CC\uB410\uC2B5\uB2C8\uB2E4. `nworks login`\uC73C\uB85C \uB2E4\uC2DC \uB85C\uADF8\uC778\uD558\uC138\uC694."
3104
+ },
3105
+ opts
3106
+ );
3107
+ } else {
3108
+ errorOutput({ message: error.message }, opts);
3109
+ }
3110
+ process.exitCode = 1;
3111
+ }
3112
+
3113
+ // src/commands/doctor.ts
3114
+ function mask(value) {
3115
+ return value.length <= 4 ? "****" : `****${value.slice(-4)}`;
3116
+ }
3117
+ async function runChecks(profile) {
3118
+ const results = [];
3119
+ let creds;
3120
+ try {
3121
+ creds = await loadCredentials(profile);
3122
+ results.push({ check: "credentials", status: "OK", detail: `clientId: ${mask(creds.clientId)}` });
3123
+ } catch {
3124
+ results.push({ check: "credentials", status: "FAIL", detail: "No credentials found. CLI: run `nworks login --user`. MCP: use the nworks_setup tool (env NWORKS_CLIENT_SECRET required)." });
3125
+ return results;
3126
+ }
3127
+ if (hasServiceAccountCreds(creds)) {
3128
+ results.push({ check: "serviceAccount", status: "OK", detail: creds.serviceAccount });
3129
+ } else {
3130
+ results.push({ check: "serviceAccount", status: "SKIP", detail: "Not set (required for bot messaging)" });
3131
+ }
3132
+ if (creds.privateKeyPath) {
3133
+ if (existsSync2(creds.privateKeyPath)) {
3134
+ try {
3135
+ await readFile4(creds.privateKeyPath, "utf-8");
3136
+ results.push({ check: "privateKey", status: "OK", detail: creds.privateKeyPath });
3137
+ } catch {
3138
+ results.push({ check: "privateKey", status: "FAIL", detail: `Cannot read: ${creds.privateKeyPath}` });
3139
+ }
3140
+ } else {
3141
+ results.push({ check: "privateKey", status: "FAIL", detail: `File not found: ${creds.privateKeyPath}` });
3142
+ }
3143
+ } else {
3144
+ results.push({ check: "privateKey", status: "SKIP", detail: "Not set" });
3145
+ }
3146
+ if (creds.privateKeyPath && existsSync2(creds.privateKeyPath)) {
3147
+ const resolvedKey = resolve2(creds.privateKeyPath);
3148
+ const cwd = resolve2(process.cwd());
3149
+ const insideCwd = resolvedKey === cwd || resolvedKey.startsWith(cwd + sep2);
3150
+ const looksLikeProject = existsSync2(join2(cwd, ".git")) || existsSync2(join2(cwd, "package.json"));
3151
+ if (insideCwd && looksLikeProject) {
3152
+ results.push({
3153
+ check: "keyLocation",
3154
+ status: "WARN",
3155
+ detail: "Private key is inside a project directory. Risk of exposure via editor plugins, AI tools, or `npm pack` \u2014 move it outside (e.g. your home directory) and update the path."
3156
+ });
3157
+ }
3158
+ }
3159
+ if (creds.botId) {
3160
+ results.push({ check: "botId", status: "OK", detail: creds.botId });
3161
+ } else {
3162
+ results.push({ check: "botId", status: "SKIP", detail: "Not set (required to send messages)" });
3163
+ }
3164
+ const token = await loadToken(profile);
3165
+ if (token) {
3166
+ const valid = token.expiresAt > Date.now() / 1e3;
3167
+ results.push({
3168
+ check: "serviceToken",
3169
+ status: valid ? "OK" : "EXPIRED",
3170
+ detail: valid ? `expires: ${new Date(token.expiresAt * 1e3).toISOString()}` : `expired: ${new Date(token.expiresAt * 1e3).toISOString()}`
3171
+ });
3172
+ } else {
3173
+ results.push({ check: "serviceToken", status: "SKIP", detail: "No token" });
3174
+ }
3175
+ const userToken = await loadUserToken(profile);
3176
+ if (userToken) {
3177
+ const valid = userToken.expiresAt > Date.now() / 1e3;
3178
+ results.push({
3179
+ check: "userOAuth",
3180
+ status: valid ? "OK" : "EXPIRED",
3181
+ detail: valid ? `scope: ${userToken.scope} | expires: ${new Date(userToken.expiresAt * 1e3).toISOString()}` : `expired | scope: ${userToken.scope}`
3182
+ });
3183
+ } else {
3184
+ results.push({ check: "userOAuth", status: "SKIP", detail: "No token. Run `nworks login --user`." });
3185
+ }
3186
+ if (hasServiceAccountCreds(creds) && token && token.expiresAt > Date.now() / 1e3) {
3187
+ try {
3188
+ const res = await fetch("https://www.worksapis.com/v1.0/users/me", {
3189
+ headers: { Authorization: `Bearer ${token.accessToken}` }
3190
+ });
3191
+ if (res.ok) {
3192
+ results.push({ check: "apiConnection", status: "OK", detail: "Connected to NAVER WORKS API" });
3193
+ } else {
3194
+ results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
3195
+ }
3196
+ } catch (e) {
3197
+ results.push({ check: "apiConnection", status: "FAIL", detail: `Connection failed: ${e.message}` });
3198
+ }
3199
+ } else if (userToken && userToken.expiresAt > Date.now() / 1e3) {
3200
+ try {
3201
+ const res = await fetch("https://www.worksapis.com/v1.0/users/me", {
3202
+ headers: { Authorization: `Bearer ${userToken.accessToken}` }
3203
+ });
3204
+ if (res.ok) {
3205
+ results.push({ check: "apiConnection", status: "OK", detail: "Connected to NAVER WORKS API" });
3206
+ } else {
3207
+ results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
3208
+ }
3209
+ } catch (e) {
3210
+ results.push({ check: "apiConnection", status: "FAIL", detail: `Connection failed: ${e.message}` });
3211
+ }
3212
+ } else {
3213
+ results.push({ check: "apiConnection", status: "SKIP", detail: "No valid token \u2014 skipping API test" });
3214
+ }
3215
+ return results;
3216
+ }
3217
+ var doctorCommand = new Command("doctor").description("Check nworks configuration and connectivity").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
3218
+ try {
3219
+ const results = await runChecks(opts.profile);
3220
+ if (opts.json || !process.stdout.isTTY) {
3221
+ output(results, opts);
3222
+ } else {
3223
+ console.log("\n nworks doctor\n");
3224
+ for (const r of results) {
3225
+ const icon = r.status === "OK" ? "\u2705" : r.status === "SKIP" ? "\u2796" : r.status === "WARN" ? "\u26A0\uFE0F" : "\u274C";
3226
+ console.log(` ${icon} ${r.check.padEnd(16)} ${r.detail}`);
3227
+ }
3228
+ console.log();
3229
+ }
3230
+ if (results.some((r) => r.status === "FAIL")) {
3231
+ process.exitCode = 1;
3232
+ }
3233
+ } catch (err) {
3234
+ cliError(err, opts);
3235
+ }
3236
+ });
3237
+
3238
+ // src/mcp/tools/diagnostics.ts
3239
+ function registerDiagnosticsTools(server) {
3240
+ defineTool(
3241
+ server,
2357
3242
  "nworks_doctor",
2358
3243
  "NAVER WORKS \uC5F0\uACB0 \uC0C1\uD0DC\uB97C \uC9C4\uB2E8\uD569\uB2C8\uB2E4. \uC778\uC99D \uC815\uBCF4, \uD1A0\uD070, Private Key, API \uC5F0\uACB0\uC744 \uC810\uAC80\uD569\uB2C8\uB2E4.",
2359
3244
  {},
2360
3245
  async () => {
2361
3246
  try {
2362
3247
  const results = await runChecks("default");
3248
+ const maskedResults = results.map((r) => {
3249
+ if (r.check === "credentials" && r.status === "OK") {
3250
+ return { ...r, detail: r.detail.replace(/clientId: .+/, "clientId: ****") };
3251
+ }
3252
+ if (r.check === "privateKey" && r.status === "OK") {
3253
+ return { ...r, detail: "OK (path hidden)" };
3254
+ }
3255
+ if (r.check === "serviceAccount" && r.status === "OK") {
3256
+ const masked = r.detail.length <= 4 ? "****" : `****${r.detail.slice(-4)}`;
3257
+ return { ...r, detail: masked };
3258
+ }
3259
+ return r;
3260
+ });
2363
3261
  return {
2364
- content: [{ type: "text", text: JSON.stringify(results) }]
3262
+ content: [{ type: "text", text: JSON.stringify(maskedResults) }]
2365
3263
  };
2366
3264
  } catch (err) {
2367
3265
  return {
@@ -2373,6 +3271,27 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2373
3271
  );
2374
3272
  }
2375
3273
 
3274
+ // src/mcp/tools.ts
3275
+ function registerTools(server) {
3276
+ registerSetupTools(server);
3277
+ registerMessageTools(server);
3278
+ registerDirectoryTools(server);
3279
+ registerCalendarTools(server);
3280
+ registerDriveTools(server);
3281
+ registerMailTools(server);
3282
+ registerTaskTools(server);
3283
+ registerBoardTools(server);
3284
+ registerContactTools(server);
3285
+ registerAuthTools(server);
3286
+ registerDiagnosticsTools(server);
3287
+ }
3288
+
3289
+ // src/version.ts
3290
+ import { createRequire } from "module";
3291
+ var require2 = createRequire(import.meta.url);
3292
+ var pkg = require2("../package.json");
3293
+ var VERSION = pkg.version;
3294
+
2376
3295
  // src/mcp/server.ts
2377
3296
  async function startMcpServer() {
2378
3297
  try {
@@ -2393,7 +3312,7 @@ async function startMcpServer() {
2393
3312
  }
2394
3313
  const server = new McpServer({
2395
3314
  name: "nworks",
2396
- version: "1.0.0"
3315
+ version: VERSION
2397
3316
  });
2398
3317
  registerTools(server);
2399
3318
  const transport = new StdioServerTransport();