@wsh19991219/mcp-server 0.1.2 → 0.1.3
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.md +1 -1
- package/lib/config.js +9 -3
- package/lib/login-server.js +67 -20
- package/lib/tools.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ npm install -g @data_mgr/mcp-server
|
|
|
74
74
|
|
|
75
75
|
| 变量 | 说明 | 默认值 |
|
|
76
76
|
| --- | --- | --- |
|
|
77
|
-
| `DATA_MGR_API_URL` | 后端 API 地址 | `http://
|
|
77
|
+
| `DATA_MGR_API_URL` | 后端 API 地址 | `http://42.194.226.85:8092` |
|
|
78
78
|
| `DATA_MGR_CONFIG_DIR` | 配置文件目录 | `~/.data-mgr` |
|
|
79
79
|
|
|
80
80
|
## 使用示例
|
package/lib/config.js
CHANGED
|
@@ -8,9 +8,15 @@ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
|
8
8
|
|
|
9
9
|
/** @typedef {{ apiUrl?: string, token?: string, username?: string, userId?: number, tenantId?: number, expiresIn?: string, expiresAt?: string, loggedInAt?: string }} DataMgrConfig */
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
/** Production API (port 8092 serves plain HTTP; use HTTPS only via reverse proxy on 443). */
|
|
12
|
+
export const DEFAULT_API_URL = "http://42.194.226.85:8092";
|
|
12
13
|
export const LOGIN_PATH = "/api/v1/auth/login";
|
|
13
14
|
|
|
15
|
+
/** @param {string} url */
|
|
16
|
+
export function normalizeApiUrl(url) {
|
|
17
|
+
return String(url).trim().replace(/\/$/, "");
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
/** @param {string | number | undefined} expiresIn @param {Date} [from] */
|
|
15
21
|
export function computeExpiresAt(expiresIn, from = new Date()) {
|
|
16
22
|
if (expiresIn === undefined || expiresIn === null || expiresIn === "") return undefined;
|
|
@@ -68,9 +74,9 @@ export function getConfigPath() {
|
|
|
68
74
|
/** @returns {string} */
|
|
69
75
|
export function getApiUrl() {
|
|
70
76
|
const fromEnv = process.env.DATA_MGR_API_URL?.trim();
|
|
71
|
-
if (fromEnv) return fromEnv
|
|
77
|
+
if (fromEnv) return normalizeApiUrl(fromEnv);
|
|
72
78
|
const cfg = loadConfig();
|
|
73
|
-
if (cfg.apiUrl) return cfg.apiUrl
|
|
79
|
+
if (cfg.apiUrl) return normalizeApiUrl(cfg.apiUrl);
|
|
74
80
|
return DEFAULT_API_URL;
|
|
75
81
|
}
|
|
76
82
|
|
package/lib/login-server.js
CHANGED
|
@@ -3,6 +3,40 @@ import { randomBytes } from "node:crypto";
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import { getApiUrl, LOGIN_PATH } from "./config.js";
|
|
5
5
|
|
|
6
|
+
const CORS_HEADERS = {
|
|
7
|
+
"Access-Control-Allow-Origin": "*",
|
|
8
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
9
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** @param {import('node:http').ServerResponse} res @param {number} status @param {Record<string, unknown>} body */
|
|
13
|
+
function sendJson(res, status, body) {
|
|
14
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", ...CORS_HEADERS });
|
|
15
|
+
res.end(JSON.stringify(body));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** @param {unknown} err @param {string} apiUrl */
|
|
19
|
+
function formatUpstreamError(err, apiUrl) {
|
|
20
|
+
const base = err instanceof Error ? err : new Error(String(err));
|
|
21
|
+
const cause = base.cause instanceof Error ? base.cause : null;
|
|
22
|
+
const code = /** @type {{ code?: string }} */ (cause ?? base).code;
|
|
23
|
+
const msg = cause?.message ?? base.message;
|
|
24
|
+
|
|
25
|
+
if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "ETIMEDOUT") {
|
|
26
|
+
return `无法连接 API(${apiUrl}):${msg}`;
|
|
27
|
+
}
|
|
28
|
+
if (/packet length too long/i.test(msg)) {
|
|
29
|
+
return `协议不匹配:${apiUrl} 使用了 HTTPS,但该端口可能只提供 HTTP。请改为 http:// 或配置 443 反向代理。`;
|
|
30
|
+
}
|
|
31
|
+
if (/certificate|UNABLE_TO_VERIFY|self signed|ALTNAME_INVALID/i.test(msg)) {
|
|
32
|
+
return `HTTPS 证书校验失败(${apiUrl})。请使用与证书匹配的域名,或配置 NODE_EXTRA_CA_CERTS。`;
|
|
33
|
+
}
|
|
34
|
+
if (base.name === "TypeError" && /fetch failed/i.test(base.message)) {
|
|
35
|
+
return `请求 API 失败(${apiUrl}):${msg || base.message}`;
|
|
36
|
+
}
|
|
37
|
+
return msg || base.message;
|
|
38
|
+
}
|
|
39
|
+
|
|
6
40
|
const LOGIN_PAGE = String.raw`<!DOCTYPE html>
|
|
7
41
|
<html lang="zh-CN">
|
|
8
42
|
<head>
|
|
@@ -130,14 +164,14 @@ const SUCCESS_PAGE = String.raw`<!DOCTYPE html>
|
|
|
130
164
|
/**
|
|
131
165
|
* Start local login server, open browser, return token when login succeeds.
|
|
132
166
|
* Modified for MCP: uses onLoginUrl callback instead of console.log.
|
|
133
|
-
* @param {{ timeoutMs?: number, openBrowser?: boolean, onLoginUrl?: (url: string) => void }} [options]
|
|
167
|
+
* @param {{ timeoutMs?: number, openBrowser?: boolean, apiUrl?: string, onLoginUrl?: (url: string) => void }} [options]
|
|
134
168
|
* @returns {Promise<LoginResult>}
|
|
135
169
|
*/
|
|
136
170
|
export function runBrowserLogin(options = {}) {
|
|
137
171
|
const timeoutMs = options.timeoutMs ?? 5 * 60 * 1000;
|
|
138
172
|
const openBrowser = options.openBrowser !== false;
|
|
139
173
|
const onLoginUrl = options.onLoginUrl ?? (() => {});
|
|
140
|
-
const apiUrl = getApiUrl();
|
|
174
|
+
const apiUrl = options.apiUrl ?? getApiUrl();
|
|
141
175
|
const state = randomBytes(16).toString("hex");
|
|
142
176
|
|
|
143
177
|
return new Promise((resolve, reject) => {
|
|
@@ -154,6 +188,12 @@ export function runBrowserLogin(options = {}) {
|
|
|
154
188
|
server = http.createServer(async (req, res) => {
|
|
155
189
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
156
190
|
|
|
191
|
+
if (req.method === "OPTIONS" && url.pathname === "/api/login") {
|
|
192
|
+
res.writeHead(204, CORS_HEADERS);
|
|
193
|
+
res.end();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
157
197
|
if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/login")) {
|
|
158
198
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
159
199
|
res.end(LOGIN_PAGE.replace("__API_URL__", apiUrl));
|
|
@@ -165,26 +205,34 @@ export function runBrowserLogin(options = {}) {
|
|
|
165
205
|
req.on("data", (chunk) => { body += chunk; });
|
|
166
206
|
req.on("end", async () => {
|
|
167
207
|
try {
|
|
168
|
-
const { username, password } = JSON.parse(body);
|
|
208
|
+
const { username, password } = JSON.parse(body || "{}");
|
|
169
209
|
if (!username || !password) {
|
|
170
|
-
res
|
|
171
|
-
res.end(JSON.stringify({ error: "username and password required" }));
|
|
210
|
+
sendJson(res, 400, { error: "username and password required" });
|
|
172
211
|
return;
|
|
173
212
|
}
|
|
174
213
|
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
214
|
+
const loginEndpoint = `${apiUrl}${LOGIN_PATH}`;
|
|
215
|
+
let loginRes;
|
|
216
|
+
try {
|
|
217
|
+
loginRes = await fetch(loginEndpoint, {
|
|
218
|
+
method: "POST",
|
|
219
|
+
headers: { "Content-Type": "application/json" },
|
|
220
|
+
body: JSON.stringify({ username, password }),
|
|
221
|
+
});
|
|
222
|
+
} catch (fetchErr) {
|
|
223
|
+
sendJson(res, 502, {
|
|
224
|
+
error: formatUpstreamError(fetchErr, apiUrl),
|
|
225
|
+
message: formatUpstreamError(fetchErr, apiUrl),
|
|
226
|
+
});
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
180
229
|
|
|
181
230
|
const data = await loginRes.json().catch(() => ({}));
|
|
182
231
|
|
|
183
232
|
// API returns {code, message, data} — check code for errors
|
|
184
233
|
if (!loginRes.ok || (data.code != null && data.code !== 0)) {
|
|
185
234
|
const message = data.message ?? data.error ?? `Login failed (HTTP ${loginRes.status}, code ${data.code})`;
|
|
186
|
-
res
|
|
187
|
-
res.end(JSON.stringify({ error: message }));
|
|
235
|
+
sendJson(res, loginRes.ok ? 401 : loginRes.status, { error: message, message });
|
|
188
236
|
return;
|
|
189
237
|
}
|
|
190
238
|
|
|
@@ -192,16 +240,15 @@ export function runBrowserLogin(options = {}) {
|
|
|
192
240
|
const inner = data.data ?? {};
|
|
193
241
|
const token = data.token ?? inner.token ?? inner.access_token;
|
|
194
242
|
if (!token) {
|
|
195
|
-
res
|
|
196
|
-
res.end(JSON.stringify({
|
|
243
|
+
sendJson(res, 502, {
|
|
197
244
|
error: "API response missing token field",
|
|
245
|
+
message: "API response missing token field",
|
|
198
246
|
debug: { topKeys: Object.keys(data), innerKeys: Object.keys(inner) },
|
|
199
|
-
})
|
|
247
|
+
});
|
|
200
248
|
return;
|
|
201
249
|
}
|
|
202
250
|
|
|
203
|
-
res
|
|
204
|
-
res.end(JSON.stringify({ ok: true }));
|
|
251
|
+
sendJson(res, 200, { ok: true });
|
|
205
252
|
|
|
206
253
|
const loggedInAt = new Date().toISOString();
|
|
207
254
|
const expiresIn = (inner.expires_in ?? data.expires_in) != null
|
|
@@ -218,9 +265,9 @@ export function runBrowserLogin(options = {}) {
|
|
|
218
265
|
loginUrl: `http://127.0.0.1:${server?.address()?.port ?? "?"}/login`,
|
|
219
266
|
});
|
|
220
267
|
} catch (err) {
|
|
221
|
-
const message = err
|
|
222
|
-
|
|
223
|
-
res
|
|
268
|
+
const message = formatUpstreamError(err, apiUrl);
|
|
269
|
+
console.error("[data-mgr login]", message, err);
|
|
270
|
+
sendJson(res, 500, { error: message, message });
|
|
224
271
|
}
|
|
225
272
|
});
|
|
226
273
|
return;
|
package/lib/tools.js
CHANGED
|
@@ -184,6 +184,7 @@ export function registerTools(server) {
|
|
|
184
184
|
const expiresAt = computeExpiresAt(result.expiresIn, new Date(result.loggedInAt));
|
|
185
185
|
|
|
186
186
|
saveConfig({
|
|
187
|
+
apiUrl: getApiUrl(),
|
|
187
188
|
token: result.token,
|
|
188
189
|
username: result.username,
|
|
189
190
|
userId: result.userId,
|