@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 +1 -1
- package/SKILL.md +1 -1
- package/lib/config.js +42 -9
- package/lib/login-server.js +37 -10
- package/lib/tools.js +3 -3
- 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
|
|
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
|
|
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
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return
|
|
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() {
|
package/lib/login-server.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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(
|
|
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 = `${
|
|
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,
|
|
225
|
-
message: formatUpstreamError(fetchErr,
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
187
|
+
apiUrl: result.apiUrl,
|
|
188
188
|
token: result.token,
|
|
189
189
|
username: result.username,
|
|
190
190
|
userId: result.userId,
|