@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 +1 -1
- package/SKILL.md +1 -1
- package/lib/config.js +45 -6
- package/lib/login-server.js +98 -24
- package/lib/tools.js +3 -2
- 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,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
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
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;
|
|
75
114
|
}
|
|
76
115
|
|
|
77
116
|
export function hasValidToken() {
|
package/lib/login-server.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
222
|
-
|
|
223
|
-
res
|
|
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
|
-
|
|
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,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,
|