nworks 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,31 @@ 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]);
535
647
  } else {
536
- exec(`${openCmd} "${authorizeUrl}"`);
648
+ execFile("xdg-open", [authorizeUrl]);
537
649
  }
538
- const tokenData = await startUserOAuthFlow(scope, profile, state);
650
+ const tokenData = await startUserOAuthFlow(mergedScope, profile, state);
539
651
  await saveUserToken(tokenData, profile);
540
652
  output(
541
653
  {
@@ -562,7 +674,7 @@ async function handleServiceAccountLogin(opts) {
562
674
  if (!botId) botId = process.env["NWORKS_BOT_ID"];
563
675
  if (process.stdin.isTTY) {
564
676
  if (!clientId) clientId = await prompt("Client ID: ");
565
- if (!clientSecret) clientSecret = await prompt("Client Secret: ");
677
+ if (!clientSecret) clientSecret = await promptSecret("Client Secret: ");
566
678
  if (!serviceAccount)
567
679
  serviceAccount = await prompt("Service Account: ");
568
680
  if (!privateKeyPath)
@@ -624,10 +736,10 @@ var REQUIRED_SCOPES = {
624
736
  "board.create": "board"
625
737
  };
626
738
  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."
739
+ FORBIDDEN: "Insufficient permission. Check the OAuth scopes in the Developer Console.",
740
+ ACCESS_DENIED: "Access denied. Make sure the Bot is added in the Admin console.",
741
+ SERVICE_ACCOUNT_NOT_ALLOWED: "This API is not available with a Service Account. Log in with User OAuth via `nworks login --user`.",
742
+ UNAUTHORIZED: "Authentication expired. Log in again with `nworks login`."
631
743
  };
632
744
  var ERROR_HINTS_MCP = {
633
745
  FORBIDDEN: "\uAD8C\uD55C\uC774 \uBD80\uC871\uD569\uB2C8\uB2E4. Developer Console\uC5D0\uC11C OAuth Scope\uB97C \uD655\uC778\uD558\uC138\uC694.",
@@ -675,7 +787,7 @@ function buildScopeHint(area, mode) {
675
787
  const scopes = scope.split(" ").join(", ");
676
788
  if (mode === "cli") {
677
789
  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.`;
790
+ \u2192 This command requires the ${scopes} scope(s). Run \`nworks login --user --scope "${scope}"\`.`;
679
791
  }
680
792
  return `
681
793
  \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 +880,7 @@ import { Command as Command4 } from "commander";
768
880
  var BASE_URL = "https://www.worksapis.com/v1.0";
769
881
  var MAX_RETRIES = 3;
770
882
  function sleep(ms) {
771
- return new Promise((resolve2) => setTimeout(resolve2, ms));
883
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
772
884
  }
773
885
  async function request(opts, _retryCount = 0) {
774
886
  const { method, path, body, profile = "default" } = opts;
@@ -818,8 +930,8 @@ async function request(opts, _retryCount = 0) {
818
930
  }
819
931
 
820
932
  // src/utils/sanitize.ts
821
- import { basename, resolve, sep } from "path";
822
- import { randomBytes as randomBytes3 } from "crypto";
933
+ import { resolve, sep } from "path";
934
+ import { randomBytes as randomBytes2 } from "crypto";
823
935
  function sanitizePathSegment(value) {
824
936
  if (!value || typeof value !== "string") {
825
937
  throw new Error("Path segment must be a non-empty string");
@@ -834,8 +946,9 @@ function sanitizeFileName(name) {
834
946
  if (!name || typeof name !== "string") {
835
947
  throw new Error("File name must be a non-empty string");
836
948
  }
837
- const base = basename(name);
838
- return base.replace(/[\r\n"\\]/g, "_");
949
+ const segments = name.split(/[/\\]/).filter((s) => s.length > 0);
950
+ const base = segments[segments.length - 1] ?? "";
951
+ return base.replace(/[\r\n"]/g, "_");
839
952
  }
840
953
  function validateLocalPath(filePath, allowedBase) {
841
954
  const resolved = resolve(filePath);
@@ -866,7 +979,7 @@ function validateRedirectUrl(location, allowedHosts) {
866
979
  return location;
867
980
  }
868
981
  function generateSecureState() {
869
- return randomBytes3(32).toString("hex");
982
+ return randomBytes2(32).toString("hex");
870
983
  }
871
984
 
872
985
  // src/api/message.ts
@@ -988,20 +1101,25 @@ var messageCommand = new Command4("message").description("Bot message operations
988
1101
  import { Command as Command5 } from "commander";
989
1102
 
990
1103
  // src/api/directory.ts
991
- async function listUsers(profile = "default") {
1104
+ async function listUsers(count = 100, cursor, profile = "default") {
1105
+ const params = new URLSearchParams();
1106
+ params.set("count", String(count));
1107
+ if (cursor) params.set("cursor", cursor);
992
1108
  const result = await request({
993
1109
  method: "GET",
994
- path: "/users",
1110
+ path: `/users?${params.toString()}`,
995
1111
  profile
996
1112
  });
997
1113
  return { users: result.users ?? [], responseMetaData: result.responseMetaData };
998
1114
  }
999
1115
 
1000
1116
  // 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) => {
1117
+ 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
1118
  try {
1003
- const result = await listUsers(opts.profile);
1119
+ const count = opts.count ? Number(opts.count) : 100;
1120
+ const result = await listUsers(count, opts.cursor, opts.profile);
1004
1121
  const formatted = {
1122
+ nextCursor: result.responseMetaData?.nextCursor ?? null,
1005
1123
  users: result.users.map((u) => ({
1006
1124
  userId: u.userId,
1007
1125
  userName: [u.userName?.lastName, u.userName?.firstName].filter(Boolean).join(" ") || "",
@@ -1024,6 +1142,7 @@ import { Command as Command6 } from "commander";
1024
1142
  import { randomUUID } from "crypto";
1025
1143
 
1026
1144
  // src/auth/token-user.ts
1145
+ var inFlightRefresh = /* @__PURE__ */ new Map();
1027
1146
  async function getValidUserToken(profile = "default") {
1028
1147
  const cached = await loadUserToken(profile);
1029
1148
  if (!cached) {
@@ -1034,22 +1153,106 @@ async function getValidUserToken(profile = "default") {
1034
1153
  if (cached.expiresAt > Date.now() / 1e3 + 300) {
1035
1154
  return cached.accessToken;
1036
1155
  }
1156
+ return refreshValidUserToken(profile);
1157
+ }
1158
+ function refreshValidUserToken(profile = "default") {
1159
+ const existing = inFlightRefresh.get(profile);
1160
+ if (existing) return existing;
1161
+ const run = doRefresh(profile);
1162
+ inFlightRefresh.set(profile, run);
1163
+ void run.finally(() => inFlightRefresh.delete(profile)).catch(() => {
1164
+ });
1165
+ return run;
1166
+ }
1167
+ async function doRefresh(profile) {
1168
+ const cached = await loadUserToken(profile);
1169
+ if (!cached) {
1170
+ throw new AuthError(
1171
+ "User OAuth token not found. Run `nworks login --user` first."
1172
+ );
1173
+ }
1037
1174
  const refreshed = await refreshUserToken(cached.refreshToken, profile);
1038
1175
  await saveUserToken(refreshed, profile);
1039
1176
  return refreshed.accessToken;
1040
1177
  }
1041
1178
 
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 });
1179
+ // src/api/user-client.ts
1180
+ var MAX_RETRIES2 = 3;
1181
+ var MAX_RETRY_AFTER_SECONDS = 60;
1182
+ var MAX_CONCURRENT_PER_HOST = 5;
1183
+ function sleep2(ms) {
1184
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
1185
+ }
1186
+ var HostSemaphore = class {
1187
+ constructor(max) {
1188
+ this.max = max;
1189
+ }
1190
+ active = 0;
1191
+ waiters = [];
1192
+ async acquire() {
1193
+ if (this.active < this.max) {
1194
+ this.active++;
1195
+ return;
1196
+ }
1197
+ await new Promise((resolve3) => this.waiters.push(resolve3));
1198
+ }
1199
+ release() {
1200
+ const next = this.waiters.shift();
1201
+ if (next) {
1202
+ next();
1203
+ } else {
1204
+ this.active--;
1205
+ }
1206
+ }
1207
+ };
1208
+ var semaphores = /* @__PURE__ */ new Map();
1209
+ function semaphoreFor(host) {
1210
+ let semaphore = semaphores.get(host);
1211
+ if (!semaphore) {
1212
+ semaphore = new HostSemaphore(MAX_CONCURRENT_PER_HOST);
1213
+ semaphores.set(host, semaphore);
1214
+ }
1215
+ return semaphore;
1216
+ }
1217
+ async function userFetch(url, init, profile) {
1218
+ let refreshedOn401 = false;
1219
+ for (let attempt = 0; ; attempt++) {
1220
+ const res = await fetchOnce(url, init, profile);
1221
+ if (res.status === 401 && !refreshedOn401) {
1222
+ refreshedOn401 = true;
1223
+ await refreshValidUserToken(profile);
1224
+ continue;
1225
+ }
1226
+ if (res.status === 429 && attempt < MAX_RETRIES2) {
1227
+ await sleep2(retryAfterMs(res));
1228
+ continue;
1229
+ }
1230
+ return res;
1231
+ }
1232
+ }
1233
+ async function fetchOnce(url, init, profile) {
1234
+ const host = new URL(url).host;
1235
+ const semaphore = semaphoreFor(host);
1236
+ await semaphore.acquire();
1237
+ try {
1238
+ const token = await getValidUserToken(profile);
1239
+ const headers = new Headers(init.headers);
1240
+ headers.set("Authorization", `Bearer ${token}`);
1241
+ return await fetch(url, { ...init, headers });
1242
+ } finally {
1243
+ semaphore.release();
1244
+ }
1245
+ }
1246
+ function retryAfterMs(res) {
1247
+ const raw = parseInt(res.headers.get("Retry-After") ?? "5", 10);
1248
+ const seconds = Math.min(Number.isNaN(raw) ? 5 : raw, MAX_RETRY_AFTER_SECONDS);
1249
+ return seconds * 1e3;
1049
1250
  }
1050
- async function handleError(res) {
1251
+ async function handleUserApiError(res) {
1051
1252
  if (res.status === 401) {
1052
- throw new AuthError("User token expired. Run `nworks login --user --scope calendar` again.");
1253
+ throw new AuthError(
1254
+ "User token expired or revoked. Run `nworks login --user` again."
1255
+ );
1053
1256
  }
1054
1257
  let code = "UNKNOWN";
1055
1258
  let description = `HTTP ${res.status}`;
@@ -1061,6 +1264,9 @@ async function handleError(res) {
1061
1264
  }
1062
1265
  throw new ApiError(code, description, res.status);
1063
1266
  }
1267
+
1268
+ // src/api/calendar.ts
1269
+ var BASE_URL2 = "https://www.worksapis.com/v1.0";
1064
1270
  function generateEventId() {
1065
1271
  return `event-${randomUUID()}`;
1066
1272
  }
@@ -1078,8 +1284,8 @@ async function listEvents(fromDateTime, untilDateTime, userId = "me", profile =
1078
1284
  if (process.env["NWORKS_VERBOSE"] === "1") {
1079
1285
  console.error(`[nworks] GET ${url}`);
1080
1286
  }
1081
- const res = await authedFetch(url, { method: "GET" }, profile);
1082
- if (!res.ok) return handleError(res);
1287
+ const res = await userFetch(url, { method: "GET" }, profile);
1288
+ if (!res.ok) return handleUserApiError(res);
1083
1289
  const data = await res.json();
1084
1290
  return { events: data.events ?? [] };
1085
1291
  }
@@ -1116,7 +1322,7 @@ async function createEvent(opts) {
1116
1322
  console.error(`[nworks] POST ${url}`);
1117
1323
  console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
1118
1324
  }
1119
- const res = await authedFetch(
1325
+ const res = await userFetch(
1120
1326
  url,
1121
1327
  {
1122
1328
  method: "POST",
@@ -1128,7 +1334,7 @@ async function createEvent(opts) {
1128
1334
  if (res.status === 201) {
1129
1335
  return await res.json();
1130
1336
  }
1131
- if (!res.ok) return handleError(res);
1337
+ if (!res.ok) return handleUserApiError(res);
1132
1338
  return await res.json();
1133
1339
  }
1134
1340
  async function getEvent(eventId, userId = "me", profile = "default") {
@@ -1136,8 +1342,8 @@ async function getEvent(eventId, userId = "me", profile = "default") {
1136
1342
  if (process.env["NWORKS_VERBOSE"] === "1") {
1137
1343
  console.error(`[nworks] GET ${url}`);
1138
1344
  }
1139
- const res = await authedFetch(url, { method: "GET" }, profile);
1140
- if (!res.ok) return handleError(res);
1345
+ const res = await userFetch(url, { method: "GET" }, profile);
1346
+ if (!res.ok) return handleUserApiError(res);
1141
1347
  const data = await res.json();
1142
1348
  const event = data.eventComponents[0];
1143
1349
  if (!event) throw new ApiError("NOT_FOUND", "Event not found", 404);
@@ -1180,7 +1386,7 @@ async function updateEvent(opts) {
1180
1386
  console.error(`[nworks] PUT ${url}`);
1181
1387
  console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
1182
1388
  }
1183
- const res = await authedFetch(
1389
+ const res = await userFetch(
1184
1390
  url,
1185
1391
  {
1186
1392
  method: "PUT",
@@ -1189,7 +1395,7 @@ async function updateEvent(opts) {
1189
1395
  },
1190
1396
  profile
1191
1397
  );
1192
- if (!res.ok) return handleError(res);
1398
+ if (!res.ok) return handleUserApiError(res);
1193
1399
  }
1194
1400
  async function deleteEvent(eventId, userId = "me", sendNotification = false, profile = "default") {
1195
1401
  const params = new URLSearchParams();
@@ -1198,9 +1404,9 @@ async function deleteEvent(eventId, userId = "me", sendNotification = false, pro
1198
1404
  if (process.env["NWORKS_VERBOSE"] === "1") {
1199
1405
  console.error(`[nworks] DELETE ${url}`);
1200
1406
  }
1201
- const res = await authedFetch(url, { method: "DELETE" }, profile);
1407
+ const res = await userFetch(url, { method: "DELETE" }, profile);
1202
1408
  if (res.status === 204) return;
1203
- if (!res.ok) return handleError(res);
1409
+ if (!res.ok) return handleUserApiError(res);
1204
1410
  }
1205
1411
 
1206
1412
  // src/commands/calendar.ts
@@ -1321,34 +1527,15 @@ import { Command as Command7 } from "commander";
1321
1527
 
1322
1528
  // src/api/drive.ts
1323
1529
  import { readFile as readFile4, stat as stat2 } from "fs/promises";
1324
- import { basename as basename2 } from "path";
1530
+ import { basename } from "path";
1325
1531
  var BASE_URL3 = "https://www.worksapis.com/v1.0";
1326
1532
  var MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
1327
1533
  var ALLOWED_HOSTS = [
1328
1534
  "storage.worksmobile.com",
1535
+ "apis-storage.worksmobile.com",
1329
1536
  "www.worksapis.com",
1330
1537
  "worksapis.com"
1331
1538
  ];
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
1539
  async function listFiles(userId = "me", folderId, count = 20, cursor, profile = "default") {
1353
1540
  const base = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files`;
1354
1541
  const path = folderId ? `${base}/${sanitizePathSegment(folderId)}/children` : base;
@@ -1359,13 +1546,13 @@ async function listFiles(userId = "me", folderId, count = 20, cursor, profile =
1359
1546
  if (process.env["NWORKS_VERBOSE"] === "1") {
1360
1547
  console.error(`[nworks] GET ${url}`);
1361
1548
  }
1362
- const res = await authedFetch2(url, { method: "GET" }, profile);
1363
- if (!res.ok) return handleError2(res);
1549
+ const res = await userFetch(url, { method: "GET" }, profile);
1550
+ if (!res.ok) return handleUserApiError(res);
1364
1551
  const data = await res.json();
1365
1552
  return { files: data.files ?? [], responseMetaData: data.responseMetaData };
1366
1553
  }
1367
1554
  async function uploadFile(localPath, userId = "me", folderId, overwrite = false, profile = "default") {
1368
- const fileName = basename2(localPath);
1555
+ const fileName = basename(localPath);
1369
1556
  const safeName = sanitizeFileName(fileName);
1370
1557
  const fileStat = await stat2(localPath);
1371
1558
  const fileSize = fileStat.size;
@@ -1377,7 +1564,7 @@ async function uploadFile(localPath, userId = "me", folderId, overwrite = false,
1377
1564
  if (process.env["NWORKS_VERBOSE"] === "1") {
1378
1565
  console.error(`[nworks] POST ${createUrl} (create upload URL)`);
1379
1566
  }
1380
- const createRes = await authedFetch2(
1567
+ const createRes = await userFetch(
1381
1568
  createUrl,
1382
1569
  {
1383
1570
  method: "POST",
@@ -1386,7 +1573,7 @@ async function uploadFile(localPath, userId = "me", folderId, overwrite = false,
1386
1573
  },
1387
1574
  profile
1388
1575
  );
1389
- if (!createRes.ok) return handleError2(createRes);
1576
+ if (!createRes.ok) return handleUserApiError(createRes);
1390
1577
  const { uploadUrl } = await createRes.json();
1391
1578
  validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
1392
1579
  const fileBuffer = await readFile4(localPath);
@@ -1405,7 +1592,7 @@ Content-Type: application/octet-stream\r
1405
1592
  if (process.env["NWORKS_VERBOSE"] === "1") {
1406
1593
  console.error(`[nworks] POST ${uploadUrl} (upload content, ${fileSize} bytes)`);
1407
1594
  }
1408
- const uploadRes = await authedFetch2(
1595
+ const uploadRes = await userFetch(
1409
1596
  uploadUrl,
1410
1597
  {
1411
1598
  method: "POST",
@@ -1414,7 +1601,7 @@ Content-Type: application/octet-stream\r
1414
1601
  },
1415
1602
  profile
1416
1603
  );
1417
- if (!uploadRes.ok) return handleError2(uploadRes);
1604
+ if (!uploadRes.ok) return handleUserApiError(uploadRes);
1418
1605
  return await uploadRes.json();
1419
1606
  }
1420
1607
  async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overwrite = false, profile = "default") {
@@ -1427,7 +1614,7 @@ async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overw
1427
1614
  if (process.env["NWORKS_VERBOSE"] === "1") {
1428
1615
  console.error(`[nworks] POST ${createUrl} (create upload URL for buffer)`);
1429
1616
  }
1430
- const createRes = await authedFetch2(
1617
+ const createRes = await userFetch(
1431
1618
  createUrl,
1432
1619
  {
1433
1620
  method: "POST",
@@ -1436,7 +1623,7 @@ async function uploadBuffer(fileBuffer, fileName, userId = "me", folderId, overw
1436
1623
  },
1437
1624
  profile
1438
1625
  );
1439
- if (!createRes.ok) return handleError2(createRes);
1626
+ if (!createRes.ok) return handleUserApiError(createRes);
1440
1627
  const { uploadUrl } = await createRes.json();
1441
1628
  validateRedirectUrl(uploadUrl, ALLOWED_HOSTS);
1442
1629
  const boundary = `----nworks${Date.now()}`;
@@ -1454,7 +1641,7 @@ Content-Type: application/octet-stream\r
1454
1641
  if (process.env["NWORKS_VERBOSE"] === "1") {
1455
1642
  console.error(`[nworks] POST ${uploadUrl} (upload buffer, ${fileSize} bytes)`);
1456
1643
  }
1457
- const uploadRes = await authedFetch2(
1644
+ const uploadRes = await userFetch(
1458
1645
  uploadUrl,
1459
1646
  {
1460
1647
  method: "POST",
@@ -1463,33 +1650,29 @@ Content-Type: application/octet-stream\r
1463
1650
  },
1464
1651
  profile
1465
1652
  );
1466
- if (!uploadRes.ok) return handleError2(uploadRes);
1653
+ if (!uploadRes.ok) return handleUserApiError(uploadRes);
1467
1654
  return await uploadRes.json();
1468
1655
  }
1469
- async function downloadFile(fileId, userId = "me", profile = "default") {
1470
- const url = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files/${sanitizePathSegment(fileId)}/download`;
1656
+ async function downloadViaRedirect(url, profile) {
1471
1657
  if (process.env["NWORKS_VERBOSE"] === "1") {
1472
1658
  console.error(`[nworks] GET ${url} (get download URL)`);
1473
1659
  }
1474
- const redirectRes = await authedFetch2(
1660
+ const redirectRes = await userFetch(
1475
1661
  url,
1476
1662
  { method: "GET", redirect: "manual" },
1477
1663
  profile
1478
1664
  );
1479
- if (redirectRes.status === 401) {
1480
- throw new AuthError("User token expired. Run `nworks login --user --scope file` again.");
1481
- }
1482
1665
  const location = redirectRes.headers.get("location");
1483
1666
  if (!location) {
1484
- if (!redirectRes.ok) return handleError2(redirectRes);
1667
+ if (!redirectRes.ok) return handleUserApiError(redirectRes);
1485
1668
  throw new ApiError("NO_REDIRECT", "No download URL returned", redirectRes.status);
1486
1669
  }
1487
1670
  const safeLocation = validateRedirectUrl(location, ALLOWED_HOSTS);
1488
1671
  if (process.env["NWORKS_VERBOSE"] === "1") {
1489
1672
  console.error(`[nworks] GET ${safeLocation} (download content)`);
1490
1673
  }
1491
- const downloadRes = await fetch(safeLocation, { method: "GET" });
1492
- if (!downloadRes.ok) return handleError2(downloadRes);
1674
+ const downloadRes = await userFetch(safeLocation, { method: "GET" }, profile);
1675
+ if (!downloadRes.ok) return handleUserApiError(downloadRes);
1493
1676
  const arrayBuffer = await downloadRes.arrayBuffer();
1494
1677
  const buffer = Buffer.from(arrayBuffer);
1495
1678
  const disposition = downloadRes.headers.get("content-disposition");
@@ -1502,6 +1685,42 @@ async function downloadFile(fileId, userId = "me", profile = "default") {
1502
1685
  }
1503
1686
  return { buffer, fileName };
1504
1687
  }
1688
+ async function downloadFile(fileId, userId = "me", profile = "default") {
1689
+ const url = `${BASE_URL3}/users/${sanitizePathSegment(userId)}/drive/files/${sanitizePathSegment(fileId)}/download`;
1690
+ return downloadViaRedirect(url, profile);
1691
+ }
1692
+ async function listSharedDrives(count = 20, cursor, profile = "default") {
1693
+ const params = new URLSearchParams();
1694
+ params.set("count", String(count));
1695
+ if (cursor) params.set("cursor", cursor);
1696
+ const url = `${BASE_URL3}/sharedrives?${params.toString()}`;
1697
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1698
+ console.error(`[nworks] GET ${url}`);
1699
+ }
1700
+ const res = await userFetch(url, { method: "GET" }, profile);
1701
+ if (!res.ok) return handleUserApiError(res);
1702
+ const data = await res.json();
1703
+ return { sharedDrives: data.sharedrives ?? [], responseMetaData: data.responseMetaData };
1704
+ }
1705
+ async function listSharedDriveFiles(sharedriveId, folderId, count = 20, cursor, profile = "default") {
1706
+ const base = `${BASE_URL3}/sharedrives/${sanitizePathSegment(sharedriveId)}/files`;
1707
+ const path = folderId ? `${base}/${sanitizePathSegment(folderId)}/children` : base;
1708
+ const params = new URLSearchParams();
1709
+ params.set("count", String(count));
1710
+ if (cursor) params.set("cursor", cursor);
1711
+ const url = `${path}?${params.toString()}`;
1712
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1713
+ console.error(`[nworks] GET ${url}`);
1714
+ }
1715
+ const res = await userFetch(url, { method: "GET" }, profile);
1716
+ if (!res.ok) return handleUserApiError(res);
1717
+ const data = await res.json();
1718
+ return { files: data.files ?? [], responseMetaData: data.responseMetaData };
1719
+ }
1720
+ async function downloadSharedDriveFile(sharedriveId, fileId, profile = "default") {
1721
+ const url = `${BASE_URL3}/sharedrives/${sanitizePathSegment(sharedriveId)}/files/${sanitizePathSegment(fileId)}/download`;
1722
+ return downloadViaRedirect(url, profile);
1723
+ }
1505
1724
 
1506
1725
  // src/commands/drive.ts
1507
1726
  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 +1814,86 @@ var downloadCommand = new Command7("download").description("Download a file from
1595
1814
  cliError(err, opts, "drive");
1596
1815
  }
1597
1816
  });
1598
- var driveCommand = new Command7("drive").description("Drive operations (requires User OAuth with file scope)").addCommand(listCommand2).addCommand(uploadCommand).addCommand(downloadCommand);
1817
+ 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) => {
1818
+ try {
1819
+ const result = await listSharedDrives(
1820
+ parseInt(opts.count, 10),
1821
+ opts.cursor,
1822
+ opts.profile
1823
+ );
1824
+ output(
1825
+ {
1826
+ sharedDrives: result.sharedDrives,
1827
+ count: result.sharedDrives.length,
1828
+ nextCursor: result.responseMetaData?.nextCursor ?? null
1829
+ },
1830
+ opts
1831
+ );
1832
+ } catch (err) {
1833
+ cliError(err, opts, "drive");
1834
+ }
1835
+ });
1836
+ 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) => {
1837
+ try {
1838
+ const result = await listSharedDriveFiles(
1839
+ opts.sharedrive,
1840
+ opts.folder,
1841
+ parseInt(opts.count, 10),
1842
+ opts.cursor,
1843
+ opts.profile
1844
+ );
1845
+ const files = result.files.map((f) => ({
1846
+ name: f.fileName,
1847
+ type: f.fileType,
1848
+ size: f.fileSize,
1849
+ modified: f.modifiedTime,
1850
+ fileId: f.fileId
1851
+ }));
1852
+ output(
1853
+ {
1854
+ files,
1855
+ count: files.length,
1856
+ nextCursor: result.responseMetaData?.nextCursor ?? null
1857
+ },
1858
+ opts
1859
+ );
1860
+ } catch (err) {
1861
+ cliError(err, opts, "drive");
1862
+ }
1863
+ });
1864
+ 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) => {
1865
+ try {
1866
+ const result = await downloadSharedDriveFile(
1867
+ opts.sharedrive,
1868
+ opts.fileId,
1869
+ opts.profile
1870
+ );
1871
+ const fileName = opts.name ?? result.fileName ?? opts.fileId;
1872
+ const outDir = opts.out ?? process.cwd();
1873
+ const outPath = join2(outDir, fileName);
1874
+ await writeFile2(outPath, result.buffer);
1875
+ output(
1876
+ {
1877
+ success: true,
1878
+ fileName,
1879
+ path: outPath,
1880
+ size: result.buffer.length
1881
+ },
1882
+ opts
1883
+ );
1884
+ } catch (err) {
1885
+ cliError(err, opts, "drive");
1886
+ }
1887
+ });
1888
+ 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
1889
 
1600
1890
  // src/commands/mail.ts
1891
+ import { writeFile as writeFile3 } from "fs/promises";
1892
+ import { join as join3 } from "path";
1601
1893
  import { Command as Command8 } from "commander";
1602
1894
 
1603
1895
  // src/api/mail.ts
1604
1896
  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
1897
  async function sendMail(opts) {
1626
1898
  const userId = opts.userId ?? "me";
1627
1899
  const profile = opts.profile ?? "default";
@@ -1637,7 +1909,7 @@ async function sendMail(opts) {
1637
1909
  if (opts.cc) body.cc = opts.cc;
1638
1910
  if (opts.bcc) body.bcc = opts.bcc;
1639
1911
  if (opts.contentType) body.contentType = opts.contentType;
1640
- const res = await authedFetch3(
1912
+ const res = await userFetch(
1641
1913
  url,
1642
1914
  {
1643
1915
  method: "POST",
@@ -1647,7 +1919,7 @@ async function sendMail(opts) {
1647
1919
  profile
1648
1920
  );
1649
1921
  if (res.status === 202) return;
1650
- if (!res.ok) return handleError3(res);
1922
+ if (!res.ok) return handleUserApiError(res);
1651
1923
  }
1652
1924
  async function listMails(folderId = 0, userId = "me", count = 30, cursor, isUnread, profile = "default") {
1653
1925
  const params = new URLSearchParams();
@@ -1658,8 +1930,8 @@ async function listMails(folderId = 0, userId = "me", count = 30, cursor, isUnre
1658
1930
  if (process.env["NWORKS_VERBOSE"] === "1") {
1659
1931
  console.error(`[nworks] GET ${url}`);
1660
1932
  }
1661
- const res = await authedFetch3(url, { method: "GET" }, profile);
1662
- if (!res.ok) return handleError3(res);
1933
+ const res = await userFetch(url, { method: "GET" }, profile);
1934
+ if (!res.ok) return handleUserApiError(res);
1663
1935
  const data = await res.json();
1664
1936
  return {
1665
1937
  mails: data.mails ?? [],
@@ -1674,10 +1946,22 @@ async function readMail(mailId, userId = "me", profile = "default") {
1674
1946
  if (process.env["NWORKS_VERBOSE"] === "1") {
1675
1947
  console.error(`[nworks] GET ${url}`);
1676
1948
  }
1677
- const res = await authedFetch3(url, { method: "GET" }, profile);
1678
- if (!res.ok) return handleError3(res);
1949
+ const res = await userFetch(url, { method: "GET" }, profile);
1950
+ if (!res.ok) return handleUserApiError(res);
1679
1951
  return await res.json();
1680
1952
  }
1953
+ async function downloadAttachment(mailId, attachmentId, userId = "me", profile = "default") {
1954
+ const url = `${BASE_URL4}/users/${sanitizePathSegment(userId)}/mail/${sanitizePathSegment(
1955
+ String(mailId)
1956
+ )}/attachments/${sanitizePathSegment(String(attachmentId))}`;
1957
+ if (process.env["NWORKS_VERBOSE"] === "1") {
1958
+ console.error(`[nworks] GET ${url}`);
1959
+ }
1960
+ const res = await userFetch(url, { method: "GET" }, profile);
1961
+ if (!res.ok) return handleUserApiError(res);
1962
+ const data = await res.json();
1963
+ return { buffer: Buffer.from(data.data, "base64"), filename: data.filename };
1964
+ }
1681
1965
 
1682
1966
  // src/commands/mail.ts
1683
1967
  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 +2049,43 @@ var readCommand = new Command8("read").description("Read a specific mail (requir
1765
2049
  cliError(err, opts, "mail");
1766
2050
  }
1767
2051
  });
1768
- var mailCommand = new Command8("mail").description("Mail operations (requires User OAuth with mail scope)").addCommand(sendCommand2).addCommand(listCommand3).addCommand(readCommand);
2052
+ 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) => {
2053
+ try {
2054
+ const result = await downloadAttachment(
2055
+ parseInt(opts.id, 10),
2056
+ parseInt(opts.attachmentId, 10),
2057
+ opts.user ?? "me",
2058
+ opts.profile
2059
+ );
2060
+ const fileName = opts.name ?? result.filename;
2061
+ const outDir = opts.out ?? process.cwd();
2062
+ const outPath = join3(outDir, fileName);
2063
+ await writeFile3(outPath, result.buffer);
2064
+ output(
2065
+ {
2066
+ success: true,
2067
+ fileName,
2068
+ path: outPath,
2069
+ size: result.buffer.length
2070
+ },
2071
+ opts
2072
+ );
2073
+ } catch (err) {
2074
+ cliError(err, opts, "mail");
2075
+ }
2076
+ });
2077
+ var mailCommand = new Command8("mail").description("Mail operations (requires User OAuth with mail scope)").addCommand(sendCommand2).addCommand(listCommand3).addCommand(readCommand).addCommand(downloadAttachmentCommand);
1769
2078
 
1770
2079
  // src/commands/task.ts
1771
2080
  import { Command as Command9 } from "commander";
1772
2081
 
1773
2082
  // src/api/task.ts
1774
2083
  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
2084
  async function resolveUserId(userId, profile) {
1796
2085
  if (userId !== "me") return userId;
1797
2086
  const url = `${BASE_URL5}/users/${sanitizePathSegment(userId)}`;
1798
- const res = await authedFetch4(url, { method: "GET" }, profile);
1799
- if (!res.ok) return handleError4(res);
2087
+ const res = await userFetch(url, { method: "GET" }, profile);
2088
+ if (!res.ok) return handleUserApiError(res);
1800
2089
  const data = await res.json();
1801
2090
  if (process.env["NWORKS_VERBOSE"] === "1") {
1802
2091
  console.error(`[nworks] Resolved "me" \u2192 ${data.userId}`);
@@ -1813,8 +2102,8 @@ async function listTasks(categoryId = "default", userId = "me", count = 50, curs
1813
2102
  if (process.env["NWORKS_VERBOSE"] === "1") {
1814
2103
  console.error(`[nworks] GET ${url}`);
1815
2104
  }
1816
- const res = await authedFetch4(url, { method: "GET" }, profile);
1817
- if (!res.ok) return handleError4(res);
2105
+ const res = await userFetch(url, { method: "GET" }, profile);
2106
+ if (!res.ok) return handleUserApiError(res);
1818
2107
  const data = await res.json();
1819
2108
  return { tasks: data.tasks ?? [], responseMetaData: data.responseMetaData };
1820
2109
  }
@@ -1838,7 +2127,7 @@ async function createTask(opts) {
1838
2127
  console.error(`[nworks] POST ${url}`);
1839
2128
  console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
1840
2129
  }
1841
- const res = await authedFetch4(
2130
+ const res = await userFetch(
1842
2131
  url,
1843
2132
  {
1844
2133
  method: "POST",
@@ -1850,7 +2139,7 @@ async function createTask(opts) {
1850
2139
  if (res.status === 201) {
1851
2140
  return await res.json();
1852
2141
  }
1853
- if (!res.ok) return handleError4(res);
2142
+ if (!res.ok) return handleUserApiError(res);
1854
2143
  return await res.json();
1855
2144
  }
1856
2145
  async function updateTask(opts) {
@@ -1863,7 +2152,7 @@ async function updateTask(opts) {
1863
2152
  if (process.env["NWORKS_VERBOSE"] === "1") {
1864
2153
  console.error(`[nworks] PATCH ${url}`);
1865
2154
  }
1866
- const res = await authedFetch4(
2155
+ const res = await userFetch(
1867
2156
  url,
1868
2157
  {
1869
2158
  method: "PATCH",
@@ -1872,7 +2161,7 @@ async function updateTask(opts) {
1872
2161
  },
1873
2162
  profile
1874
2163
  );
1875
- if (!res.ok) return handleError4(res);
2164
+ if (!res.ok) return handleUserApiError(res);
1876
2165
  return await res.json();
1877
2166
  }
1878
2167
  async function completeTask(taskId, profile = "default") {
@@ -1880,39 +2169,39 @@ async function completeTask(taskId, profile = "default") {
1880
2169
  if (process.env["NWORKS_VERBOSE"] === "1") {
1881
2170
  console.error(`[nworks] POST ${url}`);
1882
2171
  }
1883
- const res = await authedFetch4(
2172
+ const res = await userFetch(
1884
2173
  url,
1885
2174
  { method: "POST" },
1886
2175
  profile
1887
2176
  );
1888
2177
  if (res.status === 204) return;
1889
- if (!res.ok) return handleError4(res);
2178
+ if (!res.ok) return handleUserApiError(res);
1890
2179
  }
1891
2180
  async function incompleteTask(taskId, profile = "default") {
1892
2181
  const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}/incomplete`;
1893
2182
  if (process.env["NWORKS_VERBOSE"] === "1") {
1894
2183
  console.error(`[nworks] POST ${url}`);
1895
2184
  }
1896
- const res = await authedFetch4(
2185
+ const res = await userFetch(
1897
2186
  url,
1898
2187
  { method: "POST" },
1899
2188
  profile
1900
2189
  );
1901
2190
  if (res.status === 204) return;
1902
- if (!res.ok) return handleError4(res);
2191
+ if (!res.ok) return handleUserApiError(res);
1903
2192
  }
1904
2193
  async function deleteTask(taskId, profile = "default") {
1905
2194
  const url = `${BASE_URL5}/tasks/${sanitizePathSegment(taskId)}`;
1906
2195
  if (process.env["NWORKS_VERBOSE"] === "1") {
1907
2196
  console.error(`[nworks] DELETE ${url}`);
1908
2197
  }
1909
- const res = await authedFetch4(
2198
+ const res = await userFetch(
1910
2199
  url,
1911
2200
  { method: "DELETE" },
1912
2201
  profile
1913
2202
  );
1914
2203
  if (res.status === 204) return;
1915
- if (!res.ok) return handleError4(res);
2204
+ if (!res.ok) return handleUserApiError(res);
1916
2205
  }
1917
2206
 
1918
2207
  // src/commands/task.ts
@@ -2035,26 +2324,6 @@ import { Command as Command10 } from "commander";
2035
2324
 
2036
2325
  // src/api/board.ts
2037
2326
  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
2327
  function safeParseJson(text) {
2059
2328
  const safe = text.replace(
2060
2329
  /"((?:board|post|domain|user)Id)"\s*:\s*(\d{16,})/g,
@@ -2070,8 +2339,8 @@ async function listBoards(count = 20, cursor, profile = "default") {
2070
2339
  if (process.env["NWORKS_VERBOSE"] === "1") {
2071
2340
  console.error(`[nworks] GET ${url}`);
2072
2341
  }
2073
- const res = await authedFetch5(url, { method: "GET" }, profile);
2074
- if (!res.ok) return handleError5(res);
2342
+ const res = await userFetch(url, { method: "GET" }, profile);
2343
+ if (!res.ok) return handleUserApiError(res);
2075
2344
  const text = await res.text();
2076
2345
  if (process.env["NWORKS_VERBOSE"] === "1") {
2077
2346
  console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
@@ -2087,8 +2356,8 @@ async function listPosts(boardId, count = 20, cursor, profile = "default") {
2087
2356
  if (process.env["NWORKS_VERBOSE"] === "1") {
2088
2357
  console.error(`[nworks] GET ${url}`);
2089
2358
  }
2090
- const res = await authedFetch5(url, { method: "GET" }, profile);
2091
- if (!res.ok) return handleError5(res);
2359
+ const res = await userFetch(url, { method: "GET" }, profile);
2360
+ if (!res.ok) return handleUserApiError(res);
2092
2361
  const text = await res.text();
2093
2362
  if (process.env["NWORKS_VERBOSE"] === "1") {
2094
2363
  console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
@@ -2101,8 +2370,8 @@ async function readPost(boardId, postId, profile = "default") {
2101
2370
  if (process.env["NWORKS_VERBOSE"] === "1") {
2102
2371
  console.error(`[nworks] GET ${url}`);
2103
2372
  }
2104
- const res = await authedFetch5(url, { method: "GET" }, profile);
2105
- if (!res.ok) return handleError5(res);
2373
+ const res = await userFetch(url, { method: "GET" }, profile);
2374
+ if (!res.ok) return handleUserApiError(res);
2106
2375
  const text = await res.text();
2107
2376
  if (process.env["NWORKS_VERBOSE"] === "1") {
2108
2377
  console.error(`[nworks] Response: ${res.status} (${text.length} bytes)`);
@@ -2122,7 +2391,7 @@ async function createPost(opts) {
2122
2391
  console.error(`[nworks] POST ${url}`);
2123
2392
  console.error(`[nworks] Body: ${JSON.stringify(body).length} bytes`);
2124
2393
  }
2125
- const res = await authedFetch5(
2394
+ const res = await userFetch(
2126
2395
  url,
2127
2396
  {
2128
2397
  method: "POST",
@@ -2138,7 +2407,7 @@ async function createPost(opts) {
2138
2407
  }
2139
2408
  return safeParseJson(text);
2140
2409
  }
2141
- return handleError5(res);
2410
+ return handleUserApiError(res);
2142
2411
  }
2143
2412
 
2144
2413
  // src/commands/board.ts
@@ -2236,128 +2505,256 @@ var createCommand3 = new Command10("create").description("Create a post in a boa
2236
2505
  });
2237
2506
  var boardCommand = new Command10("board").description("Board operations (requires User OAuth)").addCommand(listCommand5).addCommand(postsCommand).addCommand(readCommand2).addCommand(createCommand3);
2238
2507
 
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
2508
+ // src/commands/contact.ts
2250
2509
  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;
2510
+
2511
+ // src/api/contact.ts
2512
+ var BASE_URL7 = "https://www.worksapis.com/v1.0";
2513
+ async function listContacts(opts = {}) {
2514
+ const userId = opts.userId ?? "me";
2515
+ const profile = opts.profile ?? "default";
2516
+ const params = new URLSearchParams();
2517
+ params.set("count", String(opts.count ?? 20));
2518
+ if (opts.cursor) params.set("cursor", opts.cursor);
2519
+ if (opts.accessibleRange) params.set("accessibleRange", opts.accessibleRange);
2520
+ if (opts.contactTagId) params.set("contactTagId", opts.contactTagId);
2521
+ if (opts.email) params.set("email", opts.email);
2522
+ if (opts.telephone) params.set("telephone", opts.telephone);
2523
+ if (opts.orderBy) params.set("orderBy", opts.orderBy);
2524
+ if (opts.searchDateType) params.set("searchDateType", opts.searchDateType);
2525
+ if (opts.startDateTime) params.set("startDateTime", opts.startDateTime);
2526
+ if (opts.endDateTime) params.set("endDateTime", opts.endDateTime);
2527
+ const url = `${BASE_URL7}/users/${sanitizePathSegment(userId)}/contacts?${params.toString()}`;
2528
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2529
+ console.error(`[nworks] GET ${url}`);
2262
2530
  }
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)" });
2531
+ const res = await userFetch(url, { method: "GET" }, profile);
2532
+ if (!res.ok) return handleUserApiError(res);
2533
+ const data = await res.json();
2534
+ return { contacts: data.contacts ?? [], responseMetaData: data.responseMetaData };
2535
+ }
2536
+ async function getContact(contactId, profile = "default") {
2537
+ const url = `${BASE_URL7}/contacts/${sanitizePathSegment(contactId)}`;
2538
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2539
+ console.error(`[nworks] GET ${url}`);
2267
2540
  }
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" });
2541
+ const res = await userFetch(url, { method: "GET" }, profile);
2542
+ if (!res.ok) return handleUserApiError(res);
2543
+ return await res.json();
2544
+ }
2545
+ async function createContact(payload, profile = "default") {
2546
+ const url = `${BASE_URL7}/contacts`;
2547
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2548
+ console.error(`[nworks] POST ${url}`);
2281
2549
  }
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)" });
2550
+ const res = await userFetch(
2551
+ url,
2552
+ {
2553
+ method: "POST",
2554
+ headers: { "Content-Type": "application/json" },
2555
+ body: JSON.stringify(payload)
2556
+ },
2557
+ profile
2558
+ );
2559
+ if (!res.ok) return handleUserApiError(res);
2560
+ return await res.json();
2561
+ }
2562
+ async function updateContact(contactId, payload, partial = true, profile = "default") {
2563
+ const url = `${BASE_URL7}/contacts/${sanitizePathSegment(contactId)}`;
2564
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2565
+ console.error(`[nworks] ${partial ? "PATCH" : "PUT"} ${url}`);
2286
2566
  }
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" });
2567
+ const res = await userFetch(
2568
+ url,
2569
+ {
2570
+ method: partial ? "PATCH" : "PUT",
2571
+ headers: { "Content-Type": "application/json" },
2572
+ body: JSON.stringify(payload)
2573
+ },
2574
+ profile
2575
+ );
2576
+ if (!res.ok) return handleUserApiError(res);
2577
+ return await res.json();
2578
+ }
2579
+ async function deleteContact(contactId, profile = "default") {
2580
+ const url = `${BASE_URL7}/contacts/${sanitizePathSegment(contactId)}`;
2581
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2582
+ console.error(`[nworks] DELETE ${url}`);
2297
2583
  }
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}`
2584
+ const res = await userFetch(url, { method: "DELETE" }, profile);
2585
+ if (res.status === 204) return;
2586
+ if (!res.ok) return handleUserApiError(res);
2587
+ }
2588
+ async function listContactTags(opts = {}) {
2589
+ const userId = opts.userId ?? "me";
2590
+ const profile = opts.profile ?? "default";
2591
+ const params = new URLSearchParams();
2592
+ params.set("count", String(opts.count ?? 20));
2593
+ if (opts.cursor) params.set("cursor", opts.cursor);
2594
+ const url = `${BASE_URL7}/users/${sanitizePathSegment(userId)}/contact-tags?${params.toString()}`;
2595
+ if (process.env["NWORKS_VERBOSE"] === "1") {
2596
+ console.error(`[nworks] GET ${url}`);
2597
+ }
2598
+ const res = await userFetch(url, { method: "GET" }, profile);
2599
+ if (!res.ok) return handleUserApiError(res);
2600
+ const data = await res.json();
2601
+ return { contactTags: data.contactTags ?? [], responseMetaData: data.responseMetaData };
2602
+ }
2603
+
2604
+ // src/commands/contact.ts
2605
+ 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) => {
2606
+ try {
2607
+ const result = await listContacts({
2608
+ userId: opts.user ?? "me",
2609
+ count: parseInt(opts.count, 10),
2610
+ cursor: opts.cursor,
2611
+ contactTagId: opts.tag,
2612
+ email: opts.email,
2613
+ telephone: opts.tel,
2614
+ profile: opts.profile
2305
2615
  });
2306
- } else {
2307
- results.push({ check: "userOAuth", status: "SKIP", detail: "\uD1A0\uD070 \uC5C6\uC74C. `nworks login --user` \uD544\uC694" });
2616
+ output(
2617
+ {
2618
+ contacts: result.contacts,
2619
+ count: result.contacts.length,
2620
+ nextCursor: result.responseMetaData?.nextCursor ?? null
2621
+ },
2622
+ opts
2623
+ );
2624
+ } catch (err) {
2625
+ cliError(err, opts, "contact");
2308
2626
  }
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" });
2627
+ });
2628
+ 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) => {
2629
+ try {
2630
+ const result = await getContact(opts.id, opts.profile);
2631
+ output(result, opts);
2632
+ } catch (err) {
2633
+ cliError(err, opts, "contact");
2337
2634
  }
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) => {
2635
+ });
2636
+ 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
2637
  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();
2638
+ const payload = JSON.parse(opts.payload);
2639
+ if (opts.dryRun) {
2640
+ output({ dryRun: true, request: payload }, opts);
2641
+ return;
2352
2642
  }
2643
+ const result = await createContact(payload, opts.profile);
2644
+ output({ success: true, ...result }, opts);
2353
2645
  } catch (err) {
2354
- cliError(err, opts);
2646
+ cliError(err, opts, "contact");
2647
+ }
2648
+ });
2649
+ 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) => {
2650
+ try {
2651
+ const payload = JSON.parse(opts.payload);
2652
+ const result = await updateContact(
2653
+ opts.id,
2654
+ payload,
2655
+ !opts.replace,
2656
+ opts.profile
2657
+ );
2658
+ output({ success: true, ...result }, opts);
2659
+ } catch (err) {
2660
+ cliError(err, opts, "contact");
2661
+ }
2662
+ });
2663
+ 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) => {
2664
+ try {
2665
+ await deleteContact(opts.id, opts.profile);
2666
+ output({ success: true, contactId: opts.id, message: "Contact deleted" }, opts);
2667
+ } catch (err) {
2668
+ cliError(err, opts, "contact");
2669
+ }
2670
+ });
2671
+ 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) => {
2672
+ try {
2673
+ const result = await listContactTags({
2674
+ userId: opts.user ?? "me",
2675
+ count: parseInt(opts.count, 10),
2676
+ cursor: opts.cursor,
2677
+ profile: opts.profile
2678
+ });
2679
+ output(
2680
+ {
2681
+ contactTags: result.contactTags,
2682
+ count: result.contactTags.length,
2683
+ nextCursor: result.responseMetaData?.nextCursor ?? null
2684
+ },
2685
+ opts
2686
+ );
2687
+ } catch (err) {
2688
+ cliError(err, opts, "contact");
2355
2689
  }
2356
2690
  });
2691
+ 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
2692
 
2358
- // src/mcp/tools.ts
2359
- function registerTools(server) {
2360
- server.tool(
2693
+ // src/commands/mcp-cmd.ts
2694
+ import { Command as Command13 } from "commander";
2695
+ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
2696
+
2697
+ // src/mcp/server.ts
2698
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2699
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2700
+
2701
+ // src/mcp/tools/setup.ts
2702
+ import { z as z2 } from "zod";
2703
+
2704
+ // src/mcp/tool-registry.ts
2705
+ var TOOL_ANNOTATIONS = {
2706
+ nworks_setup: { title: "Save credentials", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
2707
+ nworks_message_send: { title: "Send message", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2708
+ nworks_message_members: { title: "List channel members", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2709
+ nworks_directory_members: { title: "List organization members", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2710
+ nworks_calendar_list: { title: "List calendar events", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2711
+ nworks_calendar_create: { title: "Create calendar event", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2712
+ nworks_calendar_update: { title: "Update calendar event", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2713
+ nworks_calendar_delete: { title: "Delete calendar event", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2714
+ nworks_drive_list: { title: "List drive files", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2715
+ nworks_drive_upload: { title: "Upload file to drive", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2716
+ // download는 outputDir 지정 시 로컬 파일을 쓰므로 순수 읽기가 아니다(readOnlyHint=false).
2717
+ nworks_drive_download: { title: "Download drive file", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
2718
+ nworks_sharedrive_list: { title: "List shared drives", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2719
+ nworks_sharedrive_files: { title: "List shared drive files", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2720
+ nworks_sharedrive_download: { title: "Download shared drive file", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
2721
+ nworks_mail_send: { title: "Send mail", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2722
+ nworks_mail_list: { title: "List mail", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2723
+ nworks_mail_read: { title: "Read mail", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2724
+ // drive_download와 같은 이유로 outputDir 지정 시 로컬 파일을 쓴다(readOnlyHint=false).
2725
+ nworks_mail_download_attachment: { title: "Download mail attachment", readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
2726
+ nworks_task_list: { title: "List tasks", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2727
+ nworks_task_create: { title: "Create task", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2728
+ nworks_task_update: { title: "Update task", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2729
+ nworks_task_delete: { title: "Delete task", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2730
+ nworks_board_list: { title: "List boards", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2731
+ nworks_board_posts: { title: "List board posts", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2732
+ nworks_board_read: { title: "Read board post", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2733
+ nworks_board_create: { title: "Create board post", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2734
+ nworks_contact_list: { title: "List contacts", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2735
+ nworks_contact_get: { title: "Get contact", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2736
+ nworks_contact_create: { title: "Create contact", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2737
+ nworks_contact_update: { title: "Update contact", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2738
+ nworks_contact_delete: { title: "Delete contact", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2739
+ nworks_contact_list_tags: { title: "List contact tags", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2740
+ nworks_login_user: { title: "Log in with User OAuth", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2741
+ nworks_whoami: { title: "Show auth status", readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2742
+ nworks_logout: { title: "Log out", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
2743
+ nworks_doctor: { title: "Diagnose connection", readOnlyHint: true, idempotentHint: true, openWorldHint: true }
2744
+ };
2745
+ function defineTool(server, name, description, inputSchema, handler) {
2746
+ const annotations = TOOL_ANNOTATIONS[name];
2747
+ server.registerTool(
2748
+ name,
2749
+ { description, inputSchema, ...annotations ? { annotations } : {} },
2750
+ handler
2751
+ );
2752
+ }
2753
+
2754
+ // src/mcp/tools/setup.ts
2755
+ function registerSetupTools(server) {
2756
+ defineTool(
2757
+ server,
2361
2758
  "nworks_setup",
2362
2759
  `NAVER WORKS API \uC778\uC99D \uC815\uBCF4\uB97C \uC124\uC815\uD569\uB2C8\uB2E4.
2363
2760
 
@@ -2380,10 +2777,10 @@ function registerTools(server) {
2380
2777
 
2381
2778
  OAuth Redirect URI: http://localhost:9876/callback`,
2382
2779
  {
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")
2780
+ clientId: z2.string().describe("Client ID (Developer Console\uC5D0\uC11C \uBC1C\uAE09)"),
2781
+ serviceAccount: z2.string().optional().describe("Service Account ID (\uC608: xxxxx.serviceaccount@domain)"),
2782
+ botId: z2.string().optional().describe("Bot ID (\uBA54\uC2DC\uC9C0 \uC804\uC1A1 \uC2DC \uD544\uC694)"),
2783
+ domainId: z2.string().optional().describe("Domain ID")
2387
2784
  },
2388
2785
  async ({ clientId, serviceAccount, botId, domainId }) => {
2389
2786
  try {
@@ -2421,7 +2818,7 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2421
2818
  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
2819
  }
2423
2820
  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)))}`;
2821
+ const mask2 = (s) => s.length <= 4 ? "****" : `****${s.slice(-Math.min(4, Math.floor(s.length / 3)))}`;
2425
2822
  return {
2426
2823
  content: [
2427
2824
  {
@@ -2430,10 +2827,10 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2430
2827
  success: true,
2431
2828
  message: "\uC778\uC99D \uC815\uBCF4\uAC00 \uC800\uC7A5\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
2432
2829
  nextSteps,
2433
- clientId: mask(clientId),
2434
- clientSecret: `${mask(resolvedSecret)} (\uD658\uACBD\uBCC0\uC218)`,
2830
+ clientId: mask2(clientId),
2831
+ clientSecret: `${mask2(resolvedSecret)} (\uD658\uACBD\uBCC0\uC218)`,
2435
2832
  serviceAccount: serviceAccount ?? null,
2436
- privateKeyPath: resolvedPrivateKeyPath ? `${mask(resolvedPrivateKeyPath)} (\uD658\uACBD\uBCC0\uC218)` : null,
2833
+ privateKeyPath: resolvedPrivateKeyPath ? `${mask2(resolvedPrivateKeyPath)} (\uD658\uACBD\uBCC0\uC218)` : null,
2437
2834
  botId: botId ?? null
2438
2835
  })
2439
2836
  }
@@ -2447,16 +2844,22 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2447
2844
  }
2448
2845
  }
2449
2846
  );
2450
- server.tool(
2847
+ }
2848
+
2849
+ // src/mcp/tools/message.ts
2850
+ import { z as z3 } from "zod";
2851
+ function registerMessageTools(server) {
2852
+ defineTool(
2853
+ server,
2451
2854
  "nworks_message_send",
2452
2855
  "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
2856
  {
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)")
2857
+ to: z3.string().optional().describe("\uC218\uC2E0\uC790 userId (channel\uACFC \uD0DD 1). nworks_directory_members\uB85C userId \uC870\uD68C \uAC00\uB2A5"),
2858
+ 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"),
2859
+ text: z3.string().describe("\uBA54\uC2DC\uC9C0 \uBCF8\uBB38"),
2860
+ type: z3.enum(["text", "button", "list"]).optional().describe("\uBA54\uC2DC\uC9C0 \uD0C0\uC785 (\uAE30\uBCF8: text)"),
2861
+ actions: z3.string().optional().describe("\uBC84\uD2BC \uC561\uC158 JSON (type=button\uC77C \uB54C)"),
2862
+ elements: z3.string().optional().describe("\uB9AC\uC2A4\uD2B8 \uD56D\uBAA9 JSON (type=list\uC77C \uB54C)")
2460
2863
  },
2461
2864
  async ({ to, channel, text, type, actions, elements }) => {
2462
2865
  try {
@@ -2479,11 +2882,12 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2479
2882
  }
2480
2883
  }
2481
2884
  );
2482
- server.tool(
2885
+ defineTool(
2886
+ server,
2483
2887
  "nworks_message_members",
2484
2888
  "\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
2889
  {
2486
- channel: z.string().describe("\uCC44\uB110 channelId")
2890
+ channel: z3.string().describe("\uCC44\uB110 channelId")
2487
2891
  },
2488
2892
  async ({ channel }) => {
2489
2893
  try {
@@ -2499,15 +2903,29 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2499
2903
  }
2500
2904
  }
2501
2905
  );
2502
- server.tool(
2906
+ }
2907
+
2908
+ // src/mcp/tools/directory.ts
2909
+ import { z as z4 } from "zod";
2910
+ function registerDirectoryTools(server) {
2911
+ defineTool(
2912
+ server,
2503
2913
  "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 () => {
2914
+ "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",
2915
+ {
2916
+ count: z4.number().int().min(1).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 100)"),
2917
+ cursor: z4.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C (\uC774\uC804 \uC751\uB2F5\uC758 nextCursor \uAC12)")
2918
+ },
2919
+ async ({ count, cursor }) => {
2507
2920
  try {
2508
- const result = await listUsers();
2921
+ const result = await listUsers(count ?? 100, cursor);
2509
2922
  return {
2510
- content: [{ type: "text", text: JSON.stringify(result) }]
2923
+ content: [{ type: "text", text: JSON.stringify({
2924
+ users: result.users,
2925
+ count: result.users.length,
2926
+ hasMore: !!result.responseMetaData?.nextCursor,
2927
+ nextCursor: result.responseMetaData?.nextCursor ?? null
2928
+ }) }]
2511
2929
  };
2512
2930
  } catch (err) {
2513
2931
  return {
@@ -2517,13 +2935,19 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2517
2935
  }
2518
2936
  }
2519
2937
  );
2520
- server.tool(
2938
+ }
2939
+
2940
+ // src/mcp/tools/calendar.ts
2941
+ import { z as z5 } from "zod";
2942
+ function registerCalendarTools(server) {
2943
+ defineTool(
2944
+ server,
2521
2945
  "nworks_calendar_list",
2522
2946
  "\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
2947
  {
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)")
2948
+ fromDateTime: z5.string().describe("\uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss+09:00)"),
2949
+ untilDateTime: z5.string().describe("\uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss+09:00)"),
2950
+ userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2527
2951
  },
2528
2952
  async ({ fromDateTime, untilDateTime, userId }) => {
2529
2953
  try {
@@ -2552,19 +2976,20 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2552
2976
  }
2553
2977
  }
2554
2978
  );
2555
- server.tool(
2979
+ defineTool(
2980
+ server,
2556
2981
  "nworks_calendar_create",
2557
2982
  "\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
2983
  {
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)")
2984
+ summary: z5.string().describe("\uC77C\uC815 \uC81C\uBAA9"),
2985
+ start: z5.string().describe("\uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
2986
+ end: z5.string().describe("\uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
2987
+ timeZone: z5.string().optional().describe("\uD0C0\uC784\uC874 (\uAE30\uBCF8: Asia/Seoul)"),
2988
+ description: z5.string().optional().describe("\uC77C\uC815 \uC124\uBA85"),
2989
+ location: z5.string().optional().describe("\uC7A5\uC18C"),
2990
+ attendees: z5.array(z5.object({ email: z5.string(), displayName: z5.string().optional() })).optional().describe("\uCC38\uC11D\uC790 \uBAA9\uB85D"),
2991
+ sendNotification: z5.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
2992
+ userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2568
2993
  },
2569
2994
  async ({
2570
2995
  summary,
@@ -2601,19 +3026,20 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2601
3026
  }
2602
3027
  }
2603
3028
  );
2604
- server.tool(
3029
+ defineTool(
3030
+ server,
2605
3031
  "nworks_calendar_update",
2606
3032
  "\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
3033
  {
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)")
3034
+ eventId: z5.string().describe("\uC77C\uC815 ID (nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3035
+ summary: z5.string().optional().describe("\uC0C8 \uC81C\uBAA9"),
3036
+ start: z5.string().optional().describe("\uC0C8 \uC2DC\uC791 \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
3037
+ end: z5.string().optional().describe("\uC0C8 \uC885\uB8CC \uC77C\uC2DC (YYYY-MM-DDThh:mm:ss)"),
3038
+ timeZone: z5.string().optional().describe("\uD0C0\uC784\uC874 (\uAE30\uBCF8: Asia/Seoul)"),
3039
+ description: z5.string().optional().describe("\uC0C8 \uC124\uBA85"),
3040
+ location: z5.string().optional().describe("\uC0C8 \uC7A5\uC18C"),
3041
+ sendNotification: z5.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
3042
+ userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2617
3043
  },
2618
3044
  async ({
2619
3045
  eventId,
@@ -2649,13 +3075,14 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2649
3075
  }
2650
3076
  }
2651
3077
  );
2652
- server.tool(
3078
+ defineTool(
3079
+ server,
2653
3080
  "nworks_calendar_delete",
2654
3081
  "\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
3082
  {
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)")
3083
+ eventId: z5.string().describe("\uC0AD\uC81C\uD560 \uC77C\uC815 ID (nworks_calendar_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3084
+ sendNotification: z5.boolean().optional().describe("\uCC38\uC11D\uC790\uC5D0\uAC8C \uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)"),
3085
+ userId: z5.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2659
3086
  },
2660
3087
  async ({ eventId, sendNotification, userId }) => {
2661
3088
  try {
@@ -2675,14 +3102,20 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2675
3102
  }
2676
3103
  }
2677
3104
  );
2678
- server.tool(
3105
+ }
3106
+
3107
+ // src/mcp/tools/drive.ts
3108
+ import { z as z6 } from "zod";
3109
+ function registerDriveTools(server) {
3110
+ defineTool(
3111
+ server,
2679
3112
  "nworks_drive_list",
2680
3113
  "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
3114
  {
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")
3115
+ userId: z6.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
3116
+ folderId: z6.string().optional().describe("\uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
3117
+ count: z6.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 200)"),
3118
+ cursor: z6.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
2686
3119
  },
2687
3120
  async ({ userId, folderId, count, cursor }) => {
2688
3121
  try {
@@ -2711,16 +3144,17 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2711
3144
  }
2712
3145
  }
2713
3146
  );
2714
- server.tool(
3147
+ defineTool(
3148
+ server,
2715
3149
  "nworks_drive_upload",
2716
3150
  "\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
3151
  {
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)")
3152
+ content: z6.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uD30C\uC77C \uB0B4\uC6A9 (base64 \uC778\uCF54\uB529). filePath \uB300\uC2E0 \uC0AC\uC6A9"),
3153
+ fileName: z6.string().optional().describe("\uD30C\uC77C\uBA85 (content \uC0AC\uC6A9 \uC2DC \uD544\uC218)"),
3154
+ 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)"),
3155
+ userId: z6.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
3156
+ folderId: z6.string().optional().describe("\uC5C5\uB85C\uB4DC\uD560 \uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
3157
+ overwrite: z6.boolean().optional().describe("\uB3D9\uC77C \uD30C\uC77C\uBA85 \uB36E\uC5B4\uC4F0\uAE30 (\uAE30\uBCF8: false)")
2724
3158
  },
2725
3159
  async ({ content, fileName, filePath, userId, folderId, overwrite }) => {
2726
3160
  try {
@@ -2755,43 +3189,157 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2755
3189
  };
2756
3190
  }
2757
3191
  return {
2758
- content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
3192
+ content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
3193
+ };
3194
+ } catch (err) {
3195
+ if (process.env["NWORKS_VERBOSE"] === "1") {
3196
+ console.error(`[nworks] drive upload error: ${err.stack}`);
3197
+ }
3198
+ return {
3199
+ content: [{ type: "text", text: mcpErrorHint(err, "drive.upload") }],
3200
+ isError: true
3201
+ };
3202
+ }
3203
+ }
3204
+ );
3205
+ defineTool(
3206
+ server,
3207
+ "nworks_drive_download",
3208
+ "\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.",
3209
+ {
3210
+ fileId: z6.string().describe("\uB2E4\uC6B4\uB85C\uB4DC\uD560 \uD30C\uC77C ID (nworks_drive_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3211
+ 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)"),
3212
+ outputName: z6.string().optional().describe("\uC800\uC7A5 \uD30C\uC77C\uBA85 (\uBBF8\uC9C0\uC815 \uC2DC \uC6D0\uBCF8 \uD30C\uC77C\uBA85)"),
3213
+ userId: z6.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
3214
+ },
3215
+ async ({ fileId, outputDir, outputName, userId }) => {
3216
+ try {
3217
+ const result = await downloadFile(
3218
+ fileId,
3219
+ userId ?? "me"
3220
+ );
3221
+ const fileName = outputName ?? result.fileName ?? fileId;
3222
+ if (outputDir) {
3223
+ const { writeFile: writeFile4 } = await import("fs/promises");
3224
+ const { join: join5 } = await import("path");
3225
+ const safeDir = validateLocalPath(outputDir);
3226
+ const safeName = sanitizeFileName(fileName);
3227
+ const outPath = join5(safeDir, safeName);
3228
+ validateLocalPath(outPath, safeDir);
3229
+ await writeFile4(outPath, result.buffer);
3230
+ return {
3231
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
3232
+ };
3233
+ }
3234
+ const MAX_INLINE_SIZE = 5 * 1024 * 1024;
3235
+ if (result.buffer.length > MAX_INLINE_SIZE) {
3236
+ const sizeMB = (result.buffer.length / (1024 * 1024)).toFixed(1);
3237
+ return {
3238
+ 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 }) }],
3239
+ isError: true
3240
+ };
3241
+ }
3242
+ 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;
3243
+ const isText = textExtensions.test(fileName);
3244
+ if (isText) {
3245
+ const text = result.buffer.toString("utf-8");
3246
+ return {
3247
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "text", content: text }) }]
3248
+ };
3249
+ }
3250
+ const base64 = result.buffer.toString("base64");
3251
+ return {
3252
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "base64", content: base64 }) }]
3253
+ };
3254
+ } catch (err) {
3255
+ return {
3256
+ content: [{ type: "text", text: mcpErrorHint(err, "drive.download") }],
3257
+ isError: true
3258
+ };
3259
+ }
3260
+ }
3261
+ );
3262
+ defineTool(
3263
+ server,
3264
+ "nworks_sharedrive_list",
3265
+ "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)",
3266
+ {
3267
+ count: z6.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 200)"),
3268
+ cursor: z6.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
3269
+ },
3270
+ async ({ count, cursor }) => {
3271
+ try {
3272
+ const result = await listSharedDrives(count ?? 20, cursor);
3273
+ return {
3274
+ content: [{ type: "text", text: JSON.stringify({ sharedDrives: result.sharedDrives, count: result.sharedDrives.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
3275
+ };
3276
+ } catch (err) {
3277
+ return {
3278
+ content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_list") }],
3279
+ isError: true
3280
+ };
3281
+ }
3282
+ }
3283
+ );
3284
+ defineTool(
3285
+ server,
3286
+ "nworks_sharedrive_files",
3287
+ "\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)",
3288
+ {
3289
+ sharedriveId: z6.string().describe("\uACF5\uC720 \uB4DC\uB77C\uC774\uBE0C ID (nworks_sharedrive_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3290
+ folderId: z6.string().optional().describe("\uD3F4\uB354 ID (\uBBF8\uC9C0\uC815 \uC2DC \uB8E8\uD2B8)"),
3291
+ count: z6.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 200)"),
3292
+ cursor: z6.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
3293
+ },
3294
+ async ({ sharedriveId, folderId, count, cursor }) => {
3295
+ try {
3296
+ const result = await listSharedDriveFiles(
3297
+ sharedriveId,
3298
+ folderId,
3299
+ count ?? 20,
3300
+ cursor
3301
+ );
3302
+ const files = result.files.map((f) => ({
3303
+ fileId: f.fileId,
3304
+ name: f.fileName,
3305
+ type: f.fileType,
3306
+ size: f.fileSize,
3307
+ modified: f.modifiedTime,
3308
+ path: f.filePath
3309
+ }));
3310
+ return {
3311
+ content: [{ type: "text", text: JSON.stringify({ files, count: files.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
2759
3312
  };
2760
3313
  } catch (err) {
2761
- if (process.env["NWORKS_VERBOSE"] === "1") {
2762
- console.error(`[nworks] drive upload error: ${err.stack}`);
2763
- }
2764
3314
  return {
2765
- content: [{ type: "text", text: mcpErrorHint(err, "drive.upload") }],
3315
+ content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_files") }],
2766
3316
  isError: true
2767
3317
  };
2768
3318
  }
2769
3319
  }
2770
3320
  );
2771
- server.tool(
2772
- "nworks_drive_download",
2773
- "\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.",
3321
+ defineTool(
3322
+ server,
3323
+ "nworks_sharedrive_download",
3324
+ "\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.",
2774
3325
  {
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)")
3326
+ sharedriveId: z6.string().describe("\uACF5\uC720 \uB4DC\uB77C\uC774\uBE0C ID (nworks_sharedrive_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3327
+ fileId: z6.string().describe("\uB2E4\uC6B4\uB85C\uB4DC\uD560 \uD30C\uC77C ID (nworks_sharedrive_files\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3328
+ 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)"),
3329
+ outputName: z6.string().optional().describe("\uC800\uC7A5 \uD30C\uC77C\uBA85 (\uBBF8\uC9C0\uC815 \uC2DC \uC6D0\uBCF8 \uD30C\uC77C\uBA85)")
2779
3330
  },
2780
- async ({ fileId, outputDir, outputName, userId }) => {
3331
+ async ({ sharedriveId, fileId, outputDir, outputName }) => {
2781
3332
  try {
2782
- const result = await downloadFile(
2783
- fileId,
2784
- userId ?? "me"
2785
- );
3333
+ const result = await downloadSharedDriveFile(sharedriveId, fileId);
2786
3334
  const fileName = outputName ?? result.fileName ?? fileId;
2787
3335
  if (outputDir) {
2788
- const { writeFile: writeFile3 } = await import("fs/promises");
2789
- const { join: join3 } = await import("path");
3336
+ const { writeFile: writeFile4 } = await import("fs/promises");
3337
+ const { join: join5 } = await import("path");
2790
3338
  const safeDir = validateLocalPath(outputDir);
2791
3339
  const safeName = sanitizeFileName(fileName);
2792
- const outPath = join3(safeDir, safeName);
3340
+ const outPath = join5(safeDir, safeName);
2793
3341
  validateLocalPath(outPath, safeDir);
2794
- await writeFile3(outPath, result.buffer);
3342
+ await writeFile4(outPath, result.buffer);
2795
3343
  return {
2796
3344
  content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
2797
3345
  };
@@ -2818,23 +3366,29 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2818
3366
  };
2819
3367
  } catch (err) {
2820
3368
  return {
2821
- content: [{ type: "text", text: mcpErrorHint(err, "drive.download") }],
3369
+ content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_download") }],
2822
3370
  isError: true
2823
3371
  };
2824
3372
  }
2825
3373
  }
2826
3374
  );
2827
- server.tool(
3375
+ }
3376
+
3377
+ // src/mcp/tools/mail.ts
3378
+ import { z as z7 } from "zod";
3379
+ function registerMailTools(server) {
3380
+ defineTool(
3381
+ server,
2828
3382
  "nworks_mail_send",
2829
3383
  "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)",
2830
3384
  {
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)")
3385
+ to: z7.string().describe("\uC218\uC2E0\uC790 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
3386
+ subject: z7.string().describe("\uBA54\uC77C \uC81C\uBAA9"),
3387
+ body: z7.string().optional().describe("\uBA54\uC77C \uBCF8\uBB38"),
3388
+ cc: z7.string().optional().describe("\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
3389
+ bcc: z7.string().optional().describe("\uC228\uC740\uCC38\uC870 \uC774\uBA54\uC77C (\uC5EC\uB7EC \uBA85\uC740 ; \uB85C \uAD6C\uBD84)"),
3390
+ contentType: z7.enum(["html", "text"]).optional().describe("\uBCF8\uBB38 \uD615\uC2DD (\uAE30\uBCF8: html)"),
3391
+ userId: z7.string().optional().describe("\uBC1C\uC2E0\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2838
3392
  },
2839
3393
  async ({ to, subject, body, cc, bcc, contentType, userId }) => {
2840
3394
  try {
@@ -2858,15 +3412,16 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2858
3412
  }
2859
3413
  }
2860
3414
  );
2861
- server.tool(
3415
+ defineTool(
3416
+ server,
2862
3417
  "nworks_mail_list",
2863
3418
  "\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)",
2864
3419
  {
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)")
3420
+ folderId: z7.number().int().min(0).optional().describe("\uBA54\uC77C \uD3F4\uB354 ID (\uAE30\uBCF8: 0 = \uBC1B\uC740\uD3B8\uC9C0\uD568)"),
3421
+ count: z7.number().int().min(1).max(200).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 30, \uCD5C\uB300: 200)"),
3422
+ cursor: z7.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
3423
+ isUnread: z7.boolean().optional().describe("\uC77D\uC9C0 \uC54A\uC740 \uBA54\uC77C\uB9CC \uC870\uD68C"),
3424
+ userId: z7.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2870
3425
  },
2871
3426
  async ({ folderId, count, cursor, isUnread, userId }) => {
2872
3427
  try {
@@ -2896,12 +3451,13 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2896
3451
  }
2897
3452
  }
2898
3453
  );
2899
- server.tool(
3454
+ defineTool(
3455
+ server,
2900
3456
  "nworks_mail_read",
2901
3457
  "\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
3458
  {
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)")
3459
+ mailId: z7.number().int().min(0).describe("\uBA54\uC77C ID (nworks_mail_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3460
+ userId: z7.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2905
3461
  },
2906
3462
  async ({ mailId, userId }) => {
2907
3463
  try {
@@ -2935,15 +3491,80 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2935
3491
  }
2936
3492
  }
2937
3493
  );
2938
- server.tool(
3494
+ defineTool(
3495
+ server,
3496
+ "nworks_mail_download_attachment",
3497
+ "\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.",
3498
+ {
3499
+ mailId: z7.number().int().min(0).describe("\uBA54\uC77C ID (nworks_mail_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3500
+ attachmentId: z7.number().int().min(0).describe("\uCCA8\uBD80\uD30C\uC77C ID (nworks_mail_read\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3501
+ 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)"),
3502
+ outputName: z7.string().optional().describe("\uC800\uC7A5 \uD30C\uC77C\uBA85 (\uBBF8\uC9C0\uC815 \uC2DC \uC6D0\uBCF8 \uD30C\uC77C\uBA85)"),
3503
+ userId: z7.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
3504
+ },
3505
+ async ({ mailId, attachmentId, outputDir, outputName, userId }) => {
3506
+ try {
3507
+ const result = await downloadAttachment(
3508
+ mailId,
3509
+ attachmentId,
3510
+ userId ?? "me"
3511
+ );
3512
+ const fileName = outputName ?? result.filename;
3513
+ if (outputDir) {
3514
+ const { writeFile: writeFile4 } = await import("fs/promises");
3515
+ const { join: join5 } = await import("path");
3516
+ const safeDir = validateLocalPath(outputDir);
3517
+ const safeName = sanitizeFileName(fileName);
3518
+ const outPath = join5(safeDir, safeName);
3519
+ validateLocalPath(outPath, safeDir);
3520
+ await writeFile4(outPath, result.buffer);
3521
+ return {
3522
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, path: outPath, size: result.buffer.length }) }]
3523
+ };
3524
+ }
3525
+ const MAX_INLINE_SIZE = 5 * 1024 * 1024;
3526
+ if (result.buffer.length > MAX_INLINE_SIZE) {
3527
+ const sizeMB = (result.buffer.length / (1024 * 1024)).toFixed(1);
3528
+ return {
3529
+ 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 }) }],
3530
+ isError: true
3531
+ };
3532
+ }
3533
+ 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;
3534
+ const isText = textExtensions.test(fileName);
3535
+ if (isText) {
3536
+ const text = result.buffer.toString("utf-8");
3537
+ return {
3538
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "text", content: text }) }]
3539
+ };
3540
+ }
3541
+ const base64 = result.buffer.toString("base64");
3542
+ return {
3543
+ content: [{ type: "text", text: JSON.stringify({ success: true, fileName, size: result.buffer.length, encoding: "base64", content: base64 }) }]
3544
+ };
3545
+ } catch (err) {
3546
+ return {
3547
+ content: [{ type: "text", text: mcpErrorHint(err, "mail.download_attachment") }],
3548
+ isError: true
3549
+ };
3550
+ }
3551
+ }
3552
+ );
3553
+ }
3554
+
3555
+ // src/mcp/tools/task.ts
3556
+ import { z as z8 } from "zod";
3557
+ function registerTaskTools(server) {
3558
+ defineTool(
3559
+ server,
2939
3560
  "nworks_task_list",
2940
3561
  "\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
3562
  {
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)")
3563
+ categoryId: z8.string().optional().describe("\uCE74\uD14C\uACE0\uB9AC ID (\uAE30\uBCF8: default)"),
3564
+ status: z8.enum(["TODO", "ALL"]).optional().describe("\uD544\uD130: TODO \uB610\uB294 ALL (\uAE30\uBCF8: ALL)"),
3565
+ count: z8.number().int().min(1).max(100).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 50, \uCD5C\uB300: 100)"),
3566
+ cursor: z8.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
3567
+ userId: z8.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2947
3568
  },
2948
3569
  async ({ categoryId, status, count, cursor, userId }) => {
2949
3570
  try {
@@ -2973,16 +3594,17 @@ OAuth Redirect URI: http://localhost:9876/callback`,
2973
3594
  }
2974
3595
  }
2975
3596
  );
2976
- server.tool(
3597
+ defineTool(
3598
+ server,
2977
3599
  "nworks_task_create",
2978
3600
  "\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
3601
  {
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)")
3602
+ title: z8.string().describe("\uD560 \uC77C \uC81C\uBAA9"),
3603
+ content: z8.string().optional().describe("\uD560 \uC77C \uB0B4\uC6A9"),
3604
+ dueDate: z8.string().optional().describe("\uB9C8\uAC10\uC77C (YYYY-MM-DD)"),
3605
+ categoryId: z8.string().optional().describe("\uCE74\uD14C\uACE0\uB9AC ID"),
3606
+ assigneeIds: z8.array(z8.string()).optional().describe("\uB2F4\uB2F9\uC790 user ID \uBAA9\uB85D (\uBBF8\uC9C0\uC815 \uC2DC \uC790\uAE30 \uC790\uC2E0)"),
3607
+ userId: z8.string().optional().describe("\uC0DD\uC131\uC790 user ID (\uBBF8\uC9C0\uC815 \uC2DC me)")
2986
3608
  },
2987
3609
  async ({ title, content, dueDate, categoryId, assigneeIds, userId }) => {
2988
3610
  try {
@@ -3005,15 +3627,16 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3005
3627
  }
3006
3628
  }
3007
3629
  );
3008
- server.tool(
3630
+ defineTool(
3631
+ server,
3009
3632
  "nworks_task_update",
3010
3633
  "\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
3634
  {
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)")
3635
+ taskId: z8.string().describe("\uD560 \uC77C ID (nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3636
+ status: z8.enum(["done", "todo"]).optional().describe("\uC0C1\uD0DC \uBCC0\uACBD: done(\uC644\uB8CC) \uB610\uB294 todo(\uBBF8\uC644\uB8CC)"),
3637
+ title: z8.string().optional().describe("\uC0C8 \uC81C\uBAA9"),
3638
+ content: z8.string().optional().describe("\uC0C8 \uB0B4\uC6A9"),
3639
+ dueDate: z8.string().optional().describe("\uC0C8 \uB9C8\uAC10\uC77C (YYYY-MM-DD)")
3017
3640
  },
3018
3641
  async ({ taskId, status, title, content, dueDate }) => {
3019
3642
  try {
@@ -3047,11 +3670,12 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3047
3670
  }
3048
3671
  }
3049
3672
  );
3050
- server.tool(
3673
+ defineTool(
3674
+ server,
3051
3675
  "nworks_task_delete",
3052
3676
  "\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
3677
  {
3054
- taskId: z.string().describe("\uC0AD\uC81C\uD560 \uD560 \uC77C ID (nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
3678
+ taskId: z8.string().describe("\uC0AD\uC81C\uD560 \uD560 \uC77C ID (nworks_task_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
3055
3679
  },
3056
3680
  async ({ taskId }) => {
3057
3681
  try {
@@ -3067,12 +3691,18 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3067
3691
  }
3068
3692
  }
3069
3693
  );
3070
- server.tool(
3694
+ }
3695
+
3696
+ // src/mcp/tools/board.ts
3697
+ import { z as z9 } from "zod";
3698
+ function registerBoardTools(server) {
3699
+ defineTool(
3700
+ server,
3071
3701
  "nworks_board_list",
3072
3702
  "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
3703
  {
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")
3704
+ count: z9.number().int().min(1).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20)"),
3705
+ cursor: z9.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
3076
3706
  },
3077
3707
  async ({ count, cursor }) => {
3078
3708
  try {
@@ -3093,13 +3723,14 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3093
3723
  }
3094
3724
  }
3095
3725
  );
3096
- server.tool(
3726
+ defineTool(
3727
+ server,
3097
3728
  "nworks_board_posts",
3098
3729
  "\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
3730
  {
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")
3731
+ boardId: z9.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3732
+ count: z9.number().int().min(1).max(40).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 40)"),
3733
+ cursor: z9.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
3103
3734
  },
3104
3735
  async ({ boardId, count, cursor }) => {
3105
3736
  try {
@@ -3123,12 +3754,13 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3123
3754
  }
3124
3755
  }
3125
3756
  );
3126
- server.tool(
3757
+ defineTool(
3758
+ server,
3127
3759
  "nworks_board_read",
3128
3760
  "\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
3761
  {
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)")
3762
+ boardId: z9.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3763
+ postId: z9.string().describe("\uAE00 ID (nworks_board_posts\uB85C \uC870\uD68C \uAC00\uB2A5)")
3132
3764
  },
3133
3765
  async ({ boardId, postId }) => {
3134
3766
  try {
@@ -3154,15 +3786,16 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3154
3786
  }
3155
3787
  }
3156
3788
  );
3157
- server.tool(
3789
+ defineTool(
3790
+ server,
3158
3791
  "nworks_board_create",
3159
3792
  "\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
3793
  {
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)")
3794
+ boardId: z9.string().describe("\uAC8C\uC2DC\uD310 ID (nworks_board_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3795
+ title: z9.string().describe("\uAE00 \uC81C\uBAA9"),
3796
+ body: z9.string().optional().describe("\uAE00 \uBCF8\uBB38"),
3797
+ enableComment: z9.boolean().optional().describe("\uB313\uAE00 \uD5C8\uC6A9 (\uAE30\uBCF8: true)"),
3798
+ sendNotifications: z9.boolean().optional().describe("\uC54C\uB9BC \uBC1C\uC1A1 (\uAE30\uBCF8: false)")
3166
3799
  },
3167
3800
  async ({ boardId, title, body, enableComment, sendNotifications }) => {
3168
3801
  try {
@@ -3184,33 +3817,173 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3184
3817
  }
3185
3818
  }
3186
3819
  );
3187
- server.tool(
3820
+ }
3821
+
3822
+ // src/mcp/tools/contact.ts
3823
+ import { z as z10 } from "zod";
3824
+ function registerContactTools(server) {
3825
+ defineTool(
3826
+ server,
3827
+ "nworks_contact_list",
3828
+ "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)",
3829
+ {
3830
+ userId: z10.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
3831
+ count: z10.number().int().min(1).max(500).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 500)"),
3832
+ cursor: z10.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C"),
3833
+ contactTagId: z10.string().optional().describe("\uC5F0\uB77D\uCC98 \uD0DC\uADF8 ID\uB85C \uD544\uD130\uB9C1 (nworks_contact_list_tags\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3834
+ email: z10.string().optional().describe("\uC774\uBA54\uC77C\uB85C \uD544\uD130\uB9C1"),
3835
+ telephone: z10.string().optional().describe("\uC804\uD654\uBC88\uD638\uB85C \uD544\uD130\uB9C1")
3836
+ },
3837
+ async ({ userId, count, cursor, contactTagId, email, telephone }) => {
3838
+ try {
3839
+ const result = await listContacts({
3840
+ userId: userId ?? "me",
3841
+ count: count ?? 20,
3842
+ cursor,
3843
+ contactTagId,
3844
+ email,
3845
+ telephone
3846
+ });
3847
+ return {
3848
+ content: [{ type: "text", text: JSON.stringify({ contacts: result.contacts, count: result.contacts.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
3849
+ };
3850
+ } catch (err) {
3851
+ return {
3852
+ content: [{ type: "text", text: mcpErrorHint(err, "contact.list") }],
3853
+ isError: true
3854
+ };
3855
+ }
3856
+ }
3857
+ );
3858
+ defineTool(
3859
+ server,
3860
+ "nworks_contact_get",
3861
+ "\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)",
3862
+ {
3863
+ contactId: z10.string().describe("\uC5F0\uB77D\uCC98 ID (nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
3864
+ },
3865
+ async ({ contactId }) => {
3866
+ try {
3867
+ const result = await getContact(contactId);
3868
+ return {
3869
+ content: [{ type: "text", text: JSON.stringify(result) }]
3870
+ };
3871
+ } catch (err) {
3872
+ return {
3873
+ content: [{ type: "text", text: mcpErrorHint(err, "contact.get") }],
3874
+ isError: true
3875
+ };
3876
+ }
3877
+ }
3878
+ );
3879
+ defineTool(
3880
+ server,
3881
+ "nworks_contact_create",
3882
+ `\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)`,
3883
+ {
3884
+ 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")
3885
+ },
3886
+ async ({ payload }) => {
3887
+ try {
3888
+ const result = await createContact(payload);
3889
+ return {
3890
+ content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
3891
+ };
3892
+ } catch (err) {
3893
+ return {
3894
+ content: [{ type: "text", text: mcpErrorHint(err, "contact.create") }],
3895
+ isError: true
3896
+ };
3897
+ }
3898
+ }
3899
+ );
3900
+ defineTool(
3901
+ server,
3902
+ "nworks_contact_update",
3903
+ "\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)",
3904
+ {
3905
+ contactId: z10.string().describe("\uC5F0\uB77D\uCC98 ID (nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5)"),
3906
+ payload: z10.record(z10.string(), z10.unknown()).describe("\uC218\uC815\uD560 \uD544\uB4DC (name, email, tel, company \uB4F1)"),
3907
+ replace: z10.boolean().optional().describe("\uC804\uCCB4 \uAD50\uCCB4(PUT) \uC5EC\uBD80 (\uAE30\uBCF8: false, \uBD80\uBD84 \uC218\uC815 PATCH)")
3908
+ },
3909
+ async ({ contactId, payload, replace }) => {
3910
+ try {
3911
+ const result = await updateContact(contactId, payload, !(replace ?? false));
3912
+ return {
3913
+ content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }]
3914
+ };
3915
+ } catch (err) {
3916
+ return {
3917
+ content: [{ type: "text", text: mcpErrorHint(err, "contact.update") }],
3918
+ isError: true
3919
+ };
3920
+ }
3921
+ }
3922
+ );
3923
+ defineTool(
3924
+ server,
3925
+ "nworks_contact_delete",
3926
+ "\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)",
3927
+ {
3928
+ contactId: z10.string().describe("\uC0AD\uC81C\uD560 \uC5F0\uB77D\uCC98 ID (nworks_contact_list\uB85C \uC870\uD68C \uAC00\uB2A5)")
3929
+ },
3930
+ async ({ contactId }) => {
3931
+ try {
3932
+ await deleteContact(contactId);
3933
+ return {
3934
+ content: [{ type: "text", text: JSON.stringify({ success: true, contactId, message: "\uC5F0\uB77D\uCC98\uAC00 \uC0AD\uC81C\uB418\uC5C8\uC2B5\uB2C8\uB2E4" }) }]
3935
+ };
3936
+ } catch (err) {
3937
+ return {
3938
+ content: [{ type: "text", text: mcpErrorHint(err, "contact.delete") }],
3939
+ isError: true
3940
+ };
3941
+ }
3942
+ }
3943
+ );
3944
+ defineTool(
3945
+ server,
3946
+ "nworks_contact_list_tags",
3947
+ "\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)",
3948
+ {
3949
+ userId: z10.string().optional().describe("\uB300\uC0C1 \uC0AC\uC6A9\uC790 ID (\uBBF8\uC9C0\uC815 \uC2DC me)"),
3950
+ count: z10.number().int().min(1).max(500).optional().describe("\uD398\uC774\uC9C0\uB2F9 \uD56D\uBAA9 \uC218 (\uAE30\uBCF8: 20, \uCD5C\uB300: 500)"),
3951
+ cursor: z10.string().optional().describe("\uD398\uC774\uC9C0\uB124\uC774\uC158 \uCEE4\uC11C")
3952
+ },
3953
+ async ({ userId, count, cursor }) => {
3954
+ try {
3955
+ const result = await listContactTags({ userId: userId ?? "me", count: count ?? 20, cursor });
3956
+ return {
3957
+ content: [{ type: "text", text: JSON.stringify({ contactTags: result.contactTags, count: result.contactTags.length, hasMore: !!result.responseMetaData?.nextCursor, nextCursor: result.responseMetaData?.nextCursor ?? null }) }]
3958
+ };
3959
+ } catch (err) {
3960
+ return {
3961
+ content: [{ type: "text", text: mcpErrorHint(err, "contact.list_tags") }],
3962
+ isError: true
3963
+ };
3964
+ }
3965
+ }
3966
+ );
3967
+ }
3968
+
3969
+ // src/mcp/tools/auth.ts
3970
+ import { z as z11 } from "zod";
3971
+ function registerAuthTools(server) {
3972
+ defineTool(
3973
+ server,
3188
3974
  "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.",
3975
+ "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
3976
  {
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")
3977
+ 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."),
3978
+ 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
3979
  },
3193
- async ({ scope }) => {
3194
- const DEFAULT_SCOPE = "calendar calendar.read file file.read mail mail.read task task.read user.read board board.read";
3980
+ async ({ preset, scope }) => {
3195
3981
  try {
3196
3982
  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
3983
  const existingToken = await loadUserToken();
3211
3984
  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(" ");
3985
+ const requestedScopes = resolveScopes(preset ?? scope);
3986
+ const mergedScopes = mergeScopes(existingScopes, requestedScopes);
3214
3987
  const state = generateSecureState();
3215
3988
  const authorizeUrl = buildAuthorizeUrl(creds.clientId, mergedScopes, state);
3216
3989
  startOAuthCallbackServer(creds.clientId, creds.clientSecret, state).then(
@@ -3247,7 +4020,8 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3247
4020
  }
3248
4021
  }
3249
4022
  );
3250
- server.tool(
4023
+ defineTool(
4024
+ server,
3251
4025
  "nworks_whoami",
3252
4026
  "\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
4027
  {},
@@ -3258,10 +4032,10 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3258
4032
  const userToken = await loadUserToken();
3259
4033
  const isValid = token ? token.expiresAt > Date.now() / 1e3 : false;
3260
4034
  const userTokenValid = userToken ? userToken.expiresAt > Date.now() / 1e3 : false;
3261
- const mask = (s) => s.length <= 4 ? "****" : `****${s.slice(-4)}`;
4035
+ const mask2 = (s) => s.length <= 4 ? "****" : `****${s.slice(-4)}`;
3262
4036
  const info = {
3263
4037
  serviceAccount: creds.serviceAccount ?? null,
3264
- clientId: mask(creds.clientId),
4038
+ clientId: mask2(creds.clientId),
3265
4039
  botId: creds.botId ?? null,
3266
4040
  tokenValid: isValid,
3267
4041
  userOAuth: userToken ? { valid: userTokenValid, scope: userToken.scope } : null
@@ -3277,7 +4051,8 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3277
4051
  }
3278
4052
  }
3279
4053
  );
3280
- server.tool(
4054
+ defineTool(
4055
+ server,
3281
4056
  "nworks_logout",
3282
4057
  "\uC800\uC7A5\uB41C NAVER WORKS \uC778\uC99D \uC815\uBCF4\uC640 \uD1A0\uD070\uC744 \uBAA8\uB450 \uC0AD\uC81C\uD569\uB2C8\uB2E4",
3283
4058
  {},
@@ -3315,7 +4090,141 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3315
4090
  }
3316
4091
  }
3317
4092
  );
3318
- server.tool(
4093
+ }
4094
+
4095
+ // src/commands/doctor.ts
4096
+ import { Command as Command12 } from "commander";
4097
+ import { existsSync as existsSync3 } from "fs";
4098
+ import { readFile as readFile5 } from "fs/promises";
4099
+ import { resolve as resolve2, join as join4, sep as sep2 } from "path";
4100
+ function mask(value) {
4101
+ return value.length <= 4 ? "****" : `****${value.slice(-4)}`;
4102
+ }
4103
+ async function runChecks(profile) {
4104
+ const results = [];
4105
+ let creds;
4106
+ try {
4107
+ creds = await loadCredentials(profile);
4108
+ results.push({ check: "credentials", status: "OK", detail: `clientId: ${mask(creds.clientId)}` });
4109
+ } catch {
4110
+ 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)." });
4111
+ return results;
4112
+ }
4113
+ if (hasServiceAccountCreds(creds)) {
4114
+ results.push({ check: "serviceAccount", status: "OK", detail: creds.serviceAccount });
4115
+ } else {
4116
+ results.push({ check: "serviceAccount", status: "SKIP", detail: "Not set (required for bot messaging)" });
4117
+ }
4118
+ if (creds.privateKeyPath) {
4119
+ if (existsSync3(creds.privateKeyPath)) {
4120
+ try {
4121
+ await readFile5(creds.privateKeyPath, "utf-8");
4122
+ results.push({ check: "privateKey", status: "OK", detail: creds.privateKeyPath });
4123
+ } catch {
4124
+ results.push({ check: "privateKey", status: "FAIL", detail: `Cannot read: ${creds.privateKeyPath}` });
4125
+ }
4126
+ } else {
4127
+ results.push({ check: "privateKey", status: "FAIL", detail: `File not found: ${creds.privateKeyPath}` });
4128
+ }
4129
+ } else {
4130
+ results.push({ check: "privateKey", status: "SKIP", detail: "Not set" });
4131
+ }
4132
+ if (creds.privateKeyPath && existsSync3(creds.privateKeyPath)) {
4133
+ const resolvedKey = resolve2(creds.privateKeyPath);
4134
+ const cwd = resolve2(process.cwd());
4135
+ const insideCwd = resolvedKey === cwd || resolvedKey.startsWith(cwd + sep2);
4136
+ const looksLikeProject = existsSync3(join4(cwd, ".git")) || existsSync3(join4(cwd, "package.json"));
4137
+ if (insideCwd && looksLikeProject) {
4138
+ results.push({
4139
+ check: "keyLocation",
4140
+ status: "WARN",
4141
+ 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."
4142
+ });
4143
+ }
4144
+ }
4145
+ if (creds.botId) {
4146
+ results.push({ check: "botId", status: "OK", detail: creds.botId });
4147
+ } else {
4148
+ results.push({ check: "botId", status: "SKIP", detail: "Not set (required to send messages)" });
4149
+ }
4150
+ const token = await loadToken(profile);
4151
+ if (token) {
4152
+ const valid = token.expiresAt > Date.now() / 1e3;
4153
+ results.push({
4154
+ check: "serviceToken",
4155
+ status: valid ? "OK" : "EXPIRED",
4156
+ detail: valid ? `expires: ${new Date(token.expiresAt * 1e3).toISOString()}` : `expired: ${new Date(token.expiresAt * 1e3).toISOString()}`
4157
+ });
4158
+ } else {
4159
+ results.push({ check: "serviceToken", status: "SKIP", detail: "No token" });
4160
+ }
4161
+ const userToken = await loadUserToken(profile);
4162
+ if (userToken) {
4163
+ const valid = userToken.expiresAt > Date.now() / 1e3;
4164
+ results.push({
4165
+ check: "userOAuth",
4166
+ status: valid ? "OK" : "EXPIRED",
4167
+ detail: valid ? `scope: ${userToken.scope} | expires: ${new Date(userToken.expiresAt * 1e3).toISOString()}` : `expired | scope: ${userToken.scope}`
4168
+ });
4169
+ } else {
4170
+ results.push({ check: "userOAuth", status: "SKIP", detail: "No token. Run `nworks login --user`." });
4171
+ }
4172
+ if (hasServiceAccountCreds(creds) && token && token.expiresAt > Date.now() / 1e3) {
4173
+ try {
4174
+ const res = await fetch("https://www.worksapis.com/v1.0/users/me", {
4175
+ headers: { Authorization: `Bearer ${token.accessToken}` }
4176
+ });
4177
+ if (res.ok) {
4178
+ results.push({ check: "apiConnection", status: "OK", detail: "Connected to NAVER WORKS API" });
4179
+ } else {
4180
+ results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
4181
+ }
4182
+ } catch (e) {
4183
+ results.push({ check: "apiConnection", status: "FAIL", detail: `Connection failed: ${e.message}` });
4184
+ }
4185
+ } else if (userToken && userToken.expiresAt > Date.now() / 1e3) {
4186
+ try {
4187
+ const res = await fetch("https://www.worksapis.com/v1.0/users/me", {
4188
+ headers: { Authorization: `Bearer ${userToken.accessToken}` }
4189
+ });
4190
+ if (res.ok) {
4191
+ results.push({ check: "apiConnection", status: "OK", detail: "Connected to NAVER WORKS API" });
4192
+ } else {
4193
+ results.push({ check: "apiConnection", status: "FAIL", detail: `HTTP ${res.status}` });
4194
+ }
4195
+ } catch (e) {
4196
+ results.push({ check: "apiConnection", status: "FAIL", detail: `Connection failed: ${e.message}` });
4197
+ }
4198
+ } else {
4199
+ results.push({ check: "apiConnection", status: "SKIP", detail: "No valid token \u2014 skipping API test" });
4200
+ }
4201
+ return results;
4202
+ }
4203
+ var doctorCommand = new Command12("doctor").description("Check nworks configuration and connectivity").option("--profile <name>", "Profile name", "default").option("--json", "JSON output").action(async (opts) => {
4204
+ try {
4205
+ const results = await runChecks(opts.profile);
4206
+ if (opts.json || !process.stdout.isTTY) {
4207
+ output(results, opts);
4208
+ } else {
4209
+ console.log("\n nworks doctor\n");
4210
+ for (const r of results) {
4211
+ const icon = r.status === "OK" ? "\u2705" : r.status === "SKIP" ? "\u2796" : r.status === "WARN" ? "\u26A0\uFE0F" : "\u274C";
4212
+ console.log(` ${icon} ${r.check.padEnd(16)} ${r.detail}`);
4213
+ }
4214
+ console.log();
4215
+ }
4216
+ if (results.some((r) => r.status === "FAIL")) {
4217
+ process.exitCode = 1;
4218
+ }
4219
+ } catch (err) {
4220
+ cliError(err, opts);
4221
+ }
4222
+ });
4223
+
4224
+ // src/mcp/tools/diagnostics.ts
4225
+ function registerDiagnosticsTools(server) {
4226
+ defineTool(
4227
+ server,
3319
4228
  "nworks_doctor",
3320
4229
  "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
4230
  {},
@@ -3348,6 +4257,21 @@ OAuth Redirect URI: http://localhost:9876/callback`,
3348
4257
  );
3349
4258
  }
3350
4259
 
4260
+ // src/mcp/tools.ts
4261
+ function registerTools(server) {
4262
+ registerSetupTools(server);
4263
+ registerMessageTools(server);
4264
+ registerDirectoryTools(server);
4265
+ registerCalendarTools(server);
4266
+ registerDriveTools(server);
4267
+ registerMailTools(server);
4268
+ registerTaskTools(server);
4269
+ registerBoardTools(server);
4270
+ registerContactTools(server);
4271
+ registerAuthTools(server);
4272
+ registerDiagnosticsTools(server);
4273
+ }
4274
+
3351
4275
  // src/mcp/server.ts
3352
4276
  async function startMcpServer() {
3353
4277
  try {
@@ -3368,7 +4292,7 @@ async function startMcpServer() {
3368
4292
  }
3369
4293
  const server = new McpServer({
3370
4294
  name: "nworks",
3371
- version: "1.0.0"
4295
+ version: VERSION
3372
4296
  });
3373
4297
  registerTools(server);
3374
4298
  const transport = new StdioServerTransport();
@@ -3376,22 +4300,31 @@ async function startMcpServer() {
3376
4300
  }
3377
4301
 
3378
4302
  // 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) => {
4303
+ function listRegisteredTools() {
4304
+ const server = new McpServer2({ name: "nworks", version: "0.0.0" });
4305
+ registerTools(server);
4306
+ const registered = server._registeredTools;
4307
+ return Object.entries(registered).map(([name, tool]) => ({
4308
+ name,
4309
+ summary: (tool.description ?? "").split("\n")[0]?.trim() ?? ""
4310
+ }));
4311
+ }
4312
+ var mcpCommand = new Command13("mcp").description("Start MCP server (stdio transport)").option("--list-tools", "List available MCP tools").action(async (opts) => {
3380
4313
  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");
4314
+ const tools = listRegisteredTools();
4315
+ const width = tools.length ? Math.max(...tools.map((t) => t.name.length)) : 0;
4316
+ for (const { name, summary } of tools) {
4317
+ console.log(`${name.padEnd(width)} ${summary}`);
4318
+ }
4319
+ console.log(`
4320
+ ${tools.length} tools`);
3386
4321
  return;
3387
4322
  }
3388
4323
  await startMcpServer();
3389
4324
  });
3390
4325
 
3391
4326
  // 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");
4327
+ var program = new Command14().name("nworks").description("NAVER WORKS CLI \u2014 built for humans and AI agents").version(VERSION);
3395
4328
  program.addCommand(loginCommand);
3396
4329
  program.addCommand(logoutCommand);
3397
4330
  program.addCommand(whoamiCommand);
@@ -3402,6 +4335,7 @@ program.addCommand(driveCommand);
3402
4335
  program.addCommand(mailCommand);
3403
4336
  program.addCommand(taskCommand);
3404
4337
  program.addCommand(boardCommand);
4338
+ program.addCommand(contactCommand);
3405
4339
  program.addCommand(mcpCommand);
3406
4340
  program.addCommand(doctorCommand);
3407
4341
  program.parse();