@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 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://127.0.0.1:8092` |
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
- export const DEFAULT_API_URL = "http://127.0.0.1:8092";
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.replace(/\/$/, "");
77
+ if (fromEnv) return normalizeApiUrl(fromEnv);
72
78
  const cfg = loadConfig();
73
- if (cfg.apiUrl) return cfg.apiUrl.replace(/\/$/, "");
79
+ if (cfg.apiUrl) return normalizeApiUrl(cfg.apiUrl);
74
80
  return DEFAULT_API_URL;
75
81
  }
76
82
 
@@ -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.writeHead(400, { "Content-Type": "application/json" });
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 loginRes = await fetch(`${apiUrl}${LOGIN_PATH}`, {
176
- method: "POST",
177
- headers: { "Content-Type": "application/json" },
178
- body: JSON.stringify({ username, password }),
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.writeHead(loginRes.ok ? 401 : loginRes.status, { "Content-Type": "application/json" });
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.writeHead(502, { "Content-Type": "application/json" });
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.writeHead(200, { "Content-Type": "application/json" });
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 instanceof Error ? err.message : String(err);
222
- res.writeHead(500, { "Content-Type": "application/json" });
223
- res.end(JSON.stringify({ error: message }));
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wsh19991219/mcp-server",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "MCP server for Data Manager API — stdio transport, JWT auth",
5
5
  "type": "module",
6
6
  "main": "lib/tools.js",