nworks 1.2.2 → 1.3.1

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