nworks 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -190
- package/README.ja.md +459 -393
- package/README.ko.md +459 -393
- package/README.md +459 -393
- package/dist/index.js +1442 -508
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +1948 -1218
- package/dist/mcp.js.map +1 -1
- package/package.json +61 -58
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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((
|
|
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 {
|
|
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
|
|
311
|
-
|
|
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:
|
|
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/
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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((
|
|
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 \
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
599
|
-
var
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
638
|
-
if (!res.ok) return
|
|
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
|
|
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
|
|
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
|
|
696
|
-
if (!res.ok) return
|
|
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
|
|
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
|
|
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
|
|
1200
|
+
const res = await userFetch(url, { method: "DELETE" }, profile);
|
|
758
1201
|
if (res.status === 204) return;
|
|
759
|
-
if (!res.ok) return
|
|
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
|
|
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
|
|
803
|
-
if (!res.ok) return
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1499
|
+
if (!uploadRes.ok) return handleUserApiError(uploadRes);
|
|
907
1500
|
return await uploadRes.json();
|
|
908
1501
|
}
|
|
909
|
-
async function
|
|
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
|
|
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
|
|
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
|
|
932
|
-
if (!downloadRes.ok) return
|
|
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
|
-
|
|
947
|
-
|
|
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
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
|
|
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]
|
|
1544
|
+
console.error(`[nworks] GET ${url}`);
|
|
974
1545
|
}
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
};
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
);
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
}
|
|
995
|
-
async function
|
|
996
|
-
const
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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 ({
|
|
1583
|
+
async ({ userId, folderId, count, cursor }) => {
|
|
1676
1584
|
try {
|
|
1677
|
-
const result = await
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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
|
-
|
|
1697
|
-
|
|
1698
|
-
"
|
|
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
|
-
|
|
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 ({
|
|
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
|
-
|
|
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
|
-
|
|
1735
|
-
|
|
1736
|
-
"
|
|
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
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
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 ({
|
|
1678
|
+
async ({ fileId, outputDir, outputName, userId }) => {
|
|
1743
1679
|
try {
|
|
1744
|
-
const result = await
|
|
1745
|
-
|
|
1746
|
-
untilDateTime,
|
|
1680
|
+
const result = await downloadFile(
|
|
1681
|
+
fileId,
|
|
1747
1682
|
userId ?? "me"
|
|
1748
1683
|
);
|
|
1749
|
-
const
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
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,
|
|
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, "
|
|
1719
|
+
content: [{ type: "text", text: mcpErrorHint(err, "drive.download") }],
|
|
1861
1720
|
isError: true
|
|
1862
1721
|
};
|
|
1863
1722
|
}
|
|
1864
1723
|
}
|
|
1865
1724
|
);
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
"
|
|
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
|
-
|
|
1871
|
-
|
|
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 ({
|
|
1733
|
+
async ({ count, cursor }) => {
|
|
1875
1734
|
try {
|
|
1876
|
-
await
|
|
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({
|
|
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, "
|
|
1741
|
+
content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_list") }],
|
|
1887
1742
|
isError: true
|
|
1888
1743
|
};
|
|
1889
1744
|
}
|
|
1890
1745
|
}
|
|
1891
1746
|
);
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
"
|
|
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
|
-
|
|
1897
|
-
folderId:
|
|
1898
|
-
count:
|
|
1899
|
-
cursor:
|
|
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 ({
|
|
1757
|
+
async ({ sharedriveId, folderId, count, cursor }) => {
|
|
1902
1758
|
try {
|
|
1903
|
-
const result = await
|
|
1904
|
-
|
|
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.
|
|
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
|
-
|
|
1986
|
-
|
|
1987
|
-
"
|
|
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
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
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
|
|
1794
|
+
async ({ sharedriveId, fileId, outputDir, outputName }) => {
|
|
1995
1795
|
try {
|
|
1996
|
-
const result = await
|
|
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:
|
|
1800
|
+
const { join: join3 } = await import("path");
|
|
2004
1801
|
const safeDir = validateLocalPath(outputDir);
|
|
2005
1802
|
const safeName = sanitizeFileName(fileName);
|
|
2006
|
-
const outPath =
|
|
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.
|
|
1832
|
+
content: [{ type: "text", text: mcpErrorHint(err, "drive.sharedrive_download") }],
|
|
2036
1833
|
isError: true
|
|
2037
1834
|
};
|
|
2038
1835
|
}
|
|
2039
1836
|
}
|
|
2040
1837
|
);
|
|
2041
|
-
|
|
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:
|
|
2046
|
-
subject:
|
|
2047
|
-
body:
|
|
2048
|
-
cc:
|
|
2049
|
-
bcc:
|
|
2050
|
-
contentType:
|
|
2051
|
-
userId:
|
|
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
|
-
|
|
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:
|
|
2080
|
-
count:
|
|
2081
|
-
cursor:
|
|
2082
|
-
isUnread:
|
|
2083
|
-
userId:
|
|
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
|
-
|
|
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:
|
|
2118
|
-
userId:
|
|
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
|
-
|
|
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:
|
|
2157
|
-
status:
|
|
2158
|
-
count:
|
|
2159
|
-
cursor:
|
|
2160
|
-
userId:
|
|
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
|
-
|
|
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:
|
|
2195
|
-
content:
|
|
2196
|
-
dueDate:
|
|
2197
|
-
categoryId:
|
|
2198
|
-
assigneeIds:
|
|
2199
|
-
userId:
|
|
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
|
-
|
|
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:
|
|
2227
|
-
status:
|
|
2228
|
-
title:
|
|
2229
|
-
content:
|
|
2230
|
-
dueDate:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
2289
|
-
cursor:
|
|
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
|
-
|
|
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:
|
|
2315
|
-
count:
|
|
2316
|
-
cursor:
|
|
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, "
|
|
2765
|
+
content: [{ type: "text", text: mcpErrorHint(err, "contact.update") }],
|
|
2335
2766
|
isError: true
|
|
2336
2767
|
};
|
|
2337
2768
|
}
|
|
2338
2769
|
}
|
|
2339
2770
|
);
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
"
|
|
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
|
-
|
|
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 ({
|
|
2778
|
+
async ({ contactId }) => {
|
|
2348
2779
|
try {
|
|
2349
|
-
|
|
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, "
|
|
2786
|
+
content: [{ type: "text", text: mcpErrorHint(err, "contact.delete") }],
|
|
2366
2787
|
isError: true
|
|
2367
2788
|
};
|
|
2368
2789
|
}
|
|
2369
2790
|
}
|
|
2370
2791
|
);
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
"
|
|
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
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
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 ({
|
|
2801
|
+
async ({ userId, count, cursor }) => {
|
|
2382
2802
|
try {
|
|
2383
|
-
const
|
|
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({
|
|
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, "
|
|
2809
|
+
content: [{ type: "text", text: mcpErrorHint(err, "contact.list_tags") }],
|
|
2396
2810
|
isError: true
|
|
2397
2811
|
};
|
|
2398
2812
|
}
|
|
2399
2813
|
}
|
|
2400
2814
|
);
|
|
2401
|
-
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
2427
|
-
const mergedScopes =
|
|
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
|
-
|
|
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
|
|
2948
|
+
const mask2 = (s) => s.length <= 4 ? "****" : `****${s.slice(-4)}`;
|
|
2476
2949
|
const info = {
|
|
2477
2950
|
serviceAccount: creds.serviceAccount ?? null,
|
|
2478
|
-
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
|
-
|
|
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
|
-
|
|
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:
|
|
3315
|
+
version: VERSION
|
|
2586
3316
|
});
|
|
2587
3317
|
registerTools(server);
|
|
2588
3318
|
const transport = new StdioServerTransport();
|