@wsh19991219/mcp-server 0.1.2 → 0.1.4

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 地址(可选;未设置时在登录页填写) | |
78
78
  | `DATA_MGR_CONFIG_DIR` | 配置文件目录 | `~/.data-mgr` |
79
79
 
80
80
  ## 使用示例
package/SKILL.md CHANGED
@@ -135,7 +135,7 @@ npm install -g @data_mgr/mcp-server
135
135
 
136
136
  | 变量 | 说明 | 默认值 |
137
137
  | --- | --- | --- |
138
- | `DATA_MGR_API_URL` | 后端 API 地址 | `http://127.0.0.1:8092` |
138
+ | `DATA_MGR_API_URL` | 后端 API 地址(可选;未设置时在登录页填写) | |
139
139
  | `DATA_MGR_CONFIG_DIR` | 配置文件目录 | `~/.data-mgr` |
140
140
 
141
141
  ## 错误处理
package/lib/config.js CHANGED
@@ -8,9 +8,48 @@ 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";
12
11
  export const LOGIN_PATH = "/api/v1/auth/login";
13
12
 
13
+ /** @param {string} [url] */
14
+ export function normalizeApiUrl(url) {
15
+ return String(url ?? "").trim().replace(/\/$/, "");
16
+ }
17
+
18
+ /**
19
+ * Validate and normalize API base URL (adds http:// if scheme omitted).
20
+ * @param {string} url
21
+ * @returns {string}
22
+ */
23
+ export function parseApiUrl(url) {
24
+ let trimmed = normalizeApiUrl(url);
25
+ if (!trimmed) {
26
+ throw new Error("API 地址不能为空");
27
+ }
28
+ if (!/^https?:\/\//i.test(trimmed)) {
29
+ trimmed = `http://${trimmed}`;
30
+ }
31
+ let u;
32
+ try {
33
+ u = new URL(trimmed);
34
+ } catch {
35
+ throw new Error(`无效的 API 地址: ${url}`);
36
+ }
37
+ if (u.protocol !== "http:" && u.protocol !== "https:") {
38
+ throw new Error("仅支持 http 或 https");
39
+ }
40
+ const path = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
41
+ return u.origin + path;
42
+ }
43
+
44
+ /** Saved API URL from env or config (no default). */
45
+ export function getSavedApiUrl() {
46
+ const fromEnv = process.env.DATA_MGR_API_URL?.trim();
47
+ if (fromEnv) return normalizeApiUrl(fromEnv);
48
+ const cfg = loadConfig();
49
+ if (cfg.apiUrl) return normalizeApiUrl(cfg.apiUrl);
50
+ return undefined;
51
+ }
52
+
14
53
  /** @param {string | number | undefined} expiresIn @param {Date} [from] */
15
54
  export function computeExpiresAt(expiresIn, from = new Date()) {
16
55
  if (expiresIn === undefined || expiresIn === null || expiresIn === "") return undefined;
@@ -67,11 +106,11 @@ export function getConfigPath() {
67
106
 
68
107
  /** @returns {string} */
69
108
  export function getApiUrl() {
70
- const fromEnv = process.env.DATA_MGR_API_URL?.trim();
71
- if (fromEnv) return fromEnv.replace(/\/$/, "");
72
- const cfg = loadConfig();
73
- if (cfg.apiUrl) return cfg.apiUrl.replace(/\/$/, "");
74
- return DEFAULT_API_URL;
109
+ const url = getSavedApiUrl();
110
+ if (!url) {
111
+ throw new Error("未配置 API 地址。请运行 login 在登录页填写服务器地址,或设置 DATA_MGR_API_URL / config api-url。");
112
+ }
113
+ return url;
75
114
  }
76
115
 
77
116
  export function hasValidToken() {
@@ -1,7 +1,46 @@
1
1
  import http from "node:http";
2
2
  import { randomBytes } from "node:crypto";
3
3
  import { spawn } from "node:child_process";
4
- import { getApiUrl, LOGIN_PATH } from "./config.js";
4
+ import { getSavedApiUrl, parseApiUrl, LOGIN_PATH } from "./config.js";
5
+
6
+ /** @param {string} [defaultApiUrl] */
7
+ function renderLoginPage(defaultApiUrl) {
8
+ return LOGIN_PAGE.replace("__DEFAULT_API_URL_JSON__", JSON.stringify(defaultApiUrl ?? ""));
9
+ }
10
+
11
+ const CORS_HEADERS = {
12
+ "Access-Control-Allow-Origin": "*",
13
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
14
+ "Access-Control-Allow-Headers": "Content-Type",
15
+ };
16
+
17
+ /** @param {import('node:http').ServerResponse} res @param {number} status @param {Record<string, unknown>} body */
18
+ function sendJson(res, status, body) {
19
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", ...CORS_HEADERS });
20
+ res.end(JSON.stringify(body));
21
+ }
22
+
23
+ /** @param {unknown} err @param {string} apiUrl */
24
+ function formatUpstreamError(err, apiUrl) {
25
+ const base = err instanceof Error ? err : new Error(String(err));
26
+ const cause = base.cause instanceof Error ? base.cause : null;
27
+ const code = /** @type {{ code?: string }} */ (cause ?? base).code;
28
+ const msg = cause?.message ?? base.message;
29
+
30
+ if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "ETIMEDOUT") {
31
+ return `无法连接 API(${apiUrl}):${msg}`;
32
+ }
33
+ if (/packet length too long/i.test(msg)) {
34
+ return `协议不匹配:${apiUrl} 使用了 HTTPS,但该端口可能只提供 HTTP。请改为 http:// 或配置 443 反向代理。`;
35
+ }
36
+ if (/certificate|UNABLE_TO_VERIFY|self signed|ALTNAME_INVALID/i.test(msg)) {
37
+ return `HTTPS 证书校验失败(${apiUrl})。请使用与证书匹配的域名,或配置 NODE_EXTRA_CA_CERTS。`;
38
+ }
39
+ if (base.name === "TypeError" && /fetch failed/i.test(base.message)) {
40
+ return `请求 API 失败(${apiUrl}):${msg || base.message}`;
41
+ }
42
+ return msg || base.message;
43
+ }
5
44
 
6
45
  const LOGIN_PAGE = String.raw`<!DOCTYPE html>
7
46
  <html lang="zh-CN">
@@ -46,8 +85,11 @@ const LOGIN_PAGE = String.raw`<!DOCTYPE html>
46
85
  <body>
47
86
  <div class="card">
48
87
  <h1>Data Manager</h1>
49
- <p>请输入账号和密码,登录成功后 CLI 将自动保存 token。</p>
88
+ <p>请输入 API 地址、账号和密码。登录成功后将保存服务器地址与 token。</p>
50
89
  <form id="form">
90
+ <label for="apiUrl">API 地址</label>
91
+ <input id="apiUrl" name="apiUrl" type="url" autocomplete="url"
92
+ placeholder="http://host:8092" required />
51
93
  <label for="username">账号</label>
52
94
  <input id="username" name="username" autocomplete="username" required />
53
95
  <label for="password">密码</label>
@@ -55,12 +97,19 @@ const LOGIN_PAGE = String.raw`<!DOCTYPE html>
55
97
  <button type="submit" id="btn">登录</button>
56
98
  </form>
57
99
  <div id="msg" class="msg"></div>
58
- <div class="api">API: __API_URL__</div>
59
100
  </div>
60
101
  <script>
61
102
  const form = document.getElementById('form');
62
103
  const msg = document.getElementById('msg');
63
104
  const btn = document.getElementById('btn');
105
+ const defaultApiUrl = __DEFAULT_API_URL_JSON__;
106
+ if (defaultApiUrl) form.apiUrl.value = defaultApiUrl;
107
+ else {
108
+ try {
109
+ const last = localStorage.getItem('data-mgr-api-url');
110
+ if (last) form.apiUrl.value = last;
111
+ } catch (_) {}
112
+ }
64
113
  form.addEventListener('submit', async (e) => {
65
114
  e.preventDefault();
66
115
  msg.textContent = '';
@@ -72,6 +121,7 @@ const LOGIN_PAGE = String.raw`<!DOCTYPE html>
72
121
  method: 'POST',
73
122
  headers: { 'Content-Type': 'application/json' },
74
123
  body: JSON.stringify({
124
+ apiUrl: form.apiUrl.value.trim(),
75
125
  username: form.username.value,
76
126
  password: form.password.value,
77
127
  }),
@@ -80,6 +130,7 @@ const LOGIN_PAGE = String.raw`<!DOCTYPE html>
80
130
  if (!res.ok) {
81
131
  throw new Error(data.message || data.error || ('HTTP ' + res.status));
82
132
  }
133
+ try { localStorage.setItem('data-mgr-api-url', form.apiUrl.value.trim()); } catch (_) {}
83
134
  window.location.href = '/success';
84
135
  } catch (err) {
85
136
  msg.textContent = err.message || '登录失败';
@@ -124,20 +175,21 @@ const SUCCESS_PAGE = String.raw`<!DOCTYPE html>
124
175
  * @property {string} [expiresIn]
125
176
  * @property {string} [expiresAt]
126
177
  * @property {string} loggedInAt
178
+ * @property {string} apiUrl
127
179
  * @property {string} loginUrl - The URL the user should visit to log in
128
180
  */
129
181
 
130
182
  /**
131
183
  * Start local login server, open browser, return token when login succeeds.
132
184
  * Modified for MCP: uses onLoginUrl callback instead of console.log.
133
- * @param {{ timeoutMs?: number, openBrowser?: boolean, onLoginUrl?: (url: string) => void }} [options]
185
+ * @param {{ timeoutMs?: number, openBrowser?: boolean, apiUrl?: string, onLoginUrl?: (url: string) => void }} [options]
134
186
  * @returns {Promise<LoginResult>}
135
187
  */
136
188
  export function runBrowserLogin(options = {}) {
137
189
  const timeoutMs = options.timeoutMs ?? 5 * 60 * 1000;
138
190
  const openBrowser = options.openBrowser !== false;
139
191
  const onLoginUrl = options.onLoginUrl ?? (() => {});
140
- const apiUrl = getApiUrl();
192
+ const defaultApiUrl = options.apiUrl ?? getSavedApiUrl() ?? "";
141
193
  const state = randomBytes(16).toString("hex");
142
194
 
143
195
  return new Promise((resolve, reject) => {
@@ -154,9 +206,15 @@ export function runBrowserLogin(options = {}) {
154
206
  server = http.createServer(async (req, res) => {
155
207
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
156
208
 
209
+ if (req.method === "OPTIONS" && url.pathname === "/api/login") {
210
+ res.writeHead(204, CORS_HEADERS);
211
+ res.end();
212
+ return;
213
+ }
214
+
157
215
  if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/login")) {
158
216
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
159
- res.end(LOGIN_PAGE.replace("__API_URL__", apiUrl));
217
+ res.end(renderLoginPage(defaultApiUrl));
160
218
  return;
161
219
  }
162
220
 
@@ -164,27 +222,43 @@ export function runBrowserLogin(options = {}) {
164
222
  let body = "";
165
223
  req.on("data", (chunk) => { body += chunk; });
166
224
  req.on("end", async () => {
225
+ let loginApiUrl = defaultApiUrl;
167
226
  try {
168
- const { username, password } = JSON.parse(body);
227
+ const { username, password, apiUrl: rawApiUrl } = JSON.parse(body || "{}");
228
+ try {
229
+ loginApiUrl = parseApiUrl(rawApiUrl);
230
+ } catch (parseErr) {
231
+ const message = parseErr instanceof Error ? parseErr.message : String(parseErr);
232
+ sendJson(res, 400, { error: message, message });
233
+ return;
234
+ }
169
235
  if (!username || !password) {
170
- res.writeHead(400, { "Content-Type": "application/json" });
171
- res.end(JSON.stringify({ error: "username and password required" }));
236
+ sendJson(res, 400, { error: "username and password required" });
172
237
  return;
173
238
  }
174
239
 
175
- const loginRes = await fetch(`${apiUrl}${LOGIN_PATH}`, {
176
- method: "POST",
177
- headers: { "Content-Type": "application/json" },
178
- body: JSON.stringify({ username, password }),
179
- });
240
+ const loginEndpoint = `${loginApiUrl}${LOGIN_PATH}`;
241
+ let loginRes;
242
+ try {
243
+ loginRes = await fetch(loginEndpoint, {
244
+ method: "POST",
245
+ headers: { "Content-Type": "application/json" },
246
+ body: JSON.stringify({ username, password }),
247
+ });
248
+ } catch (fetchErr) {
249
+ sendJson(res, 502, {
250
+ error: formatUpstreamError(fetchErr, loginApiUrl),
251
+ message: formatUpstreamError(fetchErr, loginApiUrl),
252
+ });
253
+ return;
254
+ }
180
255
 
181
256
  const data = await loginRes.json().catch(() => ({}));
182
257
 
183
258
  // API returns {code, message, data} — check code for errors
184
259
  if (!loginRes.ok || (data.code != null && data.code !== 0)) {
185
260
  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 }));
261
+ sendJson(res, loginRes.ok ? 401 : loginRes.status, { error: message, message });
188
262
  return;
189
263
  }
190
264
 
@@ -192,16 +266,15 @@ export function runBrowserLogin(options = {}) {
192
266
  const inner = data.data ?? {};
193
267
  const token = data.token ?? inner.token ?? inner.access_token;
194
268
  if (!token) {
195
- res.writeHead(502, { "Content-Type": "application/json" });
196
- res.end(JSON.stringify({
269
+ sendJson(res, 502, {
197
270
  error: "API response missing token field",
271
+ message: "API response missing token field",
198
272
  debug: { topKeys: Object.keys(data), innerKeys: Object.keys(inner) },
199
- }));
273
+ });
200
274
  return;
201
275
  }
202
276
 
203
- res.writeHead(200, { "Content-Type": "application/json" });
204
- res.end(JSON.stringify({ ok: true }));
277
+ sendJson(res, 200, { ok: true });
205
278
 
206
279
  const loggedInAt = new Date().toISOString();
207
280
  const expiresIn = (inner.expires_in ?? data.expires_in) != null
@@ -210,6 +283,7 @@ export function runBrowserLogin(options = {}) {
210
283
  cleanup();
211
284
  resolve({
212
285
  token,
286
+ apiUrl: loginApiUrl,
213
287
  username: inner.username ?? data.username ?? username,
214
288
  userId: inner.user_id ?? data.user_id,
215
289
  tenantId: inner.tenant_id ?? data.tenant_id,
@@ -218,9 +292,9 @@ export function runBrowserLogin(options = {}) {
218
292
  loginUrl: `http://127.0.0.1:${server?.address()?.port ?? "?"}/login`,
219
293
  });
220
294
  } 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 }));
295
+ const message = formatUpstreamError(err, loginApiUrl);
296
+ console.error("[data-mgr login]", message, err);
297
+ sendJson(res, 500, { error: message, message });
224
298
  }
225
299
  });
226
300
  return;
package/lib/tools.js CHANGED
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import {
3
3
  loadConfig,
4
4
  saveConfig,
5
- getApiUrl,
5
+ getSavedApiUrl,
6
6
  hasValidToken,
7
7
  computeExpiresAt,
8
8
  getConfigPath,
@@ -125,7 +125,7 @@ export function registerTools(server) {
125
125
  {},
126
126
  async () => {
127
127
  const cfg = loadConfig();
128
- const apiUrl = getApiUrl();
128
+ const apiUrl = getSavedApiUrl() ?? "(未配置,登录时填写)";
129
129
  const loggedIn = hasValidToken();
130
130
 
131
131
  const expiresAt =
@@ -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: result.apiUrl,
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.4",
4
4
  "description": "MCP server for Data Manager API — stdio transport, JWT auth",
5
5
  "type": "module",
6
6
  "main": "lib/tools.js",