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