nworks 1.2.2 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import { Command as Command14 } from "commander";
5
+
6
+ // src/version.ts
4
7
  import { createRequire } from "module";
5
- import { Command as Command13 } from "commander";
8
+ var require2 = createRequire(import.meta.url);
9
+ var pkg = require2("../package.json");
10
+ var VERSION = pkg.version;
6
11
 
7
12
  // src/commands/login.ts
8
13
  import { Command } from "commander";
@@ -11,10 +16,11 @@ import { existsSync as existsSync2 } from "fs";
11
16
  import { createInterface } from "readline/promises";
12
17
 
13
18
  // src/auth/config.ts
14
- import { readFile, writeFile, mkdir } from "fs/promises";
19
+ import { readFile, writeFile, mkdir, chmod, rename } from "fs/promises";
15
20
  import { existsSync } from "fs";
16
21
  import { homedir } from "os";
17
22
  import { join } from "path";
23
+ import { z } from "zod";
18
24
 
19
25
  // src/utils/error.ts
20
26
  var AuthError = class extends Error {
@@ -43,11 +49,40 @@ var CONFIG_DIR = join(homedir(), ".config", "nworks");
43
49
  var CREDENTIALS_PATH = join(CONFIG_DIR, "credentials.json");
44
50
  var TOKEN_PATH = join(CONFIG_DIR, "token.json");
45
51
  var USER_TOKEN_PATH = join(CONFIG_DIR, "user-token.json");
52
+ var PROFILE_NAME_PATTERN = /^[\w.-]+$/;
53
+ var FORBIDDEN_PROFILE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
54
+ var DOTS_ONLY = /^\.+$/;
55
+ function validateProfileName(profile) {
56
+ if (!PROFILE_NAME_PATTERN.test(profile) || FORBIDDEN_PROFILE_KEYS.has(profile) || DOTS_ONLY.test(profile)) {
57
+ throw new AuthError(
58
+ `Invalid profile name "${profile}". Use only letters, digits, ".", "_", or "-" (and not "." / ".." alone).`
59
+ );
60
+ }
61
+ }
62
+ var credentialsSchema = z.object({
63
+ clientId: z.string(),
64
+ clientSecret: z.string(),
65
+ serviceAccount: z.string().optional(),
66
+ privateKeyPath: z.string().optional(),
67
+ botId: z.string().optional(),
68
+ domainId: z.string().optional()
69
+ }).passthrough();
46
70
  async function ensureConfigDir() {
47
71
  if (!existsSync(CONFIG_DIR)) {
48
72
  await mkdir(CONFIG_DIR, { recursive: true, ...IS_UNIX && { mode: 448 } });
73
+ } else if (IS_UNIX) {
74
+ await chmod(CONFIG_DIR, 448);
49
75
  }
50
76
  }
77
+ var tempFileCounter = 0;
78
+ async function writeSecureFile(path, data) {
79
+ const tempPath = `${path}.${process.pid}.${tempFileCounter++}.tmp`;
80
+ await writeFile(tempPath, data, { encoding: "utf-8", ...IS_UNIX && { mode: 384 } });
81
+ if (IS_UNIX) {
82
+ await chmod(tempPath, 384);
83
+ }
84
+ await rename(tempPath, path);
85
+ }
51
86
  function getCredentialsFromEnv() {
52
87
  const clientId = process.env["NWORKS_CLIENT_ID"];
53
88
  const clientSecret = process.env["NWORKS_CLIENT_SECRET"];
@@ -64,6 +99,7 @@ function getCredentialsFromEnv() {
64
99
  async function loadCredentials(profile = "default") {
65
100
  const envCreds = getCredentialsFromEnv();
66
101
  if (envCreds) return envCreds;
102
+ validateProfileName(profile);
67
103
  if (!existsSync(CREDENTIALS_PATH)) {
68
104
  throw new AuthError(
69
105
  "Not logged in. Run `nworks login` or set environment variables."
@@ -75,9 +111,16 @@ async function loadCredentials(profile = "default") {
75
111
  if (!creds) {
76
112
  throw new AuthError(`Profile "${profile}" not found in credentials.`);
77
113
  }
78
- return creds;
114
+ const parsed = credentialsSchema.safeParse(creds);
115
+ if (!parsed.success) {
116
+ throw new AuthError(
117
+ `Credentials for profile "${profile}" are malformed. Run \`nworks login\` to reconfigure.`
118
+ );
119
+ }
120
+ return parsed.data;
79
121
  }
80
122
  async function saveCredentials(creds, profile = "default") {
123
+ validateProfileName(profile);
81
124
  await ensureConfigDir();
82
125
  let profiles = {};
83
126
  if (existsSync(CREDENTIALS_PATH)) {
@@ -85,13 +128,10 @@ async function saveCredentials(creds, profile = "default") {
85
128
  profiles = JSON.parse(raw);
86
129
  }
87
130
  profiles[profile] = creds;
88
- await writeFile(
89
- CREDENTIALS_PATH,
90
- JSON.stringify(profiles, null, 2),
91
- { encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
92
- );
131
+ await writeSecureFile(CREDENTIALS_PATH, JSON.stringify(profiles, null, 2));
93
132
  }
94
133
  async function loadToken(profile = "default") {
134
+ validateProfileName(profile);
95
135
  if (!existsSync(TOKEN_PATH)) return null;
96
136
  const raw = await readFile(TOKEN_PATH, "utf-8");
97
137
  const tokens = JSON.parse(raw);
@@ -103,6 +143,7 @@ async function loadToken(profile = "default") {
103
143
  };
104
144
  }
105
145
  async function saveToken(token, profile = "default") {
146
+ validateProfileName(profile);
106
147
  await ensureConfigDir();
107
148
  let tokens = {};
108
149
  if (existsSync(TOKEN_PATH)) {
@@ -110,13 +151,10 @@ async function saveToken(token, profile = "default") {
110
151
  tokens = JSON.parse(raw);
111
152
  }
112
153
  tokens[profile] = token;
113
- await writeFile(
114
- TOKEN_PATH,
115
- JSON.stringify(tokens, null, 2),
116
- { encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
117
- );
154
+ await writeSecureFile(TOKEN_PATH, JSON.stringify(tokens, null, 2));
118
155
  }
119
156
  async function loadUserToken(profile = "default") {
157
+ validateProfileName(profile);
120
158
  if (!existsSync(USER_TOKEN_PATH)) return null;
121
159
  const raw = await readFile(USER_TOKEN_PATH, "utf-8");
122
160
  const tokens = JSON.parse(raw);
@@ -130,6 +168,7 @@ async function loadUserToken(profile = "default") {
130
168
  };
131
169
  }
132
170
  async function saveUserToken(token, profile = "default") {
171
+ validateProfileName(profile);
133
172
  await ensureConfigDir();
134
173
  let tokens = {};
135
174
  if (existsSync(USER_TOKEN_PATH)) {
@@ -137,42 +176,27 @@ async function saveUserToken(token, profile = "default") {
137
176
  tokens = JSON.parse(raw);
138
177
  }
139
178
  tokens[profile] = token;
140
- await writeFile(
141
- USER_TOKEN_PATH,
142
- JSON.stringify(tokens, null, 2),
143
- { encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
144
- );
179
+ await writeSecureFile(USER_TOKEN_PATH, JSON.stringify(tokens, null, 2));
145
180
  }
146
181
  async function clearCredentials(profile = "default") {
182
+ validateProfileName(profile);
147
183
  if (existsSync(CREDENTIALS_PATH)) {
148
184
  const raw = await readFile(CREDENTIALS_PATH, "utf-8");
149
185
  const profiles = JSON.parse(raw);
150
186
  delete profiles[profile];
151
- await writeFile(
152
- CREDENTIALS_PATH,
153
- JSON.stringify(profiles, null, 2),
154
- { encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
155
- );
187
+ await writeSecureFile(CREDENTIALS_PATH, JSON.stringify(profiles, null, 2));
156
188
  }
157
189
  if (existsSync(TOKEN_PATH)) {
158
190
  const raw = await readFile(TOKEN_PATH, "utf-8");
159
191
  const tokens = JSON.parse(raw);
160
192
  delete tokens[profile];
161
- await writeFile(
162
- TOKEN_PATH,
163
- JSON.stringify(tokens, null, 2),
164
- { encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
165
- );
193
+ await writeSecureFile(TOKEN_PATH, JSON.stringify(tokens, null, 2));
166
194
  }
167
195
  if (existsSync(USER_TOKEN_PATH)) {
168
196
  const raw = await readFile(USER_TOKEN_PATH, "utf-8");
169
197
  const tokens = JSON.parse(raw);
170
198
  delete tokens[profile];
171
- await writeFile(
172
- USER_TOKEN_PATH,
173
- JSON.stringify(tokens, null, 2),
174
- { encoding: "utf-8", ...IS_UNIX && { mode: 384 } }
175
- );
199
+ await writeSecureFile(USER_TOKEN_PATH, JSON.stringify(tokens, null, 2));
176
200
  }
177
201
  }
178
202
 
@@ -246,7 +270,6 @@ async function refreshToken(profile = "default") {
246
270
 
247
271
  // src/auth/oauth-user.ts
248
272
  import { createServer } from "http";
249
- import { randomBytes } from "crypto";
250
273
  import { URL as URL2 } from "url";
251
274
  var AUTH_URL2 = "https://auth.worksmobile.com/oauth2/v2.0/authorize";
252
275
  var TOKEN_URL = "https://auth.worksmobile.com/oauth2/v2.0/token";
@@ -262,13 +285,13 @@ function buildAuthorizeUrl(clientId, scope, state) {
262
285
  });
263
286
  return `${AUTH_URL2}?${params.toString()}`;
264
287
  }
265
- async function startUserOAuthFlow(_scope, profile = "default", expectedState) {
288
+ async function startUserOAuthFlow(_scope, profile, expectedState) {
266
289
  const creds = await loadCredentials(profile);
267
- const code = await waitForAuthCode(expectedState ?? randomBytes(16).toString("hex"));
290
+ const code = await waitForAuthCode(expectedState);
268
291
  return exchangeCodeForToken(code, creds.clientId, creds.clientSecret);
269
292
  }
270
293
  function waitForAuthCode(expectedState) {
271
- return new Promise((resolve2, reject) => {
294
+ return new Promise((resolve3, reject) => {
272
295
  const timeout = setTimeout(() => {
273
296
  server.close();
274
297
  reject(new AuthError("OAuth login timed out (120s). Try again."));
@@ -285,10 +308,7 @@ function waitForAuthCode(expectedState) {
285
308
  const state = url.searchParams.get("state");
286
309
  if (state !== expectedState) {
287
310
  res.writeHead(403, { "Content-Type": "text/html; charset=utf-8" });
288
- res.end("<h2>\uBCF4\uC548 \uC624\uB958</h2><p>state \uBD88\uC77C\uCE58. \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.</p>");
289
- clearTimeout(timeout);
290
- server.close();
291
- reject(new AuthError("OAuth state mismatch \u2014 possible CSRF attack."));
311
+ 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>");
292
312
  return;
293
313
  }
294
314
  if (error) {
@@ -311,7 +331,7 @@ function waitForAuthCode(expectedState) {
311
331
  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>");
312
332
  clearTimeout(timeout);
313
333
  server.close();
314
- resolve2(code);
334
+ resolve3(code);
315
335
  });
316
336
  server.listen(REDIRECT_PORT, "127.0.0.1", () => {
317
337
  });
@@ -321,6 +341,17 @@ function waitForAuthCode(expectedState) {
321
341
  });
322
342
  });
323
343
  }
344
+ async function safeErrorSummary(res) {
345
+ const text = await res.text();
346
+ try {
347
+ const body = JSON.parse(text);
348
+ if (body.error || body.error_description) {
349
+ return [body.error, body.error_description].filter(Boolean).join(": ");
350
+ }
351
+ } catch {
352
+ }
353
+ return text.length > 200 ? text.slice(0, 200) + "..." : text;
354
+ }
324
355
  async function exchangeCodeForToken(code, clientId, clientSecret) {
325
356
  const body = new URLSearchParams({
326
357
  grant_type: "authorization_code",
@@ -335,9 +366,7 @@ async function exchangeCodeForToken(code, clientId, clientSecret) {
335
366
  body: body.toString()
336
367
  });
337
368
  if (!res.ok) {
338
- const text = await res.text();
339
- const truncated = text.length > 200 ? text.substring(0, 200) + "..." : text;
340
- throw new AuthError(`Token exchange failed (${res.status}): ${truncated}`);
369
+ throw new AuthError(`Token exchange failed (${res.status}): ${await safeErrorSummary(res)}`);
341
370
  }
342
371
  const data = await res.json();
343
372
  return {
@@ -361,9 +390,7 @@ async function refreshUserToken(refreshToken2, profile = "default") {
361
390
  body: body.toString()
362
391
  });
363
392
  if (!res.ok) {
364
- const text = await res.text();
365
- const truncated = text.length > 200 ? text.substring(0, 200) + "..." : text;
366
- throw new AuthError(`Token refresh failed (${res.status}): ${truncated}`);
393
+ throw new AuthError(`Token refresh failed (${res.status}): ${await safeErrorSummary(res)}`);
367
394
  }
368
395
  const data = await res.json();
369
396
  return {
@@ -396,6 +423,69 @@ async function revokeToken(token, clientId, clientSecret) {
396
423
  }
397
424
  }
398
425
 
426
+ // src/auth/scopes.ts
427
+ var READ_SCOPES = [
428
+ "calendar.read",
429
+ "file.read",
430
+ "mail.read",
431
+ "task.read",
432
+ "user.read",
433
+ "board.read",
434
+ "contact.read"
435
+ ];
436
+ var ALL_SCOPES = [
437
+ "calendar",
438
+ "calendar.read",
439
+ "file",
440
+ "file.read",
441
+ "mail",
442
+ "mail.read",
443
+ "task",
444
+ "task.read",
445
+ "user.read",
446
+ "board",
447
+ "board.read",
448
+ "contact",
449
+ "contact.read"
450
+ ];
451
+ var SCOPE_DEPENDENCIES = {
452
+ calendar: ["calendar.read"],
453
+ file: ["file.read"],
454
+ mail: ["mail.read"],
455
+ task: ["task.read", "user.read"],
456
+ "task.read": ["user.read"],
457
+ board: ["board.read"],
458
+ contact: ["contact.read"]
459
+ };
460
+ var SCOPE_PRESETS = {
461
+ readonly: READ_SCOPES,
462
+ // default를 all로 두는 이유: 현재 제품의 "한 번 로그인으로 전체 기능" 약속을 보존한다.
463
+ // 읽기 전용을 기본값으로 하면 신규 사용자가 쓰기 기능(일정 생성 등)을 쓸 때 재로그인해야 한다.
464
+ // 안전을 최우선하려면 readonly로 바꾸면 되지만, 삭제류 도구는 이미 destructiveHint
465
+ // annotation으로 클라이언트 승인 게이트가 걸린다.
466
+ default: ALL_SCOPES,
467
+ all: ALL_SCOPES
468
+ };
469
+ function isPreset(value) {
470
+ return value === "readonly" || value === "default" || value === "all";
471
+ }
472
+ function expandScopeDependencies(scopes) {
473
+ const expanded = new Set(scopes);
474
+ for (const scope of scopes) {
475
+ const deps = SCOPE_DEPENDENCIES[scope];
476
+ if (deps) deps.forEach((d) => expanded.add(d));
477
+ }
478
+ return [...expanded];
479
+ }
480
+ function resolveScopes(input) {
481
+ if (!input) return SCOPE_PRESETS.default;
482
+ if (isPreset(input)) return SCOPE_PRESETS[input];
483
+ return expandScopeDependencies(input.split(" ").filter(Boolean));
484
+ }
485
+ function mergeScopes(existing, requested) {
486
+ return [.../* @__PURE__ */ new Set([...existing, ...requested])].join(" ");
487
+ }
488
+
399
489
  // src/output/format.ts
400
490
  function output(data, opts = {}) {
401
491
  const useJson = opts.json || !process.stdout.isTTY;
@@ -469,7 +559,7 @@ function errorOutput(error, opts = {}) {
469
559
  }
470
560
 
471
561
  // src/commands/login.ts
472
- import { randomBytes as randomBytes2 } from "crypto";
562
+ import { randomBytes } from "crypto";
473
563
  async function prompt(question) {
474
564
  const rl = createInterface({
475
565
  input: process.stdin,
@@ -482,11 +572,24 @@ async function prompt(question) {
482
572
  rl.close();
483
573
  }
484
574
  }
485
- var loginCommand = new Command("login").description("Authenticate with NAVER WORKS").option("--user", "User OAuth login (opens browser)").option("--scope <scope>", "OAuth scope for user login", "calendar.read").option("--client-id <id>", "Client ID").option("--client-secret <secret>", "Client Secret").option("--service-account <account>", "Service Account ID").option("--private-key <path>", "Path to private key file (.key)").option("--bot-id <id>", "Bot ID").option("--domain-id <id>", "Domain ID (optional)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
575
+ async function promptSecret(question) {
576
+ process.stdout.write(question);
577
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
578
+ rl._writeToOutput = () => {
579
+ };
580
+ try {
581
+ const answer = await rl.question("");
582
+ process.stdout.write("\n");
583
+ return answer.trim();
584
+ } finally {
585
+ rl.close();
586
+ }
587
+ }
588
+ var loginCommand = new Command("login").description("Authenticate with NAVER WORKS").option("--user", "User OAuth login (opens browser)").option("--preset <preset>", "Scope preset: readonly | default | all (default: all)").option("--scope <scope>", "Advanced: raw space-separated scopes (overrides --preset)").option("--client-id <id>", "Client ID").option("--client-secret <secret>", "Client Secret").option("--service-account <account>", "Service Account ID").option("--private-key <path>", "Path to private key file (.key)").option("--bot-id <id>", "Bot ID").option("--domain-id <id>", "Domain ID (optional)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
486
589
  try {
487
590
  const profile = opts.profile;
488
591
  if (opts.user) {
489
- await handleUserLogin(opts.scope, profile, opts);
592
+ await handleUserLogin(profile, opts);
490
593
  } else {
491
594
  await handleServiceAccountLogin(opts);
492
595
  }
@@ -496,7 +599,7 @@ var loginCommand = new Command("login").description("Authenticate with NAVER WOR
496
599
  process.exitCode = 1;
497
600
  }
498
601
  });
499
- async function handleUserLogin(scope, profile, opts) {
602
+ async function handleUserLogin(profile, opts) {
500
603
  let clientId = opts.clientId;
501
604
  let clientSecret = opts.clientSecret;
502
605
  try {
@@ -507,7 +610,7 @@ async function handleUserLogin(scope, profile, opts) {
507
610
  }
508
611
  if (process.stdin.isTTY) {
509
612
  if (!clientId) clientId = await prompt("Client ID: ");
510
- if (!clientSecret) clientSecret = await prompt("Client Secret: ");
613
+ if (!clientSecret) clientSecret = await promptSecret("Client Secret: ");
511
614
  }
512
615
  if (!clientId || !clientSecret) {
513
616
  throw new Error(
@@ -520,22 +623,33 @@ async function handleUserLogin(scope, profile, opts) {
520
623
  } catch {
521
624
  await saveCredentials({ clientId, clientSecret }, profile);
522
625
  }
523
- const state = randomBytes2(16).toString("hex");
524
- const authorizeUrl = buildAuthorizeUrl(clientId, scope, state);
626
+ const requestedScopes = resolveScopes(opts.preset ?? opts.scope);
627
+ let existingScopes = [];
628
+ try {
629
+ const existingToken = await loadUserToken(profile);
630
+ existingScopes = existingToken?.scope?.split(" ").filter(Boolean) ?? [];
631
+ } catch {
632
+ }
633
+ const mergedScope = mergeScopes(existingScopes, requestedScopes);
634
+ const state = randomBytes(16).toString("hex");
635
+ const authorizeUrl = buildAuthorizeUrl(clientId, mergedScope, state);
525
636
  console.error(`
526
637
  Opening browser for NAVER WORKS login...`);
527
638
  console.error(`If the browser does not open, visit this URL:
528
639
  `);
529
640
  console.error(` ${authorizeUrl}
530
641
  `);
531
- const { exec } = await import("child_process");
532
- const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
533
- if (process.platform === "win32") {
534
- exec(`start "" "${authorizeUrl}"`);
642
+ const { execFile } = await import("child_process");
643
+ if (process.platform === "darwin") {
644
+ execFile("open", [authorizeUrl]);
645
+ } else if (process.platform === "win32") {
646
+ execFile("cmd", ["/c", "start", '""', `"${authorizeUrl}"`], {
647
+ windowsVerbatimArguments: true
648
+ });
535
649
  } else {
536
- exec(`${openCmd} "${authorizeUrl}"`);
650
+ execFile("xdg-open", [authorizeUrl]);
537
651
  }
538
- const tokenData = await startUserOAuthFlow(scope, profile, state);
652
+ const tokenData = await startUserOAuthFlow(mergedScope, profile, state);
539
653
  await saveUserToken(tokenData, profile);
540
654
  output(
541
655
  {
@@ -562,7 +676,7 @@ async function handleServiceAccountLogin(opts) {
562
676
  if (!botId) botId = process.env["NWORKS_BOT_ID"];
563
677
  if (process.stdin.isTTY) {
564
678
  if (!clientId) clientId = await prompt("Client ID: ");
565
- if (!clientSecret) clientSecret = await prompt("Client Secret: ");
679
+ if (!clientSecret) clientSecret = await promptSecret("Client Secret: ");
566
680
  if (!serviceAccount)
567
681
  serviceAccount = await prompt("Service Account: ");
568
682
  if (!privateKeyPath)
@@ -624,10 +738,10 @@ var REQUIRED_SCOPES = {
624
738
  "board.create": "board"
625
739
  };
626
740
  var ERROR_HINTS_CLI = {
627
- FORBIDDEN: "\uAD8C\uD55C\uC774 \uBD80\uC871\uD569\uB2C8\uB2E4. Developer Console\uC5D0\uC11C OAuth Scope\uB97C \uD655\uC778\uD558\uC138\uC694.",
628
- 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.",
629
- 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.",
630
- UNAUTHORIZED: "\uC778\uC99D\uC774 \uB9CC\uB8CC\uB410\uC2B5\uB2C8\uB2E4. `nworks login`\uC73C\uB85C \uB2E4\uC2DC \uB85C\uADF8\uC778\uD558\uC138\uC694."
741
+ FORBIDDEN: "Insufficient permission. Check the OAuth scopes in the Developer Console.",
742
+ ACCESS_DENIED: "Access denied. Make sure the Bot is added in the Admin console.",
743
+ SERVICE_ACCOUNT_NOT_ALLOWED: "This API is not available with a Service Account. Log in with User OAuth via `nworks login --user`.",
744
+ UNAUTHORIZED: "Authentication expired. Log in again with `nworks login`."
631
745
  };
632
746
  var ERROR_HINTS_MCP = {
633
747
  FORBIDDEN: "\uAD8C\uD55C\uC774 \uBD80\uC871\uD569\uB2C8\uB2E4. Developer Console\uC5D0\uC11C OAuth Scope\uB97C \uD655\uC778\uD558\uC138\uC694.",
@@ -675,7 +789,7 @@ function buildScopeHint(area, mode) {
675
789
  const scopes = scope.split(" ").join(", ");
676
790
  if (mode === "cli") {
677
791
  return `
678
- \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.`;
792
+ \u2192 This command requires the ${scopes} scope(s). Run \`nworks login --user --scope "${scope}"\`.`;
679
793
  }
680
794
  return `
681
795
  \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).`;
@@ -768,7 +882,7 @@ import { Command as Command4 } from "commander";
768
882
  var BASE_URL = "https://www.worksapis.com/v1.0";
769
883
  var MAX_RETRIES = 3;
770
884
  function sleep(ms) {
771
- return new Promise((resolve2) => setTimeout(resolve2, ms));
885
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
772
886
  }
773
887
  async function request(opts, _retryCount = 0) {
774
888
  const { method, path, body, profile = "default" } = opts;
@@ -818,8 +932,8 @@ async function request(opts, _retryCount = 0) {
818
932
  }
819
933
 
820
934
  // src/utils/sanitize.ts
821
- import { basename, resolve, sep } from "path";
822
- import { randomBytes as randomBytes3 } from "crypto";
935
+ import { resolve, sep } from "path";
936
+ import { randomBytes as randomBytes2 } from "crypto";
823
937
  function sanitizePathSegment(value) {
824
938
  if (!value || typeof value !== "string") {
825
939
  throw new Error("Path segment must be a non-empty string");
@@ -834,8 +948,9 @@ function sanitizeFileName(name) {
834
948
  if (!name || typeof name !== "string") {
835
949
  throw new Error("File name must be a non-empty string");
836
950
  }
837
- const base = basename(name);
838
- return base.replace(/[\r\n"\\]/g, "_");
951
+ const segments = name.split(/[/\\]/).filter((s) => s.length > 0);
952
+ const base = segments[segments.length - 1] ?? "";
953
+ return base.replace(/[\r\n"]/g, "_");
839
954
  }
840
955
  function validateLocalPath(filePath, allowedBase) {
841
956
  const resolved = resolve(filePath);
@@ -866,7 +981,7 @@ function validateRedirectUrl(location, allowedHosts) {
866
981
  return location;
867
982
  }
868
983
  function generateSecureState() {
869
- return randomBytes3(32).toString("hex");
984
+ return randomBytes2(32).toString("hex");
870
985
  }
871
986
 
872
987
  // src/api/message.ts
@@ -988,20 +1103,25 @@ var messageCommand = new Command4("message").description("Bot message operations
988
1103
  import { Command as Command5 } from "commander";
989
1104
 
990
1105
  // src/api/directory.ts
991
- async function listUsers(profile = "default") {
1106
+ async function listUsers(count = 100, cursor, profile = "default") {
1107
+ const params = new URLSearchParams();
1108
+ params.set("count", String(count));
1109
+ if (cursor) params.set("cursor", cursor);
992
1110
  const result = await request({
993
1111
  method: "GET",
994
- path: "/users",
1112
+ path: `/users?${params.toString()}`,
995
1113
  profile
996
1114
  });
997
1115
  return { users: result.users ?? [], responseMetaData: result.responseMetaData };
998
1116
  }
999
1117
 
1000
1118
  // src/commands/directory.ts
1001
- var membersCommand2 = new Command5("members").description("List organization members (requires directory.read scope)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
1119
+ var membersCommand2 = new Command5("members").description("List organization members (requires directory.read scope)").option("--count <n>", "Items per page (default: 100)").option("--cursor <cursor>", "Pagination cursor from a previous response").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
1002
1120
  try {
1003
- const result = await listUsers(opts.profile);
1121
+ const count = opts.count ? Number(opts.count) : 100;
1122
+ const result = await listUsers(count, opts.cursor, opts.profile);
1004
1123
  const formatted = {
1124
+ nextCursor: result.responseMetaData?.nextCursor ?? null,
1005
1125
  users: result.users.map((u) => ({
1006
1126
  userId: u.userId,
1007
1127
  userName: [u.userName?.lastName, u.userName?.firstName].filter(Boolean).join(" ") || "",
@@ -1024,6 +1144,7 @@ import { Command as Command6 } from "commander";
1024
1144
  import { randomUUID } from "crypto";
1025
1145
 
1026
1146
  // src/auth/token-user.ts
1147
+ var inFlightRefresh = /* @__PURE__ */ new Map();
1027
1148
  async function getValidUserToken(profile = "default") {
1028
1149
  const cached = await loadUserToken(profile);
1029
1150
  if (!cached) {
@@ -1034,22 +1155,106 @@ async function getValidUserToken(profile = "default") {
1034
1155
  if (cached.expiresAt > Date.now() / 1e3 + 300) {
1035
1156
  return cached.accessToken;
1036
1157
  }
1158
+ return refreshValidUserToken(profile);
1159
+ }
1160
+ function refreshValidUserToken(profile = "default") {
1161
+ const existing = inFlightRefresh.get(profile);
1162
+ if (existing) return existing;
1163
+ const run = doRefresh(profile);
1164
+ inFlightRefresh.set(profile, run);
1165
+ void run.finally(() => inFlightRefresh.delete(profile)).catch(() => {
1166
+ });
1167
+ return run;
1168
+ }
1169
+ async function doRefresh(profile) {
1170
+ const cached = await loadUserToken(profile);
1171
+ if (!cached) {
1172
+ throw new AuthError(
1173
+ "User OAuth token not found. Run `nworks login --user` first."
1174
+ );
1175
+ }
1037
1176
  const refreshed = await refreshUserToken(cached.refreshToken, profile);
1038
1177
  await saveUserToken(refreshed, profile);
1039
1178
  return refreshed.accessToken;
1040
1179
  }
1041
1180
 
1042
- // src/api/calendar.ts
1043
- var BASE_URL2 = "https://www.worksapis.com/v1.0";
1044
- async function authedFetch(url, init, profile) {
1045
- const token = await getValidUserToken(profile);
1046
- const headers = new Headers(init.headers);
1047
- headers.set("Authorization", `Bearer ${token}`);
1048
- return fetch(url, { ...init, headers });
1181
+ // src/api/user-client.ts
1182
+ var MAX_RETRIES2 = 3;
1183
+ var MAX_RETRY_AFTER_SECONDS = 60;
1184
+ var MAX_CONCURRENT_PER_HOST = 5;
1185
+ function sleep2(ms) {
1186
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
1187
+ }
1188
+ var HostSemaphore = class {
1189
+ constructor(max) {
1190
+ this.max = max;
1191
+ }
1192
+ active = 0;
1193
+ waiters = [];
1194
+ async acquire() {
1195
+ if (this.active < this.max) {
1196
+ this.active++;
1197
+ return;
1198
+ }
1199
+ await new Promise((resolve3) => this.waiters.push(resolve3));
1200
+ }
1201
+ release() {
1202
+ const next = this.waiters.shift();
1203
+ if (next) {
1204
+ next();
1205
+ } else {
1206
+ this.active--;
1207
+ }
1208
+ }
1209
+ };
1210
+ var semaphores = /* @__PURE__ */ new Map();
1211
+ function semaphoreFor(host) {
1212
+ let semaphore = semaphores.get(host);
1213
+ if (!semaphore) {
1214
+ semaphore = new HostSemaphore(MAX_CONCURRENT_PER_HOST);
1215
+ semaphores.set(host, semaphore);
1216
+ }
1217
+ return semaphore;
1049
1218
  }
1050
- async function handleError(res) {
1219
+ async function userFetch(url, init, profile) {
1220
+ let refreshedOn401 = false;
1221
+ for (let attempt = 0; ; attempt++) {
1222
+ const res = await fetchOnce(url, init, profile);
1223
+ if (res.status === 401 && !refreshedOn401) {
1224
+ refreshedOn401 = true;
1225
+ await refreshValidUserToken(profile);
1226
+ continue;
1227
+ }
1228
+ if (res.status === 429 && attempt < MAX_RETRIES2) {
1229
+ await sleep2(retryAfterMs(res));
1230
+ continue;
1231
+ }
1232
+ return res;
1233
+ }
1234
+ }
1235
+ async function fetchOnce(url, init, profile) {
1236
+ const host = new URL(url).host;
1237
+ const semaphore = semaphoreFor(host);
1238
+ await semaphore.acquire();
1239
+ try {
1240
+ const token = await getValidUserToken(profile);
1241
+ const headers = new Headers(init.headers);
1242
+ headers.set("Authorization", `Bearer ${token}`);
1243
+ return await fetch(url, { ...init, headers });
1244
+ } finally {
1245
+ semaphore.release();
1246
+ }
1247
+ }
1248
+ function retryAfterMs(res) {
1249
+ const raw = parseInt(res.headers.get("Retry-After") ?? "5", 10);
1250
+ const seconds = Math.min(Number.isNaN(raw) ? 5 : raw, MAX_RETRY_AFTER_SECONDS);
1251
+ return seconds * 1e3;
1252
+ }
1253
+ async function handleUserApiError(res) {
1051
1254
  if (res.status === 401) {
1052
- throw new AuthError("User token expired. Run `nworks login --user --scope calendar` again.");
1255
+ throw new AuthError(
1256
+ "User token expired or revoked. Run `nworks login --user` again."
1257
+ );
1053
1258
  }
1054
1259
  let code = "UNKNOWN";
1055
1260
  let description = `HTTP ${res.status}`;
@@ -1061,6 +1266,9 @@ async function handleError(res) {
1061
1266
  }
1062
1267
  throw new ApiError(code, description, res.status);
1063
1268
  }
1269
+
1270
+ // src/api/calendar.ts
1271
+ var BASE_URL2 = "https://www.worksapis.com/v1.0";
1064
1272
  function generateEventId() {
1065
1273
  return `event-${randomUUID()}`;
1066
1274
  }
@@ -1078,8 +1286,8 @@ async function listEvents(fromDateTime, untilDateTime, userId = "me", profile =
1078
1286
  if (process.env["NWORKS_VERBOSE"] === "1") {
1079
1287
  console.error(`[nworks] GET ${url}`);
1080
1288
  }
1081
- const res = await authedFetch(url, { method: "GET" }, profile);
1082
- if (!res.ok) return handleError(res);
1289
+ const res = await userFetch(url, { method: "GET" }, profile);
1290
+ if (!res.ok) return handleUserApiError(res);
1083
1291
  const data = await res.json();
1084
1292
  return { events: data.events ?? [] };
1085
1293
  }
@@ -1116,7 +1324,7 @@ async function createEvent(opts) {
1116
1324
  console.error(`[nworks] POST ${url}`);
1117
1325
  console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
1118
1326
  }
1119
- const res = await authedFetch(
1327
+ const res = await userFetch(
1120
1328
  url,
1121
1329
  {
1122
1330
  method: "POST",
@@ -1128,7 +1336,7 @@ async function createEvent(opts) {
1128
1336
  if (res.status === 201) {
1129
1337
  return await res.json();
1130
1338
  }
1131
- if (!res.ok) return handleError(res);
1339
+ if (!res.ok) return handleUserApiError(res);
1132
1340
  return await res.json();
1133
1341
  }
1134
1342
  async function getEvent(eventId, userId = "me", profile = "default") {
@@ -1136,8 +1344,8 @@ async function getEvent(eventId, userId = "me", profile = "default") {
1136
1344
  if (process.env["NWORKS_VERBOSE"] === "1") {
1137
1345
  console.error(`[nworks] GET ${url}`);
1138
1346
  }
1139
- const res = await authedFetch(url, { method: "GET" }, profile);
1140
- if (!res.ok) return handleError(res);
1347
+ const res = await userFetch(url, { method: "GET" }, profile);
1348
+ if (!res.ok) return handleUserApiError(res);
1141
1349
  const data = await res.json();
1142
1350
  const event = data.eventComponents[0];
1143
1351
  if (!event) throw new ApiError("NOT_FOUND", "Event not found", 404);
@@ -1180,7 +1388,7 @@ async function updateEvent(opts) {
1180
1388
  console.error(`[nworks] PUT ${url}`);
1181
1389
  console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
1182
1390
  }
1183
- const res = await authedFetch(
1391
+ const res = await userFetch(
1184
1392
  url,
1185
1393
  {
1186
1394
  method: "PUT",
@@ -1189,7 +1397,7 @@ async function updateEvent(opts) {
1189
1397
  },
1190
1398
  profile
1191
1399
  );
1192
- if (!res.ok) return handleError(res);
1400
+ if (!res.ok) return handleUserApiError(res);
1193
1401
  }
1194
1402
  async function deleteEvent(eventId, userId = "me", sendNotification = false, profile = "default") {
1195
1403
  const params = new URLSearchParams();
@@ -1198,9 +1406,9 @@ async function deleteEvent(eventId, userId = "me", sendNotification = false, pro
1198
1406
  if (process.env["NWORKS_VERBOSE"] === "1") {
1199
1407
  console.error(`[nworks] DELETE ${url}`);
1200
1408
  }
1201
- const res = await authedFetch(url, { method: "DELETE" }, profile);
1409
+ const res = await userFetch(url, { method: "DELETE" }, profile);
1202
1410
  if (res.status === 204) return;
1203
- if (!res.ok) return handleError(res);
1411
+ if (!res.ok) return handleUserApiError(res);
1204
1412
  }
1205
1413
 
1206
1414
  // src/commands/calendar.ts
@@ -1321,34 +1529,15 @@ import { Command as Command7 } from "commander";
1321
1529
 
1322
1530
  // src/api/drive.ts
1323
1531
  import { readFile as readFile4, stat as stat2 } from "fs/promises";
1324
- import { basename as basename2 } from "path";
1532
+ import { basename } from "path";
1325
1533
  var BASE_URL3 = "https://www.worksapis.com/v1.0";
1326
1534
  var MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
1327
1535
  var ALLOWED_HOSTS = [
1328
1536
  "storage.worksmobile.com",
1537
+ "apis-storage.worksmobile.com",
1329
1538
  "www.worksapis.com",
1330
1539
  "worksapis.com"
1331
1540
  ];
1332
- async function authedFetch2(url, init, profile) {
1333
- const token = await getValidUserToken(profile);
1334
- const headers = new Headers(init.headers);
1335
- headers.set("Authorization", `Bearer ${token}`);
1336
- return fetch(url, { ...init, headers });
1337
- }
1338
- async function handleError2(res) {
1339
- if (res.status === 401) {
1340
- throw new AuthError("User token expired. Run `nworks login --user --scope file` again.");
1341
- }
1342
- let code = "UNKNOWN";
1343
- let description = `HTTP ${res.status}`;
1344
- try {
1345
- const body = await res.json();
1346
- code = body.code ?? code;
1347
- description = body.description ?? description;
1348
- } catch {
1349
- }
1350
- throw new ApiError(code, description, res.status);
1351
- }
1352
1541
  async function listFiles(userId = "me", folderId, count = 20, cursor, profile = "default") {
1353
1542
  const base = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files`;
1354
1543
  const path = folderId ? `${base}/${sanitizePathSegment(folderId)}/children` : base;
@@ -1359,13 +1548,13 @@ async function listFiles(userId = "me", folderId, count = 20, cursor, profile =
1359
1548
  if (process.env["NWORKS_VERBOSE"] === "1") {
1360
1549
  console.error(`[nworks] GET ${url}`);
1361
1550
  }
1362
- const res = await authedFetch2(url, { method: "GET" }, profile);
1363
- if (!res.ok) return handleError2(res);
1551
+ const res = await userFetch(url, { method: "GET" }, profile);
1552
+ if (!res.ok) return handleUserApiError(res);
1364
1553
  const data = await res.json();
1365
1554
  return { files: data.files ?? [], responseMetaData: data.responseMetaData };
1366
1555
  }
1367
1556
  async function uploadFile(localPath, userId = "me", folderId, overwrite = false, profile = "default") {
1368
- const fileName = basename2(localPath);
1557
+ const fileName = basename(localPath);
1369
1558
  const safeName = sanitizeFileName(fileName);
1370
1559
  const fileStat = await stat2(localPath);
1371
1560
  const fileSize = fileStat.size;
@@ -1377,7 +1566,7 @@ async function uploadFile(localPath, userId = "me", folderId, overwrite = false,
1377
1566
  if (process.env["NWORKS_VERBOSE"] === "1") {
1378
1567
  console.error(`[nworks] POST ${createUrl} (create upload URL)`);
1379
1568
  }
1380
- const createRes = await authedFetch2(
1569
+ const createRes = await userFetch(
1381
1570
  createUrl,
1382
1571
  {
1383
1572
  method: "POST",
@@ -1386,7 +1575,7 @@ async function uploadFile(localPath, userId = "me", folderId, overwrite = false,
1386
1575
  },
1387
1576
  profile
1388
1577
  );
1389
- if (!createRes.ok) return handleError2(createRes);
1578
+ if (!createRes.ok) return handleUserApiError(createRes);
1390
1579
  const { uploadUrl } = await createRes.json();
1391
1580
  validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
1392
1581
  const fileBuffer = await readFile4(localPath);
@@ -1405,7 +1594,7 @@ Content-Type: application/octet-stream\r
1405
1594
  if (process.env["NWORKS_VERBOSE"] === "1") {
1406
1595
  console.error(`[nworks] POST ${uploadUrl} (upload content, ${fileSize} bytes)`);
1407
1596
  }
1408
- const uploadRes = await authedFetch2(
1597
+ const uploadRes = await userFetch(
1409
1598
  uploadUrl,
1410
1599
  {
1411
1600
  method: "POST",
@@ -1414,7 +1603,7 @@ Content-Type: application/octet-stream\r
1414
1603
  },
1415
1604
  profile
1416
1605
  );
1417
- if (!uploadRes.ok) return handleError2(uploadRes);
1606
+ if (!uploadRes.ok) return handleUserApiError(uploadRes);
1418
1607
  return await uploadRes.json();
1419
1608
  }
1420
1609
  async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overwrite = false, profile = "default") {
@@ -1427,7 +1616,7 @@ async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overw
1427
1616
  if (process.env["NWORKS_VERBOSE"] === "1") {
1428
1617
  console.error(`[nworks] POST ${createUrl} (create upload URL for buffer)`);
1429
1618
  }
1430
- const createRes = await authedFetch2(
1619
+ const createRes = await userFetch(
1431
1620
  createUrl,
1432
1621
  {
1433
1622
  method: "POST",
@@ -1436,7 +1625,7 @@ async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overw
1436
1625
  },
1437
1626
  profile
1438
1627
  );
1439
- if (!createRes.ok) return handleError2(createRes);
1628
+ if (!createRes.ok) return handleUserApiError(createRes);
1440
1629
  const { uploadUrl } = await createRes.json();
1441
1630
  validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
1442
1631
  const boundary = `----nworks${Date.now()}`;
@@ -1454,7 +1643,7 @@ Content-Type: application/octet-stream\r
1454
1643
  if (process.env["NWORKS_VERBOSE"] === "1") {
1455
1644
  console.error(`[nworks] POST ${uploadUrl} (upload buffer, ${fileSize} bytes)`);
1456
1645
  }
1457
- const uploadRes = await authedFetch2(
1646
+ const uploadRes = await userFetch(
1458
1647
  uploadUrl,
1459
1648
  {
1460
1649
  method: "POST",
@@ -1463,33 +1652,29 @@ Content-Type: application/octet-stream\r
1463
1652
  },
1464
1653
  profile
1465
1654
  );
1466
- if (!uploadRes.ok) return handleError2(uploadRes);
1655
+ if (!uploadRes.ok) return handleUserApiError(uploadRes);
1467
1656
  return await uploadRes.json();
1468
1657
  }
1469
- async function downloadFile(fileId, userId = "me", profile = "default") {
1470
- const url = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files/${sanitizePathSegment(fileId)}/download`;
1658
+ async function downloadViaRedirect(url, profile) {
1471
1659
  if (process.env["NWORKS_VERBOSE"] === "1") {
1472
1660
  console.error(`[nworks] GET ${url} (get download URL)`);
1473
1661
  }
1474
- const redirectRes = await authedFetch2(
1662
+ const redirectRes = await userFetch(
1475
1663
  url,
1476
1664
  { method: "GET", redirect: "manual" },
1477
1665
  profile
1478
1666
  );
1479
- if (redirectRes.status === 401) {
1480
- throw new AuthError("User token expired. Run `nworks login --user --scope file` again.");
1481
- }
1482
1667
  const location = redirectRes.headers.get("location");
1483
1668
  if (!location) {
1484
- if (!redirectRes.ok) return handleError2(redirectRes);
1669
+ if (!redirectRes.ok) return handleUserApiError(redirectRes);
1485
1670
  throw new ApiError("NO_REDIRECT", "No download URL returned", redirectRes.status);
1486
1671
  }
1487
1672
  const safeLocation = validateRedirectUrl(location, ALLOWED_HOSTS);
1488
1673
  if (process.env["NWORKS_VERBOSE"] === "1") {
1489
1674
  console.error(`[nworks] GET ${safeLocation} (download content)`);
1490
1675
  }
1491
- const downloadRes = await fetch(safeLocation, { method: "GET" });
1492
- if (!downloadRes.ok) return handleError2(downloadRes);
1676
+ const downloadRes = await userFetch(safeLocation, { method: "GET" }, profile);
1677
+ if (!downloadRes.ok) return handleUserApiError(downloadRes);
1493
1678
  const arrayBuffer = await downloadRes.arrayBuffer();
1494
1679
  const buffer = Buffer.from(arrayBuffer);
1495
1680
  const disposition = downloadRes.headers.get("content-disposition");
@@ -1502,6 +1687,42 @@ async function downloadFile(fileId, userId = "me", profile = "default") {
1502
1687
  }
1503
1688
  return { buffer, fileName };
1504
1689
  }
1690
+ async function downloadFile(fileId, userId = "me", profile = "default") {
1691
+ const url = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files/${sanitizePathSegment(fileId)}/download`;
1692
+ return downloadViaRedirect(url, profile);
1693
+ }
1694
+ async function listSharedDrives(count = 20, cursor, profile = "default") {
1695
+ const params = new URLSearchParams();
1696
+ params.set("count", String(count));
1697
+ if (cursor) params.set("cursor", cursor);
1698
+ const url = `${BASE_URL3}/sharedrives?${params.toString()}`;
1699
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1700
+ console.error(`[nworks] GET ${url}`);
1701
+ }
1702
+ const res = await userFetch(url, { method: "GET" }, profile);
1703
+ if (!res.ok) return handleUserApiError(res);
1704
+ const data = await res.json();
1705
+ return { sharedDrives: data.sharedrives ?? [], responseMetaData: data.responseMetaData };
1706
+ }
1707
+ async function listSharedDriveFiles(sharedriveId, folderId, count = 20, cursor, profile = "default") {
1708
+ const base = `${BASE_URL3}/sharedrives/${sanitizePathSegment(sharedriveId)}/files`;
1709
+ const path = folderId ? `${base}/${sanitizePathSegment(folderId)}/children` : base;
1710
+ const params = new URLSearchParams();
1711
+ params.set("count", String(count));
1712
+ if (cursor) params.set("cursor", cursor);
1713
+ const url = `${path}?${params.toString()}`;
1714
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1715
+ console.error(`[nworks] GET ${url}`);
1716
+ }
1717
+ const res = await userFetch(url, { method: "GET" }, profile);
1718
+ if (!res.ok) return handleUserApiError(res);
1719
+ const data = await res.json();
1720
+ return { files: data.files ?? [], responseMetaData: data.responseMetaData };
1721
+ }
1722
+ async function downloadSharedDriveFile(sharedriveId, fileId, profile = "default") {
1723
+ const url = `${BASE_URL3}/sharedrives/${sanitizePathSegment(sharedriveId)}/files/${sanitizePathSegment(fileId)}/download`;
1724
+ return downloadViaRedirect(url, profile);
1725
+ }
1505
1726
 
1506
1727
  // src/commands/drive.ts
1507
1728
  var listCommand2 = new Command7("list").description("List files in Drive (requires User OAuth with file or file.read scope)").option("--user <userId>", "Target user ID (default: me)").option("--folder <folderId>", "Folder ID to list (default: root)").option("--count <n>", "Items per page (default: 20)", "20").option("--cursor <cursor>", "Pagination cursor").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
@@ -1595,33 +1816,86 @@ var downloadCommand = new Command7("download").description("Download a file from
1595
1816
  cliError(err, opts, "drive");
1596
1817
  }
1597
1818
  });
1598
- var driveCommand = new Command7("drive").description("Drive operations (requires User OAuth with file scope)").addCommand(listCommand2).addCommand(uploadCommand).addCommand(downloadCommand);
1819
+ var sharedriveListCommand = new Command7("sharedrive-list").description("List shared drives (requires User OAuth with file or file.read scope)").option("--count <n>", "Items per page (default: 20)", "20").option("--cursor <cursor>", "Pagination cursor").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
1820
+ try {
1821
+ const result = await listSharedDrives(
1822
+ parseInt(opts.count, 10),
1823
+ opts.cursor,
1824
+ opts.profile
1825
+ );
1826
+ output(
1827
+ {
1828
+ sharedDrives: result.sharedDrives,
1829
+ count: result.sharedDrives.length,
1830
+ nextCursor: result.responseMetaData?.nextCursor ?? null
1831
+ },
1832
+ opts
1833
+ );
1834
+ } catch (err) {
1835
+ cliError(err, opts, "drive");
1836
+ }
1837
+ });
1838
+ var sharedriveFilesCommand = new Command7("sharedrive-files").description("List files in a shared drive (requires User OAuth with file or file.read scope)").requiredOption("--sharedrive <sharedriveId>", "Shared drive ID (from sharedrive-list)").option("--folder <fileId>", "Folder ID to list (default: root)").option("--count <n>", "Items per page (default: 20)", "20").option("--cursor <cursor>", "Pagination cursor").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
1839
+ try {
1840
+ const result = await listSharedDriveFiles(
1841
+ opts.sharedrive,
1842
+ opts.folder,
1843
+ parseInt(opts.count, 10),
1844
+ opts.cursor,
1845
+ opts.profile
1846
+ );
1847
+ const files = result.files.map((f) => ({
1848
+ name: f.fileName,
1849
+ type: f.fileType,
1850
+ size: f.fileSize,
1851
+ modified: f.modifiedTime,
1852
+ fileId: f.fileId
1853
+ }));
1854
+ output(
1855
+ {
1856
+ files,
1857
+ count: files.length,
1858
+ nextCursor: result.responseMetaData?.nextCursor ?? null
1859
+ },
1860
+ opts
1861
+ );
1862
+ } catch (err) {
1863
+ cliError(err, opts, "drive");
1864
+ }
1865
+ });
1866
+ var sharedriveDownloadCommand = new Command7("sharedrive-download").description("Download a file from a shared drive (requires User OAuth with file or file.read scope)").requiredOption("--sharedrive <sharedriveId>", "Shared drive ID (from sharedrive-list)").requiredOption("--file-id <fileId>", "File ID to download").option("--out <path>", "Output directory (default: current directory)").option("--name <filename>", "Output filename (default: original name)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
1867
+ try {
1868
+ const result = await downloadSharedDriveFile(
1869
+ opts.sharedrive,
1870
+ opts.fileId,
1871
+ opts.profile
1872
+ );
1873
+ const fileName = opts.name ?? result.fileName ?? opts.fileId;
1874
+ const outDir = opts.out ?? process.cwd();
1875
+ const outPath = join2(outDir, fileName);
1876
+ await writeFile2(outPath, result.buffer);
1877
+ output(
1878
+ {
1879
+ success: true,
1880
+ fileName,
1881
+ path: outPath,
1882
+ size: result.buffer.length
1883
+ },
1884
+ opts
1885
+ );
1886
+ } catch (err) {
1887
+ cliError(err, opts, "drive");
1888
+ }
1889
+ });
1890
+ var driveCommand = new Command7("drive").description("Drive operations (requires User OAuth with file scope)").addCommand(listCommand2).addCommand(uploadCommand).addCommand(downloadCommand).addCommand(sharedriveListCommand).addCommand(sharedriveFilesCommand).addCommand(sharedriveDownloadCommand);
1599
1891
 
1600
1892
  // src/commands/mail.ts
1893
+ import { writeFile as writeFile3 } from "fs/promises";
1894
+ import { join as join3 } from "path";
1601
1895
  import { Command as Command8 } from "commander";
1602
1896
 
1603
1897
  // src/api/mail.ts
1604
1898
  var BASE_URL4 = "https://www.worksapis.com/v1.0";
1605
- async function authedFetch3(url, init, profile) {
1606
- const token = await getValidUserToken(profile);
1607
- const headers = new Headers(init.headers);
1608
- headers.set("Authorization", `Bearer ${token}`);
1609
- return fetch(url, { ...init, headers });
1610
- }
1611
- async function handleError3(res) {
1612
- if (res.status === 401) {
1613
- throw new AuthError("User token expired. Run `nworks login --user --scope mail` again.");
1614
- }
1615
- let code = "UNKNOWN";
1616
- let description = `HTTP ${res.status}`;
1617
- try {
1618
- const body = await res.json();
1619
- code = body.code ?? code;
1620
- description = body.description ?? description;
1621
- } catch {
1622
- }
1623
- throw new ApiError(code, description, res.status);
1624
- }
1625
1899
  async function sendMail(opts) {
1626
1900
  const userId = opts.userId ?? "me";
1627
1901
  const profile = opts.profile ?? "default";
@@ -1637,7 +1911,7 @@ async function sendMail(opts) {
1637
1911
  if (opts.cc) body.cc = opts.cc;
1638
1912
  if (opts.bcc) body.bcc = opts.bcc;
1639
1913
  if (opts.contentType) body.contentType = opts.contentType;
1640
- const res = await authedFetch3(
1914
+ const res = await userFetch(
1641
1915
  url,
1642
1916
  {
1643
1917
  method: "POST",
@@ -1647,7 +1921,7 @@ async function sendMail(opts) {
1647
1921
  profile
1648
1922
  );
1649
1923
  if (res.status === 202) return;
1650
- if (!res.ok) return handleError3(res);
1924
+ if (!res.ok) return handleUserApiError(res);
1651
1925
  }
1652
1926
  async function listMails(folderId = 0, userId = "me", count = 30, cursor, isUnread, profile = "default") {
1653
1927
  const params = new URLSearchParams();
@@ -1658,8 +1932,8 @@ async function listMails(folderId = 0, userId = "me", count = 30, cursor, isUnre
1658
1932
  if (process.env["NWORKS_VERBOSE"] === "1") {
1659
1933
  console.error(`[nworks] GET ${url}`);
1660
1934
  }
1661
- const res = await authedFetch3(url, { method: "GET" }, profile);
1662
- if (!res.ok) return handleError3(res);
1935
+ const res = await userFetch(url, { method: "GET" }, profile);
1936
+ if (!res.ok) return handleUserApiError(res);
1663
1937
  const data = await res.json();
1664
1938
  return {
1665
1939
  mails: data.mails ?? [],
@@ -1674,10 +1948,22 @@ async function readMail(mailId, userId = "me", profile = "default") {
1674
1948
  if (process.env["NWORKS_VERBOSE"] === "1") {
1675
1949
  console.error(`[nworks] GET ${url}`);
1676
1950
  }
1677
- const res = await authedFetch3(url, { method: "GET" }, profile);
1678
- if (!res.ok) return handleError3(res);
1951
+ const res = await userFetch(url, { method: "GET" }, profile);
1952
+ if (!res.ok) return handleUserApiError(res);
1679
1953
  return await res.json();
1680
1954
  }
1955
+ async function downloadAttachment(mailId, attachmentId, userId = "me", profile = "default") {
1956
+ const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail/${sanitizePathSegment(
1957
+ String(mailId)
1958
+ )}/attachments/${sanitizePathSegment(String(attachmentId))}`;
1959
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1960
+ console.error(`[nworks] GET ${url}`);
1961
+ }
1962
+ const res = await userFetch(url, { method: "GET" }, profile);
1963
+ if (!res.ok) return handleUserApiError(res);
1964
+ const data = await res.json();
1965
+ return { buffer: Buffer.from(data.data, "base64"), filename: data.filename };
1966
+ }
1681
1967
 
1682
1968
  // src/commands/mail.ts
1683
1969
  var sendCommand2 = new Command8("send").description("Send a mail (requires User OAuth with mail scope)").requiredOption("--to <emails>", "Recipient email addresses (separate with ;)").requiredOption("--subject <subject>", "Mail subject").option("--body <body>", "Mail body content").option("--cc <emails>", "CC email addresses (separate with ;)").option("--bcc <emails>", "BCC email addresses (separate with ;)").option("--content-type <type>", "Body format: html or text", "html").option("--user <userId>", "Sender user ID (default: me)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").option("--dry-run", "Print request without sending").action(async (opts) => {
@@ -1765,38 +2051,43 @@ var readCommand = new Command8("read").description("Read a specific mail (requir
1765
2051
  cliError(err, opts, "mail");
1766
2052
  }
1767
2053
  });
1768
- var mailCommand = new Command8("mail").description("Mail operations (requires User OAuth with mail scope)").addCommand(sendCommand2).addCommand(listCommand3).addCommand(readCommand);
2054
+ var downloadAttachmentCommand = new Command8("download-attachment").description("Download a mail attachment (requires User OAuth with mail or mail.read scope)").requiredOption("--id <mailId>", "Mail ID (from mail list)").requiredOption("--attachment-id <attachmentId>", "Attachment ID (from mail read)").option("--out <path>", "Output directory (default: current directory)").option("--name <filename>", "Output filename (default: original name)").option("--user <userId>", "Target user ID (default: me)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
2055
+ try {
2056
+ const result = await downloadAttachment(
2057
+ parseInt(opts.id, 10),
2058
+ parseInt(opts.attachmentId, 10),
2059
+ opts.user ?? "me",
2060
+ opts.profile
2061
+ );
2062
+ const fileName = opts.name ?? result.filename;
2063
+ const outDir = opts.out ?? process.cwd();
2064
+ const outPath = join3(outDir, fileName);
2065
+ await writeFile3(outPath, result.buffer);
2066
+ output(
2067
+ {
2068
+ success: true,
2069
+ fileName,
2070
+ path: outPath,
2071
+ size: result.buffer.length
2072
+ },
2073
+ opts
2074
+ );
2075
+ } catch (err) {
2076
+ cliError(err, opts, "mail");
2077
+ }
2078
+ });
2079
+ var mailCommand = new Command8("mail").description("Mail operations (requires User OAuth with mail scope)").addCommand(sendCommand2).addCommand(listCommand3).addCommand(readCommand).addCommand(downloadAttachmentCommand);
1769
2080
 
1770
2081
  // src/commands/task.ts
1771
2082
  import { Command as Command9 } from "commander";
1772
2083
 
1773
2084
  // src/api/task.ts
1774
2085
  var BASE_URL5 = "https://www.worksapis.com/v1.0";
1775
- async function authedFetch4(url, init, profile) {
1776
- const token = await getValidUserToken(profile);
1777
- const headers = new Headers(init.headers);
1778
- headers.set("Authorization", `Bearer ${token}`);
1779
- return fetch(url, { ...init, headers });
1780
- }
1781
- async function handleError4(res) {
1782
- if (res.status === 401) {
1783
- throw new AuthError("User token expired. Run `nworks login --user --scope task` again.");
1784
- }
1785
- let code = "UNKNOWN";
1786
- let description = `HTTP ${res.status}`;
1787
- try {
1788
- const body = await res.json();
1789
- code = body.code ?? code;
1790
- description = body.description ?? description;
1791
- } catch {
1792
- }
1793
- throw new ApiError(code, description, res.status);
1794
- }
1795
2086
  async function resolveUserId(userId, profile) {
1796
2087
  if (userId !== "me") return userId;
1797
2088
  const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}`;
1798
- const res = await authedFetch4(url, { method: "GET" }, profile);
1799
- if (!res.ok) return handleError4(res);
2089
+ const res = await userFetch(url, { method: "GET" }, profile);
2090
+ if (!res.ok) return handleUserApiError(res);
1800
2091
  const data = await res.json();
1801
2092
  if (process.env["NWORKS_VERBOSE"] === "1") {
1802
2093
  console.error(`[nworks] Resolved "me" \u2192 ${data.userId}`);
@@ -1813,8 +2104,8 @@ async function listTasks(categoryId = "default", userId = "me", count = 50, curs
1813
2104
  if (process.env["NWORKS_VERBOSE"] === "1") {
1814
2105
  console.error(`[nworks] GET ${url}`);
1815
2106
  }
1816
- const res = await authedFetch4(url, { method: "GET" }, profile);
1817
- if (!res.ok) return handleError4(res);
2107
+ const res = await userFetch(url, { method: "GET" }, profile);
2108
+ if (!res.ok) return handleUserApiError(res);
1818
2109
  const data = await res.json();
1819
2110
  return { tasks: data.tasks ?? [], responseMetaData: data.responseMetaData };
1820
2111
  }
@@ -1838,7 +2129,7 @@ async function createTask(opts) {
1838
2129
  console.error(`[nworks] POST ${url}`);
1839
2130
  console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
1840
2131
  }
1841
- const res = await authedFetch4(
2132
+ const res = await userFetch(
1842
2133
  url,
1843
2134
  {
1844
2135
  method: "POST",
@@ -1850,7 +2141,7 @@ async function createTask(opts) {
1850
2141
  if (res.status === 201) {
1851
2142
  return await res.json();
1852
2143
  }
1853
- if (!res.ok) return handleError4(res);
2144
+ if (!res.ok) return handleUserApiError(res);
1854
2145
  return await res.json();
1855
2146
  }
1856
2147
  async function updateTask(opts) {
@@ -1863,7 +2154,7 @@ async function updateTask(opts) {
1863
2154
  if (process.env["NWORKS_VERBOSE"] === "1") {
1864
2155
  console.error(`[nworks] PATCH ${url}`);
1865
2156
  }
1866
- const res = await authedFetch4(
2157
+ const res = await userFetch(
1867
2158
  url,
1868
2159
  {
1869
2160
  method: "PATCH",
@@ -1872,7 +2163,7 @@ async function updateTask(opts) {
1872
2163
  },
1873
2164
  profile
1874
2165
  );
1875
- if (!res.ok) return handleError4(res);
2166
+ if (!res.ok) return handleUserApiError(res);
1876
2167
  return await res.json();
1877
2168
  }
1878
2169
  async function completeTask(taskId, profile = "default") {
@@ -1880,39 +2171,39 @@ async function completeTask(taskId, profile = "default") {
1880
2171
  if (process.env["NWORKS_VERBOSE"] === "1") {
1881
2172
  console.error(`[nworks] POST ${url}`);
1882
2173
  }
1883
- const res = await authedFetch4(
2174
+ const res = await userFetch(
1884
2175
  url,
1885
2176
  { method: "POST" },
1886
2177
  profile
1887
2178
  );
1888
2179
  if (res.status === 204) return;
1889
- if (!res.ok) return handleError4(res);
2180
+ if (!res.ok) return handleUserApiError(res);
1890
2181
  }
1891
2182
  async function incompleteTask(taskId, profile = "default") {
1892
2183
  const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}/incomplete`;
1893
2184
  if (process.env["NWORKS_VERBOSE"] === "1") {
1894
2185
  console.error(`[nworks] POST ${url}`);
1895
2186
  }
1896
- const res = await authedFetch4(
2187
+ const res = await userFetch(
1897
2188
  url,
1898
2189
  { method: "POST" },
1899
2190
  profile
1900
2191
  );
1901
2192
  if (res.status === 204) return;
1902
- if (!res.ok) return handleError4(res);
2193
+ if (!res.ok) return handleUserApiError(res);
1903
2194
  }
1904
2195
  async function deleteTask(taskId, profile = "default") {
1905
2196
  const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}`;
1906
2197
  if (process.env["NWORKS_VERBOSE"] === "1") {
1907
2198
  console.error(`[nworks] DELETE ${url}`);
1908
2199
  }
1909
- const res = await authedFetch4(
2200
+ const res = await userFetch(
1910
2201
  url,
1911
2202
  { method: "DELETE" },
1912
2203
  profile
1913
2204
  );
1914
2205
  if (res.status === 204) return;
1915
- if (!res.ok) return handleError4(res);
2206
+ if (!res.ok) return handleUserApiError(res);
1916
2207
  }
1917
2208
 
1918
2209
  // src/commands/task.ts
@@ -2035,32 +2326,12 @@ import { Command as Command10 } from "commander";
2035
2326
 
2036
2327
  // src/api/board.ts
2037
2328
  var BASE_URL6 = "https://www.worksapis.com/v1.0";
2038
- async function authedFetch5(url, init, profile) {
2039
- const token = await getValidUserToken(profile);
2040
- const headers = new Headers(init.headers);
2041
- headers.set("Authorization", `Bearer ${token}`);
2042
- return fetch(url, { ...init, headers });
2043
- }
2044
- async function handleError5(res) {
2045
- if (res.status === 401) {
2046
- throw new AuthError("User token expired. Run `nworks login --user --scope board` again.");
2047
- }
2048
- let code = "UNKNOWN";
2049
- let description = `HTTP ${res.status}`;
2050
- try {
2051
- const body = await res.json();
2052
- code = body.code ?? code;
2053
- description = body.description ?? description;
2054
- } catch {
2055
- }
2056
- throw new ApiError(code, description, res.status);
2057
- }
2058
- function safeParseJson(text) {
2059
- const safe = text.replace(
2060
- /"((?:board|post|domain|user)Id)"\s*:\s*(\d{16,})/g,
2061
- '"$1":"$2"'
2062
- );
2063
- return JSON.parse(safe);
2329
+ function safeParseJson(text) {
2330
+ const safe = text.replace(
2331
+ /"((?:board|post|domain|user)Id)"\s*:\s*(\d{16,})/g,
2332
+ '"$1":"$2"'
2333
+ );
2334
+ return JSON.parse(safe);
2064
2335
  }
2065
2336
  async function listBoards(count = 20, cursor, profile = "default") {
2066
2337
  const params = new URLSearchParams();
@@ -2070,8 +2341,8 @@ async function listBoards(count = 20, cursor, profile = "default") {
2070
2341
  if (process.env["NWORKS_VERBOSE"] === "1") {
2071
2342
  console.error(`[nworks] GET ${url}`);
2072
2343
  }
2073
- const res = await authedFetch5(url, { method: "GET" }, profile);
2074
- if (!res.ok) return handleError5(res);
2344
+ const res = await userFetch(url, { method: "GET" }, profile);
2345
+ if (!res.ok) return handleUserApiError(res);
2075
2346
  const text = await res.text();
2076
2347
  if (process.env["NWORKS_VERBOSE"] === "1") {
2077
2348
  console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
@@ -2087,8 +2358,8 @@ async function listPosts(boardId, count = 20, cursor, profile = "default") {
2087
2358
  if (process.env["NWORKS_VERBOSE"] === "1") {
2088
2359
  console.error(`[nworks] GET ${url}`);
2089
2360
  }
2090
- const res = await authedFetch5(url, { method: "GET" }, profile);
2091
- if (!res.ok) return handleError5(res);
2361
+ const res = await userFetch(url, { method: "GET" }, profile);
2362
+ if (!res.ok) return handleUserApiError(res);
2092
2363
  const text = await res.text();
2093
2364
  if (process.env["NWORKS_VERBOSE"] === "1") {
2094
2365
  console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
@@ -2101,8 +2372,8 @@ async function readPost(boardId, postId, profile = "default") {
2101
2372
  if (process.env["NWORKS_VERBOSE"] === "1") {
2102
2373
  console.error(`[nworks] GET ${url}`);
2103
2374
  }
2104
- const res = await authedFetch5(url, { method: "GET" }, profile);
2105
- if (!res.ok) return handleError5(res);
2375
+ const res = await userFetch(url, { method: "GET" }, profile);
2376
+ if (!res.ok) return handleUserApiError(res);
2106
2377
  const text = await res.text();
2107
2378
  if (process.env["NWORKS_VERBOSE"] === "1") {
2108
2379
  console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
@@ -2122,7 +2393,7 @@ async function createPost(opts) {
2122
2393
  console.error(`[nworks] POST ${url}`);
2123
2394
  console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
2124
2395
  }
2125
- const res = await authedFetch5(
2396
+ const res = await userFetch(
2126
2397
  url,
2127
2398
  {
2128
2399
  method: "POST",
@@ -2138,7 +2409,7 @@ async function createPost(opts) {
2138
2409
  }
2139
2410
  return safeParseJson(text);
2140
2411
  }
2141
- return handleError5(res);
2412
+ return handleUserApiError(res);
2142
2413
  }
2143
2414
 
2144
2415
  // src/commands/board.ts
@@ -2236,128 +2507,256 @@ var createCommand3 = new Command10("create").description("Create a post in a boa
2236
2507
  });
2237
2508
  var boardCommand = new Command10("board").description("Board operations (requires User OAuth)").addCommand(listCommand5).addCommand(postsCommand).addCommand(readCommand2).addCommand(createCommand3);
2238
2509
 
2239
- // src/commands/mcp-cmd.ts
2240
- import { Command as Command12 } from "commander";
2241
-
2242
- // src/mcp/server.ts
2243
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2244
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2245
-
2246
- // src/mcp/tools.ts
2247
- import { z } from "zod";
2248
-
2249
- // src/commands/doctor.ts
2510
+ // src/commands/contact.ts
2250
2511
  import { Command as Command11 } from "commander";
2251
- import { existsSync as existsSync3 } from "fs";
2252
- import { readFile as readFile5 } from "fs/promises";
2253
- async function runChecks(profile) {
2254
- const results = [];
2255
- let creds;
2256
- try {
2257
- creds = await loadCredentials(profile);
2258
- results.push({ check: "credentials", status: "OK", detail: `clientId: ${creds.clientId}` });
2259
- } catch {
2260
- 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)" });
2261
- return results;
2512
+
2513
+ // src/api/contact.ts
2514
+ var BASE_URL7 = "https://www.worksapis.com/v1.0";
2515
+ async function listContacts(opts = {}) {
2516
+ const userId = opts.userId ?? "me";
2517
+ const profile = opts.profile ?? "default";
2518
+ const params = new URLSearchParams();
2519
+ params.set("count", String(opts.count ?? 20));
2520
+ if (opts.cursor) params.set("cursor", opts.cursor);
2521
+ if (opts.accessibleRange) params.set("accessibleRange", opts.accessibleRange);
2522
+ if (opts.contactTagId) params.set("contactTagId", opts.contactTagId);
2523
+ if (opts.email) params.set("email", opts.email);
2524
+ if (opts.telephone) params.set("telephone", opts.telephone);
2525
+ if (opts.orderBy) params.set("orderBy", opts.orderBy);
2526
+ if (opts.searchDateType) params.set("searchDateType", opts.searchDateType);
2527
+ if (opts.startDateTime) params.set("startDateTime", opts.startDateTime);
2528
+ if (opts.endDateTime) params.set("endDateTime", opts.endDateTime);
2529
+ const url = `${BASE_URL7}/users/${sanitizePathSegment(userId)}/contacts?${params.toString()}`;
2530
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2531
+ console.error(`[nworks] GET ${url}`);
2262
2532
  }
2263
- if (hasServiceAccountCreds(creds)) {
2264
- results.push({ check: "serviceAccount", status: "OK", detail: creds.serviceAccount });
2265
- } else {
2266
- results.push({ check: "serviceAccount", status: "SKIP", detail: "\uBBF8\uC124\uC815 (\uBD07 \uBA54\uC2DC\uC9C0 \uC0AC\uC6A9 \uC2DC \uD544\uC694)" });
2533
+ const res = await userFetch(url, { method: "GET" }, profile);
2534
+ if (!res.ok) return handleUserApiError(res);
2535
+ const data = await res.json();
2536
+ return { contacts: data.contacts ?? [], responseMetaData: data.responseMetaData };
2537
+ }
2538
+ async function getContact(contactId, profile = "default") {
2539
+ const url = `${BASE_URL7}/contacts/${sanitizePathSegment(contactId)}`;
2540
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2541
+ console.error(`[nworks] GET ${url}`);
2267
2542
  }
2268
- if (creds.privateKeyPath) {
2269
- if (existsSync3(creds.privateKeyPath)) {
2270
- try {
2271
- await readFile5(creds.privateKeyPath, "utf-8");
2272
- results.push({ check: "privateKey", status: "OK", detail: creds.privateKeyPath });
2273
- } catch {
2274
- results.push({ check: "privateKey", status: "FAIL", detail: `\uC77D\uAE30 \uBD88\uAC00: ${creds.privateKeyPath}` });
2275
- }
2276
- } else {
2277
- results.push({ check: "privateKey", status: "FAIL", detail: `\uD30C\uC77C \uC5C6\uC74C: ${creds.privateKeyPath}` });
2278
- }
2279
- } else {
2280
- results.push({ check: "privateKey", status: "SKIP", detail: "\uBBF8\uC124\uC815" });
2543
+ const res = await userFetch(url, { method: "GET" }, profile);
2544
+ if (!res.ok) return handleUserApiError(res);
2545
+ return await res.json();
2546
+ }
2547
+ async function createContact(payload, profile = "default") {
2548
+ const url = `${BASE_URL7}/contacts`;
2549
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2550
+ console.error(`[nworks] POST ${url}`);
2281
2551
  }
2282
- if (creds.botId) {
2283
- results.push({ check: "botId", status: "OK", detail: creds.botId });
2284
- } else {
2285
- results.push({ check: "botId", status: "SKIP", detail: "\uBBF8\uC124\uC815 (\uBA54\uC2DC\uC9C0 \uC804\uC1A1 \uC2DC \uD544\uC694)" });
2552
+ const res = await userFetch(
2553
+ url,
2554
+ {
2555
+ method: "POST",
2556
+ headers: { "Content-Type": "application/json" },
2557
+ body: JSON.stringify(payload)
2558
+ },
2559
+ profile
2560
+ );
2561
+ if (!res.ok) return handleUserApiError(res);
2562
+ return await res.json();
2563
+ }
2564
+ async function updateContact(contactId, payload, partial = true, profile = "default") {
2565
+ const url = `${BASE_URL7}/contacts/${sanitizePathSegment(contactId)}`;
2566
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2567
+ console.error(`[nworks] ${partial ? "PATCH" : "PUT"} ${url}`);
2286
2568
  }
2287
- const token = await loadToken(profile);
2288
- if (token) {
2289
- const valid = token.expiresAt > Date.now() / 1e3;
2290
- results.push({
2291
- check: "serviceToken",
2292
- status: valid ? "OK" : "EXPIRED",
2293
- detail: valid ? `\uB9CC\uB8CC: ${new Date(token.expiresAt * 1e3).toISOString()}` : `\uB9CC\uB8CC\uB428: ${new Date(token.expiresAt * 1e3).toISOString()}`
2294
- });
2295
- } else {
2296
- results.push({ check: "serviceToken", status: "SKIP", detail: "\uD1A0\uD070 \uC5C6\uC74C" });
2569
+ const res = await userFetch(
2570
+ url,
2571
+ {
2572
+ method: partial ? "PATCH" : "PUT",
2573
+ headers: { "Content-Type": "application/json" },
2574
+ body: JSON.stringify(payload)
2575
+ },
2576
+ profile
2577
+ );
2578
+ if (!res.ok) return handleUserApiError(res);
2579
+ return await res.json();
2580
+ }
2581
+ async function deleteContact(contactId, profile = "default") {
2582
+ const url = `${BASE_URL7}/contacts/${sanitizePathSegment(contactId)}`;
2583
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2584
+ console.error(`[nworks] DELETE ${url}`);
2297
2585
  }
2298
- const userToken = await loadUserToken(profile);
2299
- if (userToken) {
2300
- const valid = userToken.expiresAt > Date.now() / 1e3;
2301
- results.push({
2302
- check: "userOAuth",
2303
- status: valid ? "OK" : "EXPIRED",
2304
- detail: valid ? `scope: ${userToken.scope} | \uB9CC\uB8CC: ${new Date(userToken.expiresAt * 1e3).toISOString()}` : `\uB9CC\uB8CC\uB428 | scope: ${userToken.scope}`
2586
+ const res = await userFetch(url, { method: "DELETE" }, profile);
2587
+ if (res.status === 204) return;
2588
+ if (!res.ok) return handleUserApiError(res);
2589
+ }
2590
+ async function listContactTags(opts = {}) {
2591
+ const userId = opts.userId ?? "me";
2592
+ const profile = opts.profile ?? "default";
2593
+ const params = new URLSearchParams();
2594
+ params.set("count", String(opts.count ?? 20));
2595
+ if (opts.cursor) params.set("cursor", opts.cursor);
2596
+ const url = `${BASE_URL7}/users/${sanitizePathSegment(userId)}/contact-tags?${params.toString()}`;
2597
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2598
+ console.error(`[nworks] GET ${url}`);
2599
+ }
2600
+ const res = await userFetch(url, { method: "GET" }, profile);
2601
+ if (!res.ok) return handleUserApiError(res);
2602
+ const data = await res.json();
2603
+ return { contactTags: data.contactTags ?? [], responseMetaData: data.responseMetaData };
2604
+ }
2605
+
2606
+ // src/commands/contact.ts
2607
+ var listCommand6 = new Command11("list").description("List contacts (requires User OAuth with contact or contact.read scope)").option("--user <userId>", "Target user ID (default: me)").option("--count <n>", "Items per page (default: 20)", "20").option("--cursor <cursor>", "Pagination cursor").option("--tag <contactTagId>", "Filter by contact tag ID").option("--email <email>", "Filter by email").option("--tel <telephone>", "Filter by phone number").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
2608
+ try {
2609
+ const result = await listContacts({
2610
+ userId: opts.user ?? "me",
2611
+ count: parseInt(opts.count, 10),
2612
+ cursor: opts.cursor,
2613
+ contactTagId: opts.tag,
2614
+ email: opts.email,
2615
+ telephone: opts.tel,
2616
+ profile: opts.profile
2305
2617
  });
2306
- } else {
2307
- results.push({ check: "userOAuth", status: "SKIP", detail: "\uD1A0\uD070 \uC5C6\uC74C. `nworks login --user` \uD544\uC694" });
2618
+ output(
2619
+ {
2620
+ contacts: result.contacts,
2621
+ count: result.contacts.length,
2622
+ nextCursor: result.responseMetaData?.nextCursor ?? null
2623
+ },
2624
+ opts
2625
+ );
2626
+ } catch (err) {
2627
+ cliError(err, opts, "contact");
2308
2628
  }
2309
- if (hasServiceAccountCreds(creds) && token && token.expiresAt > Date.now() / 1e3) {
2310
- try {
2311
- const res = await fetch("https://www.worksapis.com/v1.0/users/me", {
2312
- headers: { Authorization: `Bearer ${token.accessToken}` }
2313
- });
2314
- if (res.ok) {
2315
- results.push({ check: "apiConnection", status: "OK", detail: "NAVER WORKS API \uC5F0\uACB0 \uC131\uACF5" });
2316
- } else {
2317
- results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
2318
- }
2319
- } catch (e) {
2320
- results.push({ check: "apiConnection", status: "FAIL", detail: `\uC5F0\uACB0 \uC2E4\uD328: ${e.message}` });
2321
- }
2322
- } else if (userToken && userToken.expiresAt > Date.now() / 1e3) {
2323
- try {
2324
- const res = await fetch("https://www.worksapis.com/v1.0/users/me", {
2325
- headers: { Authorization: `Bearer ${userToken.accessToken}` }
2326
- });
2327
- if (res.ok) {
2328
- results.push({ check: "apiConnection", status: "OK", detail: "NAVER WORKS API \uC5F0\uACB0 \uC131\uACF5" });
2329
- } else {
2330
- results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
2331
- }
2332
- } catch (e) {
2333
- results.push({ check: "apiConnection", status: "FAIL", detail: `\uC5F0\uACB0 \uC2E4\uD328: ${e.message}` });
2334
- }
2335
- } else {
2336
- results.push({ check: "apiConnection", status: "SKIP", detail: "\uC720\uD6A8\uD55C \uD1A0\uD070 \uC5C6\uC74C \u2014 API \uD14C\uC2A4\uD2B8 \uAC74\uB108\uB700" });
2629
+ });
2630
+ var getCommand = new Command11("get").description("Get a contact by ID (requires User OAuth with contact or contact.read scope)").requiredOption("--id <contactId>", "Contact ID (from contact list)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
2631
+ try {
2632
+ const result = await getContact(opts.id, opts.profile);
2633
+ output(result, opts);
2634
+ } catch (err) {
2635
+ cliError(err, opts, "contact");
2337
2636
  }
2338
- return results;
2339
- }
2340
- var doctorCommand = new Command11("doctor").description("Check nworks configuration and connectivity").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
2637
+ });
2638
+ var createCommand4 = new Command11("create").description("Create a contact (requires User OAuth with contact scope)").requiredOption("--payload <json>", "Contact fields as JSON (passed through to the NAVER WORKS API)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").option("--dry-run", "Print request without creating").action(async (opts) => {
2341
2639
  try {
2342
- const results = await runChecks(opts.profile);
2343
- if (opts.json || !process.stdout.isTTY) {
2344
- output(results, opts);
2345
- } else {
2346
- console.log("\n nworks doctor\n");
2347
- for (const r of results) {
2348
- const icon = r.status === "OK" ? "\u2705" : r.status === "SKIP" ? "\u2796" : "\u274C";
2349
- console.log(` ${icon} ${r.check.padEnd(16)} ${r.detail}`);
2350
- }
2351
- console.log();
2640
+ const payload = JSON.parse(opts.payload);
2641
+ if (opts.dryRun) {
2642
+ output({ dryRun: true, request: payload }, opts);
2643
+ return;
2352
2644
  }
2645
+ const result = await createContact(payload, opts.profile);
2646
+ output({ success: true, ...result }, opts);
2353
2647
  } catch (err) {
2354
- cliError(err, opts);
2648
+ cliError(err, opts, "contact");
2649
+ }
2650
+ });
2651
+ var updateCommand3 = new Command11("update").description("Update a contact (requires User OAuth with contact scope)").requiredOption("--id <contactId>", "Contact ID (from contact list)").requiredOption("--payload <json>", "Fields to update as JSON (passed through to the NAVER WORKS API)").option("--replace", "Use PUT instead of PATCH (replace entire contact)", false).option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
2652
+ try {
2653
+ const payload = JSON.parse(opts.payload);
2654
+ const result = await updateContact(
2655
+ opts.id,
2656
+ payload,
2657
+ !opts.replace,
2658
+ opts.profile
2659
+ );
2660
+ output({ success: true, ...result }, opts);
2661
+ } catch (err) {
2662
+ cliError(err, opts, "contact");
2663
+ }
2664
+ });
2665
+ var deleteCommand3 = new Command11("delete").description("Delete a contact (requires User OAuth with contact scope)").requiredOption("--id <contactId>", "Contact ID (from contact list)").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
2666
+ try {
2667
+ await deleteContact(opts.id, opts.profile);
2668
+ output({ success: true, contactId: opts.id, message: "Contact deleted" }, opts);
2669
+ } catch (err) {
2670
+ cliError(err, opts, "contact");
2671
+ }
2672
+ });
2673
+ var listTagsCommand = new Command11("list-tags").description("List contact tags (requires User OAuth with contact or contact.read scope)").option("--user <userId>", "Target user ID (default: me)").option("--count <n>", "Items per page (default: 20)", "20").option("--cursor <cursor>", "Pagination cursor").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
2674
+ try {
2675
+ const result = await listContactTags({
2676
+ userId: opts.user ?? "me",
2677
+ count: parseInt(opts.count, 10),
2678
+ cursor: opts.cursor,
2679
+ profile: opts.profile
2680
+ });
2681
+ output(
2682
+ {
2683
+ contactTags: result.contactTags,
2684
+ count: result.contactTags.length,
2685
+ nextCursor: result.responseMetaData?.nextCursor ?? null
2686
+ },
2687
+ opts
2688
+ );
2689
+ } catch (err) {
2690
+ cliError(err, opts, "contact");
2355
2691
  }
2356
2692
  });
2693
+ var contactCommand = new Command11("contact").description("Contact operations (requires User OAuth with contact scope)").addCommand(listCommand6).addCommand(getCommand).addCommand(createCommand4).addCommand(updateCommand3).addCommand(deleteCommand3).addCommand(listTagsCommand);
2357
2694
 
2358
- // src/mcp/tools.ts
2359
- function registerTools(server) {
2360
- server.tool(
2695
+ // src/commands/mcp-cmd.ts
2696
+ import { Command as Command13 } from "commander";
2697
+ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
2698
+
2699
+ // src/mcp/server.ts
2700
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2701
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2702
+
2703
+ // src/mcp/tools/setup.ts
2704
+ import { z as z2 } from "zod";
2705
+
2706
+ // src/mcp/tool-registry.ts
2707
+ var TOOL_ANNOTATIONS = {
2708
+ nworks_setup: { title: "Save credentials", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
2709
+ nworks_message_send: { title: "Send message", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2710
+ nworks_message_members: { title: "List channel members", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2711
+ nworks_directory_members: { title: "List organization members", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2712
+ nworks_calendar_list: { title: "List calendar events", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2713
+ nworks_calendar_create: { title: "Create calendar event", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2714
+ nworks_calendar_update: { title: "Update calendar event", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2715
+ nworks_calendar_delete: { title: "Delete calendar event", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2716
+ nworks_drive_list: { title: "List drive files", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2717
+ nworks_drive_upload: { title: "Upload file to drive", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2718
+ // download는 outputDir 지정 시 로컬 파일을 쓰므로 순수 읽기가 아니다(readOnlyHint=false).
2719
+ nworks_drive_download: { title: "Download drive file", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
2720
+ nworks_sharedrive_list: { title: "List shared drives", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2721
+ nworks_sharedrive_files: { title: "List shared drive files", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2722
+ nworks_sharedrive_download: { title: "Download shared drive file", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
2723
+ nworks_mail_send: { title: "Send mail", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2724
+ nworks_mail_list: { title: "List mail", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2725
+ nworks_mail_read: { title: "Read mail", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2726
+ // drive_download와 같은 이유로 outputDir 지정 시 로컬 파일을 쓴다(readOnlyHint=false).
2727
+ nworks_mail_download_attachment: { title: "Download mail attachment", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
2728
+ nworks_task_list: { title: "List tasks", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2729
+ nworks_task_create: { title: "Create task", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2730
+ nworks_task_update: { title: "Update task", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2731
+ nworks_task_delete: { title: "Delete task", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2732
+ nworks_board_list: { title: "List boards", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2733
+ nworks_board_posts: { title: "List board posts", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2734
+ nworks_board_read: { title: "Read board post", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2735
+ nworks_board_create: { title: "Create board post", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2736
+ nworks_contact_list: { title: "List contacts", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2737
+ nworks_contact_get: { title: "Get contact", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2738
+ nworks_contact_create: { title: "Create contact", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2739
+ nworks_contact_update: { title: "Update contact", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2740
+ nworks_contact_delete: { title: "Delete contact", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2741
+ nworks_contact_list_tags: { title: "List contact tags", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2742
+ nworks_login_user: { title: "Log in with User OAuth", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2743
+ nworks_whoami: { title: "Show auth status", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2744
+ nworks_logout: { title: "Log out", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2745
+ nworks_doctor: { title: "Diagnose connection", readOnlyHint: true, idempotentHint: true, openWorldHint: true }
2746
+ };
2747
+ function defineTool(server, name, description, inputSchema, handler) {
2748
+ const annotations = TOOL_ANNOTATIONS[name];
2749
+ server.registerTool(
2750
+ name,
2751
+ { description, inputSchema, ...annotations ? { annotations } : {} },
2752
+ handler
2753
+ );
2754
+ }
2755
+
2756
+ // src/mcp/tools/setup.ts
2757
+ function registerSetupTools(server) {
2758
+ defineTool(
2759
+ server,
2361
2760
  "nworks_setup",
2362
2761
  `NAVER WORKS API \uC778\uC99D \uC815\uBCF4\uB97C \uC124\uC815\uD569\uB2C8\uB2E4.
2363
2762
 
@@ -2380,10 +2779,10 @@ function registerTools(server) {
2380
2779
 
2381
2780
  OAuth Redirect URI: http://localhost:9876/callback`,
2382
2781
  {
2383
- clientId: z.string().describe("Client ID (Developer Console\uC5D0\uC11C \uBC1C\uAE09)"),
2384
- serviceAccount: z.string().optional().describe("Service Account ID (\uC608: xxxxx.serviceaccount@domain)"),
2385
- botId: z.string().optional().describe("Bot ID (\uBA54\uC2DC\uC9C0 \uC804\uC1A1 \uC2DC \uD544\uC694)"),
2386
- domainId: z.string().optional().describe("Domain ID")
2782
+ clientId: z2.string().describe("Client ID (Developer Console\uC5D0\uC11C \uBC1C\uAE09)"),
2783
+ serviceAccount: z2.string().optional().describe("Service Account ID (\uC608: xxxxx.serviceaccount@domain)"),
2784
+ botId: z2.string().optional().describe("Bot ID (\uBA54\uC2DC\uC9C0 \uC804\uC1A1 \uC2DC \uD544\uC694)"),
2785
+ domainId: z2.string().optional().describe("Domain ID")
2387
2786
  },
2388
2787
  async ({ clientId, serviceAccount, botId, domainId }) => {
2389
2788
  try {
@@ -2421,7 +2820,7 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2421
2820
  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');
2422
2821
  }
2423
2822
  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");
2424
- const mask = (s) => s.length <= 4 ? "****" : `****${s.slice(-Math.min(4, Math.floor(s.length / 3)))}`;
2823
+ const mask2 = (s) => s.length <= 4 ? "****" : `****${s.slice(-Math.min(4, Math.floor(s.length / 3)))}`;
2425
2824
  return {
2426
2825
  content: [
2427
2826
  {
@@ -2430,10 +2829,10 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2430
2829
  success: true,
2431
2830
  message: "\uC778\uC99D \uC815\uBCF4\uAC00 \uC800\uC7A5\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
2432
2831
  nextSteps,
2433
- clientId: mask(clientId),
2434
- clientSecret: `${mask(resolvedSecret)} (\uD658\uACBD\uBCC0\uC218)`,
2832
+ clientId: mask2(clientId),
2833
+ clientSecret: `${mask2(resolvedSecret)} (\uD658\uACBD\uBCC0\uC218)`,
2435
2834
  serviceAccount: serviceAccount ?? null,
2436
- privateKeyPath: resolvedPrivateKeyPath ? `${mask(resolvedPrivateKeyPath)} (\uD658\uACBD\uBCC0\uC218)` : null,
2835
+ privateKeyPath: resolvedPrivateKeyPath ? `${mask2(resolvedPrivateKeyPath)} (\uD658\uACBD\uBCC0\uC218)` : null,
2437
2836
  botId: botId ?? null
2438
2837
  })
2439
2838
  }
@@ -2447,16 +2846,22 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2447
2846
  }
2448
2847
  }
2449
2848
  );
2450
- server.tool(
2849
+ }
2850
+
2851
+ // src/mcp/tools/message.ts
2852
+ import { z as z3 } from "zod";
2853
+ function registerMessageTools(server) {
2854
+ defineTool(
2855
+ server,
2451
2856
  "nworks_message_send",
2452
2857
  "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)",
2453
2858
  {
2454
- to: z.string().optional().describe("\uC218\uC2E0\uC790 userId (channel\uACFC \uD0DD 1). nworks_directory_members\uB85C userId \uC870\uD68C \uAC00\uB2A5"),
2455
- 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"),
2456
- text: z.string().describe("\uBA54\uC2DC\uC9C0 \uBCF8\uBB38"),
2457
- type: z.enum(["text", "button", "list"]).optional().describe("\uBA54\uC2DC\uC9C0 \uD0C0\uC785 (\uAE30\uBCF8: text)"),
2458
- actions: z.string().optional().describe("\uBC84\uD2BC \uC561\uC158 JSON (type=button\uC77C \uB54C)"),
2459
- elements: z.string().optional().describe("\uB9AC\uC2A4\uD2B8 \uD56D\uBAA9 JSON (type=list\uC77C \uB54C)")
2859
+ to: z3.string().optional().describe("\uC218\uC2E0\uC790 userId (channel\uACFC \uD0DD 1). nworks_directory_members\uB85C userId \uC870\uD68C \uAC00\uB2A5"),
2860
+ 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"),
2861
+ text: z3.string().describe("\uBA54\uC2DC\uC9C0 \uBCF8\uBB38"),
2862
+ type: z3.enum(["text", "button", "list"]).optional().describe("\uBA54\uC2DC\uC9C0 \uD0C0\uC785 (\uAE30\uBCF8: text)"),
2863
+ actions: z3.string().optional().describe("\uBC84\uD2BC \uC561\uC158 JSON (type=button\uC77C \uB54C)"),
2864
+ elements: z3.string().optional().describe("\uB9AC\uC2A4\uD2B8 \uD56D\uBAA9 JSON (type=list\uC77C \uB54C)")
2460
2865
  },
2461
2866
  async ({ to, channel, text, type, actions, elements }) => {
2462
2867
  try {
@@ -2479,11 +2884,12 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2479
2884
  }
2480
2885
  }
2481
2886
  );
2482
- server.tool(
2887
+ defineTool(
2888
+ server,
2483
2889
  "nworks_message_members",
2484
2890
  "\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)",
2485
2891
  {
2486
- channel: z.string().describe("\uCC44\uB110 channelId")
2892
+ channel: z3.string().describe("\uCC44\uB110 channelId")
2487
2893
  },
2488
2894
  async ({ channel }) => {
2489
2895
  try {
@@ -2499,15 +2905,29 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2499
2905
  }
2500
2906
  }
2501
2907
  );
2502
- server.tool(
2908
+ }
2909
+
2910
+ // src/mcp/tools/directory.ts
2911
+ import { z as z4 } from "zod";
2912
+ function registerDirectoryTools(server) {
2913
+ defineTool(
2914
+ server,
2503
2915
  "nworks_directory_members",
2504
- "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",
2505
- {},
2506
- async () => {
2916
+ "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",
2917
+ {
2918
+ count: z4.number().int().min(1).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 100)"),
2919
+ cursor: z4.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C (\uC774\uC804 \uC751\uB2F5\uC758 nextCursor \uAC12)")
2920
+ },
2921
+ async ({ count, cursor }) => {
2507
2922
  try {
2508
- const result = await listUsers();
2923
+ const result = await listUsers(count ?? 100, cursor);
2509
2924
  return {
2510
- content: [{ type: "text", text: JSON.stringify(result) }]
2925
+ content: [{ type: "text", text: JSON.stringify({
2926
+ users: result.users,
2927
+ count: result.users.length,
2928
+ hasMore: !!result.responseMetaData?.nextCursor,
2929
+ nextCursor: result.responseMetaData?.nextCursor ?? null
2930
+ }) }]
2511
2931
  };
2512
2932
  } catch (err) {
2513
2933
  return {
@@ -2517,13 +2937,19 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2517
2937
  }
2518
2938
  }
2519
2939
  );
2520
- server.tool(
2940
+ }
2941
+
2942
+ // src/mcp/tools/calendar.ts
2943
+ import { z as z5 } from "zod";
2944
+ function registerCalendarTools(server) {
2945
+ defineTool(
2946
+ server,
2521
2947
  "nworks_calendar_list",
2522
2948
  "\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",
2523
2949
  {
2524
- fromDateTime: z.string().describe("\uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss+09:00)"),
2525
- untilDateTime: z.string().describe("\uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss+09:00)"),
2526
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2950
+ fromDateTime: z5.string().describe("\uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss+09:00)"),
2951
+ untilDateTime: z5.string().describe("\uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss+09:00)"),
2952
+ userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2527
2953
  },
2528
2954
  async ({ fromDateTime, untilDateTime, userId }) => {
2529
2955
  try {
@@ -2552,19 +2978,20 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2552
2978
  }
2553
2979
  }
2554
2980
  );
2555
- server.tool(
2981
+ defineTool(
2982
+ server,
2556
2983
  "nworks_calendar_create",
2557
2984
  "\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)",
2558
2985
  {
2559
- summary: z.string().describe("\uC77C\uC815 \uC81C\uBAA9"),
2560
- start: z.string().describe("\uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
2561
- end: z.string().describe("\uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
2562
- timeZone: z.string().optional().describe("\uD0C0\uC784\uC874 (\uAE30\uBCF8: Asia/Seoul)"),
2563
- description: z.string().optional().describe("\uC77C\uC815 \uC124\uBA85"),
2564
- location: z.string().optional().describe("\uC7A5\uC18C"),
2565
- attendees: z.array(z.object({ email: z.string(), displayName: z.string().optional() })).optional().describe("\uCC38\uC11D\uC790 \uBAA9\uB85D"),
2566
- sendNotification: z.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
2567
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2986
+ summary: z5.string().describe("\uC77C\uC815 \uC81C\uBAA9"),
2987
+ start: z5.string().describe("\uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
2988
+ end: z5.string().describe("\uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
2989
+ timeZone: z5.string().optional().describe("\uD0C0\uC784\uC874 (\uAE30\uBCF8: Asia/Seoul)"),
2990
+ description: z5.string().optional().describe("\uC77C\uC815 \uC124\uBA85"),
2991
+ location: z5.string().optional().describe("\uC7A5\uC18C"),
2992
+ attendees: z5.array(z5.object({ email: z5.string(), displayName: z5.string().optional() })).optional().describe("\uCC38\uC11D\uC790 \uBAA9\uB85D"),
2993
+ sendNotification: z5.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
2994
+ userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2568
2995
  },
2569
2996
  async ({
2570
2997
  summary,
@@ -2601,19 +3028,20 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2601
3028
  }
2602
3029
  }
2603
3030
  );
2604
- server.tool(
3031
+ defineTool(
3032
+ server,
2605
3033
  "nworks_calendar_update",
2606
3034
  "\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",
2607
3035
  {
2608
- eventId: z.string().describe("\uC77C\uC815 ID (nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2609
- summary: z.string().optional().describe("\uC0C8 \uC81C\uBAA9"),
2610
- start: z.string().optional().describe("\uC0C8 \uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
2611
- end: z.string().optional().describe("\uC0C8 \uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
2612
- timeZone: z.string().optional().describe("\uD0C0\uC784\uC874 (\uAE30\uBCF8: Asia/Seoul)"),
2613
- description: z.string().optional().describe("\uC0C8 \uC124\uBA85"),
2614
- location: z.string().optional().describe("\uC0C8 \uC7A5\uC18C"),
2615
- sendNotification: z.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
2616
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
3036
+ eventId: z5.string().describe("\uC77C\uC815 ID (nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3037
+ summary: z5.string().optional().describe("\uC0C8 \uC81C\uBAA9"),
3038
+ start: z5.string().optional().describe("\uC0C8 \uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
3039
+ end: z5.string().optional().describe("\uC0C8 \uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
3040
+ timeZone: z5.string().optional().describe("\uD0C0\uC784\uC874 (\uAE30\uBCF8: Asia/Seoul)"),
3041
+ description: z5.string().optional().describe("\uC0C8 \uC124\uBA85"),
3042
+ location: z5.string().optional().describe("\uC0C8 \uC7A5\uC18C"),
3043
+ sendNotification: z5.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
3044
+ userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2617
3045
  },
2618
3046
  async ({
2619
3047
  eventId,
@@ -2649,13 +3077,14 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2649
3077
  }
2650
3078
  }
2651
3079
  );
2652
- server.tool(
3080
+ defineTool(
3081
+ server,
2653
3082
  "nworks_calendar_delete",
2654
3083
  "\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",
2655
3084
  {
2656
- eventId: z.string().describe("\uC0AD\uC81C\uD560 \uC77C\uC815 ID (nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2657
- sendNotification: z.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
2658
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
3085
+ eventId: z5.string().describe("\uC0AD\uC81C\uD560 \uC77C\uC815 ID (nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3086
+ sendNotification: z5.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
3087
+ userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2659
3088
  },
2660
3089
  async ({ eventId, sendNotification, userId }) => {
2661
3090
  try {
@@ -2675,14 +3104,20 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2675
3104
  }
2676
3105
  }
2677
3106
  );
2678
- server.tool(
3107
+ }
3108
+
3109
+ // src/mcp/tools/drive.ts
3110
+ import { z as z6 } from "zod";
3111
+ function registerDriveTools(server) {
3112
+ defineTool(
3113
+ server,
2679
3114
  "nworks_drive_list",
2680
3115
  "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)",
2681
3116
  {
2682
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
2683
- folderId: z.string().optional().describe("\uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
2684
- count: z.number().optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 200)"),
2685
- cursor: z.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
3117
+ userId: z6.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
3118
+ folderId: z6.string().optional().describe("\uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
3119
+ count: z6.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 200)"),
3120
+ cursor: z6.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
2686
3121
  },
2687
3122
  async ({ userId, folderId, count, cursor }) => {
2688
3123
  try {
@@ -2711,16 +3146,17 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2711
3146
  }
2712
3147
  }
2713
3148
  );
2714
- server.tool(
3149
+ defineTool(
3150
+ server,
2715
3151
  "nworks_drive_upload",
2716
3152
  "\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.",
2717
3153
  {
2718
- content: z.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uD30C\uC77C \uB0B4\uC6A9 (base64 \uC778\uCF54\uB529). filePath \uB300\uC2E0 \uC0AC\uC6A9"),
2719
- fileName: z.string().optional().describe("\uD30C\uC77C\uBA85 (content \uC0AC\uC6A9 \uC2DC \uD544\uC218)"),
2720
- 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)"),
2721
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
2722
- folderId: z.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
2723
- overwrite: z.boolean().optional().describe("\uB3D9\uC77C \uD30C\uC77C\uBA85 \uB36E\uC5B4\uC4F0\uAE30 (\uAE30\uBCF8: false)")
3154
+ content: z6.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uD30C\uC77C \uB0B4\uC6A9 (base64 \uC778\uCF54\uB529). filePath \uB300\uC2E0 \uC0AC\uC6A9"),
3155
+ fileName: z6.string().optional().describe("\uD30C\uC77C\uBA85 (content \uC0AC\uC6A9 \uC2DC \uD544\uC218)"),
3156
+ 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)"),
3157
+ userId: z6.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
3158
+ folderId: z6.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
3159
+ overwrite: z6.boolean().optional().describe("\uB3D9\uC77C \uD30C\uC77C\uBA85 \uB36E\uC5B4\uC4F0\uAE30 (\uAE30\uBCF8: false)")
2724
3160
  },
2725
3161
  async ({ content, fileName, filePath, userId, folderId, overwrite }) => {
2726
3162
  try {
@@ -2768,14 +3204,15 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2768
3204
  }
2769
3205
  }
2770
3206
  );
2771
- server.tool(
3207
+ defineTool(
3208
+ server,
2772
3209
  "nworks_drive_download",
2773
3210
  "\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.",
2774
3211
  {
2775
- fileId: z.string().describe("\uB2E4\uC6B4\uB85C\uB4DC\uD560 \uD30C\uC77C ID (nworks_drive_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2776
- 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)"),
2777
- outputName: z.string().optional().describe("\uC800\uC7A5 \uD30C\uC77C\uBA85 (\uBBF8\uC9C0\uC815 \uC2DC \uC6D0\uBCF8 \uD30C\uC77C\uBA85)"),
2778
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
3212
+ fileId: z6.string().describe("\uB2E4\uC6B4\uB85C\uB4DC\uD560 \uD30C\uC77C ID (nworks_drive_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3213
+ 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)"),
3214
+ outputName: z6.string().optional().describe("\uC800\uC7A5 \uD30C\uC77C\uBA85 (\uBBF8\uC9C0\uC815 \uC2DC \uC6D0\uBCF8 \uD30C\uC77C\uBA85)"),
3215
+ userId: z6.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2779
3216
  },
2780
3217
  async ({ fileId, outputDir, outputName, userId }) => {
2781
3218
  try {
@@ -2785,13 +3222,13 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2785
3222
  );
2786
3223
  const fileName = outputName ?? result.fileName ?? fileId;
2787
3224
  if (outputDir) {
2788
- const { writeFile: writeFile3 } = await import("fs/promises");
2789
- const { join: join3 } = await import("path");
3225
+ const { writeFile: writeFile4 } = await import("fs/promises");
3226
+ const { join: join5 } = await import("path");
2790
3227
  const safeDir = validateLocalPath(outputDir);
2791
3228
  const safeName = sanitizeFileName(fileName);
2792
- const outPath = join3(safeDir, safeName);
3229
+ const outPath = join5(safeDir, safeName);
2793
3230
  validateLocalPath(outPath, safeDir);
2794
- await writeFile3(outPath, result.buffer);
3231
+ await writeFile4(outPath, result.buffer);
2795
3232
  return {
2796
3233
  content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
2797
3234
  };
@@ -2824,66 +3261,186 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2824
3261
  }
2825
3262
  }
2826
3263
  );
2827
- server.tool(
2828
- "nworks_mail_send",
2829
- "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)",
3264
+ defineTool(
3265
+ server,
3266
+ "nworks_sharedrive_list",
3267
+ "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)",
2830
3268
  {
2831
- to: z.string().describe("\uC218\uC2E0\uC790 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
2832
- subject: z.string().describe("\uBA54\uC77C \uC81C\uBAA9"),
2833
- body: z.string().optional().describe("\uBA54\uC77C \uBCF8\uBB38"),
2834
- cc: z.string().optional().describe("\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
2835
- bcc: z.string().optional().describe("\uC228\uC740\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
2836
- contentType: z.enum(["html", "text"]).optional().describe("\uBCF8\uBB38 \uD615\uC2DD (\uAE30\uBCF8: html)"),
2837
- userId: z.string().optional().describe("\uBC1C\uC2E0\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
3269
+ count: z6.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 200)"),
3270
+ cursor: z6.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
2838
3271
  },
2839
- async ({ to, subject, body, cc, bcc, contentType, userId }) => {
3272
+ async ({ count, cursor }) => {
2840
3273
  try {
2841
- await sendMail({
2842
- to,
2843
- subject,
2844
- body,
2845
- cc,
2846
- bcc,
2847
- contentType,
2848
- userId: userId ?? "me"
2849
- });
3274
+ const result = await listSharedDrives(count ?? 20, cursor);
2850
3275
  return {
2851
- content: [{ type: "text", text: JSON.stringify({ success: true, message: "\uBA54\uC77C\uC774 \uC804\uC1A1\uB418\uC5C8\uC2B5\uB2C8\uB2E4 (\uBE44\uB3D9\uAE30 \uCC98\uB9AC)" }) }]
3276
+ content: [{ type: "text", text: JSON.stringify({ sharedDrives: result.sharedDrives, count: result.sharedDrives.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
2852
3277
  };
2853
3278
  } catch (err) {
2854
3279
  return {
2855
- content: [{ type: "text", text: mcpErrorHint(err, "mail.send") }],
3280
+ content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_list") }],
2856
3281
  isError: true
2857
3282
  };
2858
3283
  }
2859
3284
  }
2860
3285
  );
2861
- server.tool(
2862
- "nworks_mail_list",
2863
- "\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)",
3286
+ defineTool(
3287
+ server,
3288
+ "nworks_sharedrive_files",
3289
+ "\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)",
2864
3290
  {
2865
- folderId: z.number().optional().describe("\uBA54\uC77C \uD3F4\uB354 ID (\uAE30\uBCF8: 0 = \uBC1B\uC740\uD3B8\uC9C0\uD568)"),
2866
- count: z.number().optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 30, \uCD5C\uB300: 200)"),
2867
- cursor: z.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
2868
- isUnread: z.boolean().optional().describe("\uC77D\uC9C0 \uC54A\uC740 \uBA54\uC77C\uB9CC \uC870\uD68C"),
2869
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
3291
+ sharedriveId: z6.string().describe("\uACF5\uC720 \uB4DC\uB77C\uC774\uBE0C ID (nworks_sharedrive_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3292
+ folderId: z6.string().optional().describe("\uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
3293
+ count: z6.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 200)"),
3294
+ cursor: z6.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
2870
3295
  },
2871
- async ({ folderId, count, cursor, isUnread, userId }) => {
3296
+ async ({ sharedriveId, folderId, count, cursor }) => {
2872
3297
  try {
2873
- const result = await listMails(
2874
- folderId ?? 0,
2875
- userId ?? "me",
2876
- count ?? 30,
2877
- cursor,
2878
- isUnread
3298
+ const result = await listSharedDriveFiles(
3299
+ sharedriveId,
3300
+ folderId,
3301
+ count ?? 20,
3302
+ cursor
2879
3303
  );
2880
- const mails = result.mails.map((m) => ({
2881
- mailId: m.mailId,
2882
- from: m.from.email,
2883
- subject: m.subject,
2884
- date: m.receivedTime,
2885
- status: m.status,
2886
- attachments: m.attachCount ?? 0
3304
+ const files = result.files.map((f) => ({
3305
+ fileId: f.fileId,
3306
+ name: f.fileName,
3307
+ type: f.fileType,
3308
+ size: f.fileSize,
3309
+ modified: f.modifiedTime,
3310
+ path: f.filePath
3311
+ }));
3312
+ return {
3313
+ content: [{ type: "text", text: JSON.stringify({ files, count: files.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
3314
+ };
3315
+ } catch (err) {
3316
+ return {
3317
+ content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_files") }],
3318
+ isError: true
3319
+ };
3320
+ }
3321
+ }
3322
+ );
3323
+ defineTool(
3324
+ server,
3325
+ "nworks_sharedrive_download",
3326
+ "\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.",
3327
+ {
3328
+ sharedriveId: z6.string().describe("\uACF5\uC720 \uB4DC\uB77C\uC774\uBE0C ID (nworks_sharedrive_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3329
+ fileId: z6.string().describe("\uB2E4\uC6B4\uB85C\uB4DC\uD560 \uD30C\uC77C ID (nworks_sharedrive_files\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3330
+ 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)"),
3331
+ outputName: z6.string().optional().describe("\uC800\uC7A5 \uD30C\uC77C\uBA85 (\uBBF8\uC9C0\uC815 \uC2DC \uC6D0\uBCF8 \uD30C\uC77C\uBA85)")
3332
+ },
3333
+ async ({ sharedriveId, fileId, outputDir, outputName }) => {
3334
+ try {
3335
+ const result = await downloadSharedDriveFile(sharedriveId, fileId);
3336
+ const fileName = outputName ?? result.fileName ?? fileId;
3337
+ if (outputDir) {
3338
+ const { writeFile: writeFile4 } = await import("fs/promises");
3339
+ const { join: join5 } = await import("path");
3340
+ const safeDir = validateLocalPath(outputDir);
3341
+ const safeName = sanitizeFileName(fileName);
3342
+ const outPath = join5(safeDir, safeName);
3343
+ validateLocalPath(outPath, safeDir);
3344
+ await writeFile4(outPath, result.buffer);
3345
+ return {
3346
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
3347
+ };
3348
+ }
3349
+ const MAX_INLINE_SIZE = 5 * 1024 * 1024;
3350
+ if (result.buffer.length > MAX_INLINE_SIZE) {
3351
+ const sizeMB = (result.buffer.length / (1024 * 1024)).toFixed(1);
3352
+ return {
3353
+ 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 }) }],
3354
+ isError: true
3355
+ };
3356
+ }
3357
+ 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;
3358
+ const isText = textExtensions.test(fileName);
3359
+ if (isText) {
3360
+ const text = result.buffer.toString("utf-8");
3361
+ return {
3362
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "text", content: text }) }]
3363
+ };
3364
+ }
3365
+ const base64 = result.buffer.toString("base64");
3366
+ return {
3367
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "base64", content: base64 }) }]
3368
+ };
3369
+ } catch (err) {
3370
+ return {
3371
+ content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_download") }],
3372
+ isError: true
3373
+ };
3374
+ }
3375
+ }
3376
+ );
3377
+ }
3378
+
3379
+ // src/mcp/tools/mail.ts
3380
+ import { z as z7 } from "zod";
3381
+ function registerMailTools(server) {
3382
+ defineTool(
3383
+ server,
3384
+ "nworks_mail_send",
3385
+ "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)",
3386
+ {
3387
+ to: z7.string().describe("\uC218\uC2E0\uC790 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
3388
+ subject: z7.string().describe("\uBA54\uC77C \uC81C\uBAA9"),
3389
+ body: z7.string().optional().describe("\uBA54\uC77C \uBCF8\uBB38"),
3390
+ cc: z7.string().optional().describe("\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
3391
+ bcc: z7.string().optional().describe("\uC228\uC740\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
3392
+ contentType: z7.enum(["html", "text"]).optional().describe("\uBCF8\uBB38 \uD615\uC2DD (\uAE30\uBCF8: html)"),
3393
+ userId: z7.string().optional().describe("\uBC1C\uC2E0\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
3394
+ },
3395
+ async ({ to, subject, body, cc, bcc, contentType, userId }) => {
3396
+ try {
3397
+ await sendMail({
3398
+ to,
3399
+ subject,
3400
+ body,
3401
+ cc,
3402
+ bcc,
3403
+ contentType,
3404
+ userId: userId ?? "me"
3405
+ });
3406
+ return {
3407
+ content: [{ type: "text", text: JSON.stringify({ success: true, message: "\uBA54\uC77C\uC774 \uC804\uC1A1\uB418\uC5C8\uC2B5\uB2C8\uB2E4 (\uBE44\uB3D9\uAE30 \uCC98\uB9AC)" }) }]
3408
+ };
3409
+ } catch (err) {
3410
+ return {
3411
+ content: [{ type: "text", text: mcpErrorHint(err, "mail.send") }],
3412
+ isError: true
3413
+ };
3414
+ }
3415
+ }
3416
+ );
3417
+ defineTool(
3418
+ server,
3419
+ "nworks_mail_list",
3420
+ "\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)",
3421
+ {
3422
+ folderId: z7.number().int().min(0).optional().describe("\uBA54\uC77C \uD3F4\uB354 ID (\uAE30\uBCF8: 0 = \uBC1B\uC740\uD3B8\uC9C0\uD568)"),
3423
+ count: z7.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 30, \uCD5C\uB300: 200)"),
3424
+ cursor: z7.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
3425
+ isUnread: z7.boolean().optional().describe("\uC77D\uC9C0 \uC54A\uC740 \uBA54\uC77C\uB9CC \uC870\uD68C"),
3426
+ userId: z7.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
3427
+ },
3428
+ async ({ folderId, count, cursor, isUnread, userId }) => {
3429
+ try {
3430
+ const result = await listMails(
3431
+ folderId ?? 0,
3432
+ userId ?? "me",
3433
+ count ?? 30,
3434
+ cursor,
3435
+ isUnread
3436
+ );
3437
+ const mails = result.mails.map((m) => ({
3438
+ mailId: m.mailId,
3439
+ from: m.from.email,
3440
+ subject: m.subject,
3441
+ date: m.receivedTime,
3442
+ status: m.status,
3443
+ attachments: m.attachCount ?? 0
2887
3444
  }));
2888
3445
  return {
2889
3446
  content: [{ type: "text", text: JSON.stringify({ mails, count: mails.length, totalCount: result.totalCount, unreadCount: result.unreadCount, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
@@ -2896,12 +3453,13 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2896
3453
  }
2897
3454
  }
2898
3455
  );
2899
- server.tool(
3456
+ defineTool(
3457
+ server,
2900
3458
  "nworks_mail_read",
2901
3459
  "\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)",
2902
3460
  {
2903
- mailId: z.number().describe("\uBA54\uC77C ID (nworks_mail_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
2904
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
3461
+ mailId: z7.number().int().min(0).describe("\uBA54\uC77C ID (nworks_mail_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3462
+ userId: z7.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2905
3463
  },
2906
3464
  async ({ mailId, userId }) => {
2907
3465
  try {
@@ -2935,15 +3493,80 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2935
3493
  }
2936
3494
  }
2937
3495
  );
2938
- server.tool(
3496
+ defineTool(
3497
+ server,
3498
+ "nworks_mail_download_attachment",
3499
+ "\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.",
3500
+ {
3501
+ mailId: z7.number().int().min(0).describe("\uBA54\uC77C ID (nworks_mail_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3502
+ attachmentId: z7.number().int().min(0).describe("\uCCA8\uBD80\uD30C\uC77C ID (nworks_mail_read\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3503
+ 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)"),
3504
+ outputName: z7.string().optional().describe("\uC800\uC7A5 \uD30C\uC77C\uBA85 (\uBBF8\uC9C0\uC815 \uC2DC \uC6D0\uBCF8 \uD30C\uC77C\uBA85)"),
3505
+ userId: z7.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
3506
+ },
3507
+ async ({ mailId, attachmentId, outputDir, outputName, userId }) => {
3508
+ try {
3509
+ const result = await downloadAttachment(
3510
+ mailId,
3511
+ attachmentId,
3512
+ userId ?? "me"
3513
+ );
3514
+ const fileName = outputName ?? result.filename;
3515
+ if (outputDir) {
3516
+ const { writeFile: writeFile4 } = await import("fs/promises");
3517
+ const { join: join5 } = await import("path");
3518
+ const safeDir = validateLocalPath(outputDir);
3519
+ const safeName = sanitizeFileName(fileName);
3520
+ const outPath = join5(safeDir, safeName);
3521
+ validateLocalPath(outPath, safeDir);
3522
+ await writeFile4(outPath, result.buffer);
3523
+ return {
3524
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
3525
+ };
3526
+ }
3527
+ const MAX_INLINE_SIZE = 5 * 1024 * 1024;
3528
+ if (result.buffer.length > MAX_INLINE_SIZE) {
3529
+ const sizeMB = (result.buffer.length / (1024 * 1024)).toFixed(1);
3530
+ return {
3531
+ 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 }) }],
3532
+ isError: true
3533
+ };
3534
+ }
3535
+ 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;
3536
+ const isText = textExtensions.test(fileName);
3537
+ if (isText) {
3538
+ const text = result.buffer.toString("utf-8");
3539
+ return {
3540
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "text", content: text }) }]
3541
+ };
3542
+ }
3543
+ const base64 = result.buffer.toString("base64");
3544
+ return {
3545
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "base64", content: base64 }) }]
3546
+ };
3547
+ } catch (err) {
3548
+ return {
3549
+ content: [{ type: "text", text: mcpErrorHint(err, "mail.download_attachment") }],
3550
+ isError: true
3551
+ };
3552
+ }
3553
+ }
3554
+ );
3555
+ }
3556
+
3557
+ // src/mcp/tools/task.ts
3558
+ import { z as z8 } from "zod";
3559
+ function registerTaskTools(server) {
3560
+ defineTool(
3561
+ server,
2939
3562
  "nworks_task_list",
2940
3563
  "\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)",
2941
3564
  {
2942
- categoryId: z.string().optional().describe("\uCE74\uD14C\uACE0\uB9AC ID (\uAE30\uBCF8: default)"),
2943
- status: z.enum(["TODO", "ALL"]).optional().describe("\uD544\uD130: TODO \uB610\uB294 ALL (\uAE30\uBCF8: ALL)"),
2944
- count: z.number().optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 50, \uCD5C\uB300: 100)"),
2945
- cursor: z.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
2946
- userId: z.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
3565
+ categoryId: z8.string().optional().describe("\uCE74\uD14C\uACE0\uB9AC ID (\uAE30\uBCF8: default)"),
3566
+ status: z8.enum(["TODO", "ALL"]).optional().describe("\uD544\uD130: TODO \uB610\uB294 ALL (\uAE30\uBCF8: ALL)"),
3567
+ count: z8.number().int().min(1).max(100).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 50, \uCD5C\uB300: 100)"),
3568
+ cursor: z8.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
3569
+ userId: z8.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2947
3570
  },
2948
3571
  async ({ categoryId, status, count, cursor, userId }) => {
2949
3572
  try {
@@ -2973,16 +3596,17 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2973
3596
  }
2974
3597
  }
2975
3598
  );
2976
- server.tool(
3599
+ defineTool(
3600
+ server,
2977
3601
  "nworks_task_create",
2978
3602
  "\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)",
2979
3603
  {
2980
- title: z.string().describe("\uD560 \uC77C \uC81C\uBAA9"),
2981
- content: z.string().optional().describe("\uD560 \uC77C \uB0B4\uC6A9"),
2982
- dueDate: z.string().optional().describe("\uB9C8\uAC10\uC77C (YYYY-MM-DD)"),
2983
- categoryId: z.string().optional().describe("\uCE74\uD14C\uACE0\uB9AC ID"),
2984
- assigneeIds: z.array(z.string()).optional().describe("\uB2F4\uB2F9\uC790 user ID \uBAA9\uB85D (\uBBF8\uC9C0\uC815 \uC2DC \uC790\uAE30 \uC790\uC2E0)"),
2985
- userId: z.string().optional().describe("\uC0DD\uC131\uC790 user ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
3604
+ title: z8.string().describe("\uD560 \uC77C \uC81C\uBAA9"),
3605
+ content: z8.string().optional().describe("\uD560 \uC77C \uB0B4\uC6A9"),
3606
+ dueDate: z8.string().optional().describe("\uB9C8\uAC10\uC77C (YYYY-MM-DD)"),
3607
+ categoryId: z8.string().optional().describe("\uCE74\uD14C\uACE0\uB9AC ID"),
3608
+ assigneeIds: z8.array(z8.string()).optional().describe("\uB2F4\uB2F9\uC790 user ID \uBAA9\uB85D (\uBBF8\uC9C0\uC815 \uC2DC \uC790\uAE30 \uC790\uC2E0)"),
3609
+ userId: z8.string().optional().describe("\uC0DD\uC131\uC790 user ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2986
3610
  },
2987
3611
  async ({ title, content, dueDate, categoryId, assigneeIds, userId }) => {
2988
3612
  try {
@@ -3005,15 +3629,16 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3005
3629
  }
3006
3630
  }
3007
3631
  );
3008
- server.tool(
3632
+ defineTool(
3633
+ server,
3009
3634
  "nworks_task_update",
3010
3635
  "\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)",
3011
3636
  {
3012
- taskId: z.string().describe("\uD560 \uC77C ID (nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3013
- status: z.enum(["done", "todo"]).optional().describe("\uC0C1\uD0DC \uBCC0\uACBD: done(\uC644\uB8CC) \uB610\uB294 todo(\uBBF8\uC644\uB8CC)"),
3014
- title: z.string().optional().describe("\uC0C8 \uC81C\uBAA9"),
3015
- content: z.string().optional().describe("\uC0C8 \uB0B4\uC6A9"),
3016
- dueDate: z.string().optional().describe("\uC0C8 \uB9C8\uAC10\uC77C (YYYY-MM-DD)")
3637
+ taskId: z8.string().describe("\uD560 \uC77C ID (nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3638
+ status: z8.enum(["done", "todo"]).optional().describe("\uC0C1\uD0DC \uBCC0\uACBD: done(\uC644\uB8CC) \uB610\uB294 todo(\uBBF8\uC644\uB8CC)"),
3639
+ title: z8.string().optional().describe("\uC0C8 \uC81C\uBAA9"),
3640
+ content: z8.string().optional().describe("\uC0C8 \uB0B4\uC6A9"),
3641
+ dueDate: z8.string().optional().describe("\uC0C8 \uB9C8\uAC10\uC77C (YYYY-MM-DD)")
3017
3642
  },
3018
3643
  async ({ taskId, status, title, content, dueDate }) => {
3019
3644
  try {
@@ -3047,11 +3672,12 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3047
3672
  }
3048
3673
  }
3049
3674
  );
3050
- server.tool(
3675
+ defineTool(
3676
+ server,
3051
3677
  "nworks_task_delete",
3052
3678
  "\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)",
3053
3679
  {
3054
- taskId: z.string().describe("\uC0AD\uC81C\uD560 \uD560 \uC77C ID (nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
3680
+ taskId: z8.string().describe("\uC0AD\uC81C\uD560 \uD560 \uC77C ID (nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
3055
3681
  },
3056
3682
  async ({ taskId }) => {
3057
3683
  try {
@@ -3067,12 +3693,18 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3067
3693
  }
3068
3694
  }
3069
3695
  );
3070
- server.tool(
3696
+ }
3697
+
3698
+ // src/mcp/tools/board.ts
3699
+ import { z as z9 } from "zod";
3700
+ function registerBoardTools(server) {
3701
+ defineTool(
3702
+ server,
3071
3703
  "nworks_board_list",
3072
3704
  "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)",
3073
3705
  {
3074
- count: z.number().optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20)"),
3075
- cursor: z.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
3706
+ count: z9.number().int().min(1).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20)"),
3707
+ cursor: z9.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
3076
3708
  },
3077
3709
  async ({ count, cursor }) => {
3078
3710
  try {
@@ -3093,13 +3725,14 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3093
3725
  }
3094
3726
  }
3095
3727
  );
3096
- server.tool(
3728
+ defineTool(
3729
+ server,
3097
3730
  "nworks_board_posts",
3098
3731
  "\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)",
3099
3732
  {
3100
- boardId: z.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3101
- count: z.number().optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 40)"),
3102
- cursor: z.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
3733
+ boardId: z9.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3734
+ count: z9.number().int().min(1).max(40).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 40)"),
3735
+ cursor: z9.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
3103
3736
  },
3104
3737
  async ({ boardId, count, cursor }) => {
3105
3738
  try {
@@ -3123,12 +3756,13 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3123
3756
  }
3124
3757
  }
3125
3758
  );
3126
- server.tool(
3759
+ defineTool(
3760
+ server,
3127
3761
  "nworks_board_read",
3128
3762
  "\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)",
3129
3763
  {
3130
- boardId: z.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3131
- postId: z.string().describe("\uAE00 ID (nworks_board_posts\uB85C \uC870\uD68C \uAC00\uB2A5)")
3764
+ boardId: z9.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3765
+ postId: z9.string().describe("\uAE00 ID (nworks_board_posts\uB85C \uC870\uD68C \uAC00\uB2A5)")
3132
3766
  },
3133
3767
  async ({ boardId, postId }) => {
3134
3768
  try {
@@ -3154,15 +3788,16 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3154
3788
  }
3155
3789
  }
3156
3790
  );
3157
- server.tool(
3791
+ defineTool(
3792
+ server,
3158
3793
  "nworks_board_create",
3159
3794
  "\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)",
3160
3795
  {
3161
- boardId: z.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3162
- title: z.string().describe("\uAE00 \uC81C\uBAA9"),
3163
- body: z.string().optional().describe("\uAE00 \uBCF8\uBB38"),
3164
- enableComment: z.boolean().optional().describe("\uB313\uAE00 \uD5C8\uC6A9 (\uAE30\uBCF8: true)"),
3165
- sendNotifications: z.boolean().optional().describe("\uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)")
3796
+ boardId: z9.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3797
+ title: z9.string().describe("\uAE00 \uC81C\uBAA9"),
3798
+ body: z9.string().optional().describe("\uAE00 \uBCF8\uBB38"),
3799
+ enableComment: z9.boolean().optional().describe("\uB313\uAE00 \uD5C8\uC6A9 (\uAE30\uBCF8: true)"),
3800
+ sendNotifications: z9.boolean().optional().describe("\uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)")
3166
3801
  },
3167
3802
  async ({ boardId, title, body, enableComment, sendNotifications }) => {
3168
3803
  try {
@@ -3184,33 +3819,173 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3184
3819
  }
3185
3820
  }
3186
3821
  );
3187
- server.tool(
3822
+ }
3823
+
3824
+ // src/mcp/tools/contact.ts
3825
+ import { z as z10 } from "zod";
3826
+ function registerContactTools(server) {
3827
+ defineTool(
3828
+ server,
3829
+ "nworks_contact_list",
3830
+ "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)",
3831
+ {
3832
+ userId: z10.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
3833
+ count: z10.number().int().min(1).max(500).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 500)"),
3834
+ cursor: z10.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
3835
+ contactTagId: z10.string().optional().describe("\uC5F0\uB77D\uCC98 \uD0DC\uADF8 ID\uB85C \uD544\uD130\uB9C1 (nworks_contact_list_tags\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3836
+ email: z10.string().optional().describe("\uC774\uBA54\uC77C\uB85C \uD544\uD130\uB9C1"),
3837
+ telephone: z10.string().optional().describe("\uC804\uD654\uBC88\uD638\uB85C \uD544\uD130\uB9C1")
3838
+ },
3839
+ async ({ userId, count, cursor, contactTagId, email, telephone }) => {
3840
+ try {
3841
+ const result = await listContacts({
3842
+ userId: userId ?? "me",
3843
+ count: count ?? 20,
3844
+ cursor,
3845
+ contactTagId,
3846
+ email,
3847
+ telephone
3848
+ });
3849
+ return {
3850
+ content: [{ type: "text", text: JSON.stringify({ contacts: result.contacts, count: result.contacts.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
3851
+ };
3852
+ } catch (err) {
3853
+ return {
3854
+ content: [{ type: "text", text: mcpErrorHint(err, "contact.list") }],
3855
+ isError: true
3856
+ };
3857
+ }
3858
+ }
3859
+ );
3860
+ defineTool(
3861
+ server,
3862
+ "nworks_contact_get",
3863
+ "\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)",
3864
+ {
3865
+ contactId: z10.string().describe("\uC5F0\uB77D\uCC98 ID (nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
3866
+ },
3867
+ async ({ contactId }) => {
3868
+ try {
3869
+ const result = await getContact(contactId);
3870
+ return {
3871
+ content: [{ type: "text", text: JSON.stringify(result) }]
3872
+ };
3873
+ } catch (err) {
3874
+ return {
3875
+ content: [{ type: "text", text: mcpErrorHint(err, "contact.get") }],
3876
+ isError: true
3877
+ };
3878
+ }
3879
+ }
3880
+ );
3881
+ defineTool(
3882
+ server,
3883
+ "nworks_contact_create",
3884
+ `\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)`,
3885
+ {
3886
+ 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")
3887
+ },
3888
+ async ({ payload }) => {
3889
+ try {
3890
+ const result = await createContact(payload);
3891
+ return {
3892
+ content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
3893
+ };
3894
+ } catch (err) {
3895
+ return {
3896
+ content: [{ type: "text", text: mcpErrorHint(err, "contact.create") }],
3897
+ isError: true
3898
+ };
3899
+ }
3900
+ }
3901
+ );
3902
+ defineTool(
3903
+ server,
3904
+ "nworks_contact_update",
3905
+ "\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)",
3906
+ {
3907
+ contactId: z10.string().describe("\uC5F0\uB77D\uCC98 ID (nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3908
+ payload: z10.record(z10.string(), z10.unknown()).describe("\uC218\uC815\uD560 \uD544\uB4DC (name, email, tel, company \uB4F1)"),
3909
+ replace: z10.boolean().optional().describe("\uC804\uCCB4 \uAD50\uCCB4(PUT) \uC5EC\uBD80 (\uAE30\uBCF8: false, \uBD80\uBD84 \uC218\uC815 PATCH)")
3910
+ },
3911
+ async ({ contactId, payload, replace }) => {
3912
+ try {
3913
+ const result = await updateContact(contactId, payload, !(replace ?? false));
3914
+ return {
3915
+ content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
3916
+ };
3917
+ } catch (err) {
3918
+ return {
3919
+ content: [{ type: "text", text: mcpErrorHint(err, "contact.update") }],
3920
+ isError: true
3921
+ };
3922
+ }
3923
+ }
3924
+ );
3925
+ defineTool(
3926
+ server,
3927
+ "nworks_contact_delete",
3928
+ "\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)",
3929
+ {
3930
+ contactId: z10.string().describe("\uC0AD\uC81C\uD560 \uC5F0\uB77D\uCC98 ID (nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
3931
+ },
3932
+ async ({ contactId }) => {
3933
+ try {
3934
+ await deleteContact(contactId);
3935
+ return {
3936
+ content: [{ type: "text", text: JSON.stringify({ success: true, contactId, message: "\uC5F0\uB77D\uCC98\uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4" }) }]
3937
+ };
3938
+ } catch (err) {
3939
+ return {
3940
+ content: [{ type: "text", text: mcpErrorHint(err, "contact.delete") }],
3941
+ isError: true
3942
+ };
3943
+ }
3944
+ }
3945
+ );
3946
+ defineTool(
3947
+ server,
3948
+ "nworks_contact_list_tags",
3949
+ "\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)",
3950
+ {
3951
+ userId: z10.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
3952
+ count: z10.number().int().min(1).max(500).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 500)"),
3953
+ cursor: z10.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
3954
+ },
3955
+ async ({ userId, count, cursor }) => {
3956
+ try {
3957
+ const result = await listContactTags({ userId: userId ?? "me", count: count ?? 20, cursor });
3958
+ return {
3959
+ content: [{ type: "text", text: JSON.stringify({ contactTags: result.contactTags, count: result.contactTags.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
3960
+ };
3961
+ } catch (err) {
3962
+ return {
3963
+ content: [{ type: "text", text: mcpErrorHint(err, "contact.list_tags") }],
3964
+ isError: true
3965
+ };
3966
+ }
3967
+ }
3968
+ );
3969
+ }
3970
+
3971
+ // src/mcp/tools/auth.ts
3972
+ import { z as z11 } from "zod";
3973
+ function registerAuthTools(server) {
3974
+ defineTool(
3975
+ server,
3188
3976
  "nworks_login_user",
3189
- "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.",
3977
+ "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).",
3190
3978
  {
3191
- 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")
3979
+ 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."),
3980
+ 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")
3192
3981
  },
3193
- async ({ scope }) => {
3194
- const DEFAULT_SCOPE = "calendar calendar.read file file.read mail mail.read task task.read user.read board board.read";
3982
+ async ({ preset, scope }) => {
3195
3983
  try {
3196
3984
  const creds = await loadCredentials();
3197
- const SCOPE_DEPS = {
3198
- calendar: ["calendar.read"],
3199
- task: ["user.read"],
3200
- "task.read": ["user.read"]
3201
- };
3202
- const expandScopes = (scopes) => {
3203
- const expanded = new Set(scopes);
3204
- for (const s of scopes) {
3205
- const deps = SCOPE_DEPS[s];
3206
- if (deps) deps.forEach((d) => expanded.add(d));
3207
- }
3208
- return [...expanded];
3209
- };
3210
3985
  const existingToken = await loadUserToken();
3211
3986
  const existingScopes = existingToken?.scope?.split(" ").filter(Boolean) ?? [];
3212
- const requestedScopes = expandScopes((scope ?? DEFAULT_SCOPE).split(" ").filter(Boolean));
3213
- const mergedScopes = [.../* @__PURE__ */ new Set([...existingScopes, ...requestedScopes])].join(" ");
3987
+ const requestedScopes = resolveScopes(preset ?? scope);
3988
+ const mergedScopes = mergeScopes(existingScopes, requestedScopes);
3214
3989
  const state = generateSecureState();
3215
3990
  const authorizeUrl = buildAuthorizeUrl(creds.clientId, mergedScopes, state);
3216
3991
  startOAuthCallbackServer(creds.clientId, creds.clientSecret, state).then(
@@ -3247,7 +4022,8 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3247
4022
  }
3248
4023
  }
3249
4024
  );
3250
- server.tool(
4025
+ defineTool(
4026
+ server,
3251
4027
  "nworks_whoami",
3252
4028
  "\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",
3253
4029
  {},
@@ -3258,10 +4034,10 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3258
4034
  const userToken = await loadUserToken();
3259
4035
  const isValid = token ? token.expiresAt > Date.now() / 1e3 : false;
3260
4036
  const userTokenValid = userToken ? userToken.expiresAt > Date.now() / 1e3 : false;
3261
- const mask = (s) => s.length <= 4 ? "****" : `****${s.slice(-4)}`;
4037
+ const mask2 = (s) => s.length <= 4 ? "****" : `****${s.slice(-4)}`;
3262
4038
  const info = {
3263
4039
  serviceAccount: creds.serviceAccount ?? null,
3264
- clientId: mask(creds.clientId),
4040
+ clientId: mask2(creds.clientId),
3265
4041
  botId: creds.botId ?? null,
3266
4042
  tokenValid: isValid,
3267
4043
  userOAuth: userToken ? { valid: userTokenValid, scope: userToken.scope } : null
@@ -3277,7 +4053,8 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3277
4053
  }
3278
4054
  }
3279
4055
  );
3280
- server.tool(
4056
+ defineTool(
4057
+ server,
3281
4058
  "nworks_logout",
3282
4059
  "\uC800\uC7A5\uB41C NAVER WORKS \uC778\uC99D \uC815\uBCF4\uC640 \uD1A0\uD070\uC744 \uBAA8\uB450 \uC0AD\uC81C\uD569\uB2C8\uB2E4",
3283
4060
  {},
@@ -3315,7 +4092,141 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3315
4092
  }
3316
4093
  }
3317
4094
  );
3318
- server.tool(
4095
+ }
4096
+
4097
+ // src/commands/doctor.ts
4098
+ import { Command as Command12 } from "commander";
4099
+ import { existsSync as existsSync3 } from "fs";
4100
+ import { readFile as readFile5 } from "fs/promises";
4101
+ import { resolve as resolve2, join as join4, sep as sep2 } from "path";
4102
+ function mask(value) {
4103
+ return value.length <= 4 ? "****" : `****${value.slice(-4)}`;
4104
+ }
4105
+ async function runChecks(profile) {
4106
+ const results = [];
4107
+ let creds;
4108
+ try {
4109
+ creds = await loadCredentials(profile);
4110
+ results.push({ check: "credentials", status: "OK", detail: `clientId: ${mask(creds.clientId)}` });
4111
+ } catch {
4112
+ 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)." });
4113
+ return results;
4114
+ }
4115
+ if (hasServiceAccountCreds(creds)) {
4116
+ results.push({ check: "serviceAccount", status: "OK", detail: creds.serviceAccount });
4117
+ } else {
4118
+ results.push({ check: "serviceAccount", status: "SKIP", detail: "Not set (required for bot messaging)" });
4119
+ }
4120
+ if (creds.privateKeyPath) {
4121
+ if (existsSync3(creds.privateKeyPath)) {
4122
+ try {
4123
+ await readFile5(creds.privateKeyPath, "utf-8");
4124
+ results.push({ check: "privateKey", status: "OK", detail: creds.privateKeyPath });
4125
+ } catch {
4126
+ results.push({ check: "privateKey", status: "FAIL", detail: `Cannot read: ${creds.privateKeyPath}` });
4127
+ }
4128
+ } else {
4129
+ results.push({ check: "privateKey", status: "FAIL", detail: `File not found: ${creds.privateKeyPath}` });
4130
+ }
4131
+ } else {
4132
+ results.push({ check: "privateKey", status: "SKIP", detail: "Not set" });
4133
+ }
4134
+ if (creds.privateKeyPath && existsSync3(creds.privateKeyPath)) {
4135
+ const resolvedKey = resolve2(creds.privateKeyPath);
4136
+ const cwd = resolve2(process.cwd());
4137
+ const insideCwd = resolvedKey === cwd || resolvedKey.startsWith(cwd + sep2);
4138
+ const looksLikeProject = existsSync3(join4(cwd, ".git")) || existsSync3(join4(cwd, "package.json"));
4139
+ if (insideCwd && looksLikeProject) {
4140
+ results.push({
4141
+ check: "keyLocation",
4142
+ status: "WARN",
4143
+ 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."
4144
+ });
4145
+ }
4146
+ }
4147
+ if (creds.botId) {
4148
+ results.push({ check: "botId", status: "OK", detail: creds.botId });
4149
+ } else {
4150
+ results.push({ check: "botId", status: "SKIP", detail: "Not set (required to send messages)" });
4151
+ }
4152
+ const token = await loadToken(profile);
4153
+ if (token) {
4154
+ const valid = token.expiresAt > Date.now() / 1e3;
4155
+ results.push({
4156
+ check: "serviceToken",
4157
+ status: valid ? "OK" : "EXPIRED",
4158
+ detail: valid ? `expires: ${new Date(token.expiresAt * 1e3).toISOString()}` : `expired: ${new Date(token.expiresAt * 1e3).toISOString()}`
4159
+ });
4160
+ } else {
4161
+ results.push({ check: "serviceToken", status: "SKIP", detail: "No token" });
4162
+ }
4163
+ const userToken = await loadUserToken(profile);
4164
+ if (userToken) {
4165
+ const valid = userToken.expiresAt > Date.now() / 1e3;
4166
+ results.push({
4167
+ check: "userOAuth",
4168
+ status: valid ? "OK" : "EXPIRED",
4169
+ detail: valid ? `scope: ${userToken.scope} | expires: ${new Date(userToken.expiresAt * 1e3).toISOString()}` : `expired | scope: ${userToken.scope}`
4170
+ });
4171
+ } else {
4172
+ results.push({ check: "userOAuth", status: "SKIP", detail: "No token. Run `nworks login --user`." });
4173
+ }
4174
+ if (hasServiceAccountCreds(creds) && token && token.expiresAt > Date.now() / 1e3) {
4175
+ try {
4176
+ const res = await fetch("https://www.worksapis.com/v1.0/users/me", {
4177
+ headers: { Authorization: `Bearer ${token.accessToken}` }
4178
+ });
4179
+ if (res.ok) {
4180
+ results.push({ check: "apiConnection", status: "OK", detail: "Connected to NAVER WORKS API" });
4181
+ } else {
4182
+ results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
4183
+ }
4184
+ } catch (e) {
4185
+ results.push({ check: "apiConnection", status: "FAIL", detail: `Connection failed: ${e.message}` });
4186
+ }
4187
+ } else if (userToken && userToken.expiresAt > Date.now() / 1e3) {
4188
+ try {
4189
+ const res = await fetch("https://www.worksapis.com/v1.0/users/me", {
4190
+ headers: { Authorization: `Bearer ${userToken.accessToken}` }
4191
+ });
4192
+ if (res.ok) {
4193
+ results.push({ check: "apiConnection", status: "OK", detail: "Connected to NAVER WORKS API" });
4194
+ } else {
4195
+ results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
4196
+ }
4197
+ } catch (e) {
4198
+ results.push({ check: "apiConnection", status: "FAIL", detail: `Connection failed: ${e.message}` });
4199
+ }
4200
+ } else {
4201
+ results.push({ check: "apiConnection", status: "SKIP", detail: "No valid token \u2014 skipping API test" });
4202
+ }
4203
+ return results;
4204
+ }
4205
+ var doctorCommand = new Command12("doctor").description("Check nworks configuration and connectivity").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
4206
+ try {
4207
+ const results = await runChecks(opts.profile);
4208
+ if (opts.json || !process.stdout.isTTY) {
4209
+ output(results, opts);
4210
+ } else {
4211
+ console.log("\n nworks doctor\n");
4212
+ for (const r of results) {
4213
+ const icon = r.status === "OK" ? "\u2705" : r.status === "SKIP" ? "\u2796" : r.status === "WARN" ? "\u26A0\uFE0F" : "\u274C";
4214
+ console.log(` ${icon} ${r.check.padEnd(16)} ${r.detail}`);
4215
+ }
4216
+ console.log();
4217
+ }
4218
+ if (results.some((r) => r.status === "FAIL")) {
4219
+ process.exitCode = 1;
4220
+ }
4221
+ } catch (err) {
4222
+ cliError(err, opts);
4223
+ }
4224
+ });
4225
+
4226
+ // src/mcp/tools/diagnostics.ts
4227
+ function registerDiagnosticsTools(server) {
4228
+ defineTool(
4229
+ server,
3319
4230
  "nworks_doctor",
3320
4231
  "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.",
3321
4232
  {},
@@ -3348,6 +4259,21 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3348
4259
  );
3349
4260
  }
3350
4261
 
4262
+ // src/mcp/tools.ts
4263
+ function registerTools(server) {
4264
+ registerSetupTools(server);
4265
+ registerMessageTools(server);
4266
+ registerDirectoryTools(server);
4267
+ registerCalendarTools(server);
4268
+ registerDriveTools(server);
4269
+ registerMailTools(server);
4270
+ registerTaskTools(server);
4271
+ registerBoardTools(server);
4272
+ registerContactTools(server);
4273
+ registerAuthTools(server);
4274
+ registerDiagnosticsTools(server);
4275
+ }
4276
+
3351
4277
  // src/mcp/server.ts
3352
4278
  async function startMcpServer() {
3353
4279
  try {
@@ -3368,7 +4294,7 @@ async function startMcpServer() {
3368
4294
  }
3369
4295
  const server = new McpServer({
3370
4296
  name: "nworks",
3371
- version: "1.0.0"
4297
+ version: VERSION
3372
4298
  });
3373
4299
  registerTools(server);
3374
4300
  const transport = new StdioServerTransport();
@@ -3376,22 +4302,31 @@ async function startMcpServer() {
3376
4302
  }
3377
4303
 
3378
4304
  // src/commands/mcp-cmd.ts
3379
- var mcpCommand = new Command12("mcp").description("Start MCP server (stdio transport)").option("--list-tools", "List available MCP tools").action(async (opts) => {
4305
+ function listRegisteredTools() {
4306
+ const server = new McpServer2({ name: "nworks", version: "0.0.0" });
4307
+ registerTools(server);
4308
+ const registered = server._registeredTools;
4309
+ return Object.entries(registered).map(([name, tool]) => ({
4310
+ name,
4311
+ summary: (tool.description ?? "").split("\n")[0]?.trim() ?? ""
4312
+ }));
4313
+ }
4314
+ var mcpCommand = new Command13("mcp").description("Start MCP server (stdio transport)").option("--list-tools", "List available MCP tools").action(async (opts) => {
3380
4315
  if (opts.listTools) {
3381
- console.log("nworks_message_send \u2014 Send message to user or channel");
3382
- console.log("nworks_message_members \u2014 List channel members");
3383
- console.log("nworks_directory_members \u2014 List organization members");
3384
- console.log("nworks_calendar_list \u2014 List calendar events (User OAuth)");
3385
- console.log("nworks_whoami \u2014 Show auth status");
4316
+ const tools = listRegisteredTools();
4317
+ const width = tools.length ? Math.max(...tools.map((t) => t.name.length)) : 0;
4318
+ for (const { name, summary } of tools) {
4319
+ console.log(`${name.padEnd(width)} ${summary}`);
4320
+ }
4321
+ console.log(`
4322
+ ${tools.length} tools`);
3386
4323
  return;
3387
4324
  }
3388
4325
  await startMcpServer();
3389
4326
  });
3390
4327
 
3391
4328
  // src/index.ts
3392
- var require2 = createRequire(import.meta.url);
3393
- var { version } = require2("../package.json");
3394
- var program = new Command13().name("nworks").description("NAVER WORKS CLI \u2014 built for humans and AI agents").version(version).option("--json", "Always output JSON").option("-v, --verbose", "Debug logging").option("--dry-run", "Print request without calling API").option("-p, --profile <name>", "Profile name", "default");
4329
+ var program = new Command14().name("nworks").description("NAVER WORKS CLI \u2014 built for humans and AI agents").version(VERSION);
3395
4330
  program.addCommand(loginCommand);
3396
4331
  program.addCommand(logoutCommand);
3397
4332
  program.addCommand(whoamiCommand);
@@ -3402,6 +4337,7 @@ program.addCommand(driveCommand);
3402
4337
  program.addCommand(mailCommand);
3403
4338
  program.addCommand(taskCommand);
3404
4339
  program.addCommand(boardCommand);
4340
+ program.addCommand(contactCommand);
3405
4341
  program.addCommand(mcpCommand);
3406
4342
  program.addCommand(doctorCommand);
3407
4343
  program.parse();