@wsh19991219/mcp-server 0.1.3 → 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://42.194.226.85: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,13 +8,46 @@ 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
- /** 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";
13
11
  export const LOGIN_PATH = "/api/v1/auth/login";
14
12
 
15
- /** @param {string} url */
13
+ /** @param {string} [url] */
16
14
  export function normalizeApiUrl(url) {
17
- return String(url).trim().replace(/\/$/, "");
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;
18
51
  }
19
52
 
20
53
  /** @param {string | number | undefined} expiresIn @param {Date} [from] */
@@ -73,11 +106,11 @@ export function getConfigPath() {
73
106
 
74
107
  /** @returns {string} */
75
108
  export function getApiUrl() {
76
- const fromEnv = process.env.DATA_MGR_API_URL?.trim();
77
- if (fromEnv) return normalizeApiUrl(fromEnv);
78
- const cfg = loadConfig();
79
- if (cfg.apiUrl) return normalizeApiUrl(cfg.apiUrl);
80
- 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;
81
114
  }
82
115
 
83
116
  export function hasValidToken() {
@@ -1,7 +1,12 @@
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
+ }
5
10
 
6
11
  const CORS_HEADERS = {
7
12
  "Access-Control-Allow-Origin": "*",
@@ -80,8 +85,11 @@ const LOGIN_PAGE = String.raw`<!DOCTYPE html>
80
85
  <body>
81
86
  <div class="card">
82
87
  <h1>Data Manager</h1>
83
- <p>请输入账号和密码,登录成功后 CLI 将自动保存 token。</p>
88
+ <p>请输入 API 地址、账号和密码。登录成功后将保存服务器地址与 token。</p>
84
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 />
85
93
  <label for="username">账号</label>
86
94
  <input id="username" name="username" autocomplete="username" required />
87
95
  <label for="password">密码</label>
@@ -89,12 +97,19 @@ const LOGIN_PAGE = String.raw`<!DOCTYPE html>
89
97
  <button type="submit" id="btn">登录</button>
90
98
  </form>
91
99
  <div id="msg" class="msg"></div>
92
- <div class="api">API: __API_URL__</div>
93
100
  </div>
94
101
  <script>
95
102
  const form = document.getElementById('form');
96
103
  const msg = document.getElementById('msg');
97
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
+ }
98
113
  form.addEventListener('submit', async (e) => {
99
114
  e.preventDefault();
100
115
  msg.textContent = '';
@@ -106,6 +121,7 @@ const LOGIN_PAGE = String.raw`<!DOCTYPE html>
106
121
  method: 'POST',
107
122
  headers: { 'Content-Type': 'application/json' },
108
123
  body: JSON.stringify({
124
+ apiUrl: form.apiUrl.value.trim(),
109
125
  username: form.username.value,
110
126
  password: form.password.value,
111
127
  }),
@@ -114,6 +130,7 @@ const LOGIN_PAGE = String.raw`<!DOCTYPE html>
114
130
  if (!res.ok) {
115
131
  throw new Error(data.message || data.error || ('HTTP ' + res.status));
116
132
  }
133
+ try { localStorage.setItem('data-mgr-api-url', form.apiUrl.value.trim()); } catch (_) {}
117
134
  window.location.href = '/success';
118
135
  } catch (err) {
119
136
  msg.textContent = err.message || '登录失败';
@@ -158,6 +175,7 @@ const SUCCESS_PAGE = String.raw`<!DOCTYPE html>
158
175
  * @property {string} [expiresIn]
159
176
  * @property {string} [expiresAt]
160
177
  * @property {string} loggedInAt
178
+ * @property {string} apiUrl
161
179
  * @property {string} loginUrl - The URL the user should visit to log in
162
180
  */
163
181
 
@@ -171,7 +189,7 @@ export function runBrowserLogin(options = {}) {
171
189
  const timeoutMs = options.timeoutMs ?? 5 * 60 * 1000;
172
190
  const openBrowser = options.openBrowser !== false;
173
191
  const onLoginUrl = options.onLoginUrl ?? (() => {});
174
- const apiUrl = options.apiUrl ?? getApiUrl();
192
+ const defaultApiUrl = options.apiUrl ?? getSavedApiUrl() ?? "";
175
193
  const state = randomBytes(16).toString("hex");
176
194
 
177
195
  return new Promise((resolve, reject) => {
@@ -196,7 +214,7 @@ export function runBrowserLogin(options = {}) {
196
214
 
197
215
  if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/login")) {
198
216
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
199
- res.end(LOGIN_PAGE.replace("__API_URL__", apiUrl));
217
+ res.end(renderLoginPage(defaultApiUrl));
200
218
  return;
201
219
  }
202
220
 
@@ -204,14 +222,22 @@ export function runBrowserLogin(options = {}) {
204
222
  let body = "";
205
223
  req.on("data", (chunk) => { body += chunk; });
206
224
  req.on("end", async () => {
225
+ let loginApiUrl = defaultApiUrl;
207
226
  try {
208
- 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
+ }
209
235
  if (!username || !password) {
210
236
  sendJson(res, 400, { error: "username and password required" });
211
237
  return;
212
238
  }
213
239
 
214
- const loginEndpoint = `${apiUrl}${LOGIN_PATH}`;
240
+ const loginEndpoint = `${loginApiUrl}${LOGIN_PATH}`;
215
241
  let loginRes;
216
242
  try {
217
243
  loginRes = await fetch(loginEndpoint, {
@@ -221,8 +247,8 @@ export function runBrowserLogin(options = {}) {
221
247
  });
222
248
  } catch (fetchErr) {
223
249
  sendJson(res, 502, {
224
- error: formatUpstreamError(fetchErr, apiUrl),
225
- message: formatUpstreamError(fetchErr, apiUrl),
250
+ error: formatUpstreamError(fetchErr, loginApiUrl),
251
+ message: formatUpstreamError(fetchErr, loginApiUrl),
226
252
  });
227
253
  return;
228
254
  }
@@ -257,6 +283,7 @@ export function runBrowserLogin(options = {}) {
257
283
  cleanup();
258
284
  resolve({
259
285
  token,
286
+ apiUrl: loginApiUrl,
260
287
  username: inner.username ?? data.username ?? username,
261
288
  userId: inner.user_id ?? data.user_id,
262
289
  tenantId: inner.tenant_id ?? data.tenant_id,
@@ -265,7 +292,7 @@ export function runBrowserLogin(options = {}) {
265
292
  loginUrl: `http://127.0.0.1:${server?.address()?.port ?? "?"}/login`,
266
293
  });
267
294
  } catch (err) {
268
- const message = formatUpstreamError(err, apiUrl);
295
+ const message = formatUpstreamError(err, loginApiUrl);
269
296
  console.error("[data-mgr login]", message, err);
270
297
  sendJson(res, 500, { error: message, message });
271
298
  }
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,7 +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
+ apiUrl: result.apiUrl,
188
188
  token: result.token,
189
189
  username: result.username,
190
190
  userId: result.userId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wsh19991219/mcp-server",
3
- "version": "0.1.3",
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",