@wsh19991219/mcp-server 0.1.3 → 0.1.6

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
@@ -4,7 +4,7 @@ Data Manager MCP Server — 通过 [MCP](https://modelcontextprotocol.io) 协议
4
4
 
5
5
  ## 功能
6
6
 
7
- - **鉴权管理**:浏览器一键登录,JWT 自动续期
7
+ - **鉴权管理**:浏览器登录(页面填写 API 地址 + 账号密码),JWT 自动保存
8
8
  - **表结构探索**:模糊搜表、查看列定义
9
9
  - **数据查询**:预定义指标 + 自定义只读 SQL
10
10
  - **多库支持**:主库及多个区域数据库 group
@@ -13,7 +13,7 @@ Data Manager MCP Server — 通过 [MCP](https://modelcontextprotocol.io) 协议
13
13
  ## 安装
14
14
 
15
15
  ```bash
16
- npm install -g @data_mgr/mcp-server
16
+ npm install -g @wang19991219/mcp-server
17
17
  ```
18
18
 
19
19
  需要 **Node.js 18+**。
@@ -53,29 +53,47 @@ npm install -g @data_mgr/mcp-server
53
53
 
54
54
  ## MCP 工具
55
55
 
56
- | 工具 | 功能 | 说明 |
57
- | --- | --- | --- |
58
- | `check_auth` | 检查登录状态 | 返回用户名、token 有效期 |
59
- | `login` | 浏览器登录 | 打开浏览器完成 OAuth 登录 |
60
- | `analytics_query` | 预定义指标查询 | 支持 metric、filters、limit 等参数 |
61
- | `api_request` | 通用 REST 请求 | GET/POST/PUT/DELETE,含自定义 SQL |
56
+
57
+ | 工具 | 功能 | 说明 |
58
+ | ----------------- | ---------- | -------------------------------- |
59
+ | `check_auth` | 检查登录状态 | 返回用户名、token 有效期 |
60
+ | `login` | 浏览器登录 | 在页面填写 **API 地址**、账号、密码(无硬编码默认地址) |
61
+ | `analytics_query` | 预定义指标查询 | 支持 metric、filters、limit 等参数 |
62
+ | `api_request` | 通用 REST 请求 | GET/POST/PUT/DELETE,含自定义 SQL |
63
+
62
64
 
63
65
  ### api_request 端点
64
66
 
65
- | 端点 | 方法 | 用途 |
66
- | --- | --- | --- |
67
- | `/health` | GET | 健康检查,返回可用数据库 group |
68
- | `/api/v1/analytics/catalog` | GET | 列出可用指标和数据库 group |
69
- | `/api/v1/analytics/schema?q=<关键词>` | GET | 模糊搜索表名 |
70
- | `/api/v1/analytics/schema?tables=<表名>` | GET | 查看表的列结构 |
71
- | `/api/v1/analytics/sql` | POST | 执行自定义只读 SQL |
67
+
68
+ | 端点 | 方法 | 用途 |
69
+ | -------------------------------------- | ---- | ------------------ |
70
+ | `/health` | GET | 健康检查,返回可用数据库 group |
71
+ | `/api/v1/analytics/catalog` | GET | 列出可用指标和数据库 group |
72
+ | `/api/v1/analytics/schema?q=<关键词>` | GET | 模糊搜索表名 |
73
+ | `/api/v1/analytics/schema?tables=<表名>` | GET | 查看表的列结构 |
74
+ | `/api/v1/analytics/sql` | POST | 执行自定义只读 SQL |
75
+
72
76
 
73
77
  ## 环境变量
74
78
 
75
- | 变量 | 说明 | 默认值 |
76
- | --- | --- | --- |
77
- | `DATA_MGR_API_URL` | 后端 API 地址 | `http://42.194.226.85:8092` |
78
- | `DATA_MGR_CONFIG_DIR` | 配置文件目录 | `~/.data-mgr` |
79
+
80
+ | 变量 | 说明 | 默认值 |
81
+ | --------------------- | ------------------------ | ------------- |
82
+ | `DATA_MGR_API_URL` | 后端 API 地址(可选;未设置时在登录页填写) | — |
83
+ | `DATA_MGR_CONFIG_DIR` | 配置文件目录 | `~/.data-mgr` |
84
+
85
+
86
+ ## 登录说明
87
+
88
+ 1. 调用 MCP 工具 `login`(或对话中说「登录 data-mgr」)
89
+ 2. 浏览器打开本地登录页,填写:
90
+ - **API 地址**:如 `http://your-host:8092`(8092 端口一般为 HTTP)
91
+ - **账号 / 密码**
92
+ 3. 成功后写入 `~/.data-mgr/config.json`(含 `apiUrl` 与 `token`)
93
+
94
+ 也可预先设置环境变量 `DATA_MGR_API_URL`,登录页会自动填充。
95
+
96
+ **若登录页仍是旧版(无 API 地址框)**:在 Cursor 中 **Settings → MCP → 重启 data-mgr**,并 **Ctrl+F5** 刷新登录页;或执行 `npm install -g ./data-mgr-mcp-server` 更新全局包。
79
97
 
80
98
  ## 使用示例
81
99
 
@@ -88,11 +106,13 @@ npm install -g @data_mgr/mcp-server
88
106
 
89
107
  ## 相关包
90
108
 
91
- | 包名 | 用途 |
92
- | --- | --- |
93
- | [`@data_mgr/cli`](https://www.npmjs.com/package/@data_mgr/cli) | Data Manager CLI,适用于 Cursor Agent |
94
- | [`weather-query-cli`](https://www.npmjs.com/package/weather-query-cli) | 天气查询 CLI |
109
+
110
+ | 包名 | 用途 |
111
+ | ---------------------------------------------------------------------- | --------------------------------- |
112
+ | `[@data_mgr/cli](https://www.npmjs.com/package/@data_mgr/cli)` | Data Manager CLI,适用于 Cursor Agent |
113
+ | `[weather-query-cli](https://www.npmjs.com/package/weather-query-cli)` | 天气查询 CLI |
114
+
95
115
 
96
116
  ## License
97
117
 
98
- MIT
118
+ MIT
package/SKILL.md CHANGED
@@ -15,14 +15,14 @@ description: >-
15
15
 
16
16
  1. 全局安装:`npm install -g @data_mgr/mcp-server`
17
17
  2. 注册 MCP 服务(见下方 [注册](#注册到-claude-code) 章节)
18
- 3. 对话中触发登录:Agent 会自动调用 `login` 工具打开浏览器
18
+ 3. 对话中触发登录:Agent 调用 `login`,在浏览器页填写 **API 地址**、账号、密码(无内置默认地址)
19
19
 
20
20
  ## MCP 工具
21
21
 
22
22
  | 工具 | 功能 | 关键参数 |
23
23
  | --- | --- | --- |
24
24
  | `check_auth` | 检查登录状态,返回 username / token 有效期 | 无 |
25
- | `login` | 浏览器登录获取 JWT | `openBrowser`: bool(默认 true) |
25
+ | `login` | 浏览器登录(填写 API 地址 + 账号密码) | `openBrowser`: bool(默认 true) |
26
26
  | `analytics_query` | 查询预定义指标 | `metric`(必填), `db`, `params`, `filters`, `limit` |
27
27
  | `api_request` | 通用 REST 请求(GET/POST/PUT/DELETE) | `method`(必填), `path`(必填), `body`, `query` |
28
28
 
@@ -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
  ## 错误处理
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Standalone login subprocess — always loads latest login-server.js (avoids MCP ESM cache).
4
+ * Usage: node bin/data-mgr-login.js [--no-open]
5
+ * Prints one JSON line to stdout on success.
6
+ */
7
+ import { runBrowserLogin } from "../lib/login-server.js";
8
+
9
+ const noOpen = process.argv.includes("--no-open");
10
+
11
+ try {
12
+ const result = await runBrowserLogin({
13
+ openBrowser: !noOpen,
14
+ timeoutMs: 5 * 60 * 1000,
15
+ });
16
+ process.stdout.write(`${JSON.stringify(result)}\n`);
17
+ } catch (err) {
18
+ const message = err instanceof Error ? err.message : String(err);
19
+ process.stderr.write(`${message}\n`);
20
+ process.exit(1);
21
+ }
@@ -6,7 +6,7 @@ import { registerTools } from "../lib/tools.js";
6
6
 
7
7
  const server = new McpServer({
8
8
  name: "data-mgr",
9
- version: "0.1.0",
9
+ version: "0.1.6",
10
10
  });
11
11
 
12
12
  registerTools(server);
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 pathSuffix = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
41
+ return u.origin + pathSuffix;
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() {
@@ -0,0 +1,101 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Data Manager 登录</title>
7
+ <style>
8
+ * { box-sizing: border-box; }
9
+ body {
10
+ margin: 0; min-height: 100vh; display: grid; place-items: center;
11
+ font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
12
+ background: linear-gradient(135deg, #0f172a, #1e3a5f);
13
+ color: #e2e8f0;
14
+ }
15
+ .card {
16
+ width: min(420px, 92vw); padding: 2rem; border-radius: 16px;
17
+ background: rgba(15, 23, 42, 0.9); border: 1px solid #334155;
18
+ box-shadow: 0 20px 50px rgba(0,0,0,.35);
19
+ }
20
+ h1 { margin: 0 0 .25rem; font-size: 1.35rem; }
21
+ p { margin: 0 0 1.5rem; color: #94a3b8; font-size: .9rem; }
22
+ label { display: block; margin-bottom: .35rem; font-size: .85rem; color: #cbd5e1; }
23
+ input {
24
+ width: 100%; padding: .65rem .75rem; margin-bottom: 1rem;
25
+ border: 1px solid #475569; border-radius: 8px; background: #0f172a; color: #f8fafc;
26
+ font-size: 1rem;
27
+ }
28
+ input:focus { outline: 2px solid #3b82f6; border-color: #3b82f6; }
29
+ button {
30
+ width: 100%; padding: .75rem; border: none; border-radius: 8px;
31
+ background: #2563eb; color: #fff; font-size: 1rem; font-weight: 600; cursor: pointer;
32
+ }
33
+ button:hover { background: #1d4ed8; }
34
+ button:disabled { opacity: .6; cursor: not-allowed; }
35
+ .msg { margin-top: 1rem; font-size: .9rem; min-height: 1.25rem; }
36
+ .err { color: #f87171; }
37
+ .ok { color: #4ade80; }
38
+ </style>
39
+ </head>
40
+ <body>
41
+ <div class="card">
42
+ <h1>Data Manager</h1>
43
+ <p>请输入 API 地址、账号和密码。登录成功后将保存服务器地址与 token。</p>
44
+ <form id="form">
45
+ <label for="apiUrl">API 地址</label>
46
+ <input id="apiUrl" name="apiUrl" type="text" inputmode="url" spellcheck="false"
47
+ placeholder="http://host:8092" required autofocus />
48
+ <label for="username">账号</label>
49
+ <input id="username" name="username" autocomplete="username" required />
50
+ <label for="password">密码</label>
51
+ <input id="password" name="password" type="password" autocomplete="current-password" required />
52
+ <button type="submit" id="btn">登录</button>
53
+ </form>
54
+ <div id="msg" class="msg"></div>
55
+ </div>
56
+ <script>
57
+ const form = document.getElementById('form');
58
+ const msg = document.getElementById('msg');
59
+ const btn = document.getElementById('btn');
60
+ const apiUrlInput = document.getElementById('apiUrl');
61
+ const defaultApiUrl = __DEFAULT_API_URL_JSON__;
62
+ if (defaultApiUrl) apiUrlInput.value = defaultApiUrl;
63
+ else {
64
+ try {
65
+ const last = localStorage.getItem('data-mgr-api-url');
66
+ if (last) apiUrlInput.value = last;
67
+ } catch (_) {}
68
+ }
69
+ form.addEventListener('submit', async (e) => {
70
+ e.preventDefault();
71
+ msg.textContent = '';
72
+ msg.className = 'msg';
73
+ btn.disabled = true;
74
+ btn.textContent = '登录中…';
75
+ const apiUrl = apiUrlInput.value.trim();
76
+ try {
77
+ const res = await fetch('/api/login', {
78
+ method: 'POST',
79
+ headers: { 'Content-Type': 'application/json' },
80
+ body: JSON.stringify({
81
+ apiUrl,
82
+ username: document.getElementById('username').value,
83
+ password: document.getElementById('password').value,
84
+ }),
85
+ });
86
+ const data = await res.json().catch(() => ({}));
87
+ if (!res.ok) {
88
+ throw new Error(data.message || data.error || ('HTTP ' + res.status));
89
+ }
90
+ try { localStorage.setItem('data-mgr-api-url', apiUrl); } catch (_) {}
91
+ window.location.href = '/success';
92
+ } catch (err) {
93
+ msg.textContent = err.message || '登录失败';
94
+ msg.className = 'msg err';
95
+ btn.disabled = false;
96
+ btn.textContent = '登录';
97
+ }
98
+ });
99
+ </script>
100
+ </body>
101
+ </html>
@@ -1,7 +1,13 @@
1
1
  import http from "node:http";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
2
5
  import { randomBytes } from "node:crypto";
3
6
  import { spawn } from "node:child_process";
4
- import { getApiUrl, LOGIN_PATH } from "./config.js";
7
+ import { getSavedApiUrl, parseApiUrl, LOGIN_PATH } from "./config.js";
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const LOGIN_PAGE_PATH = path.join(__dirname, "login-page.html");
5
11
 
6
12
  const CORS_HEADERS = {
7
13
  "Access-Control-Allow-Origin": "*",
@@ -9,6 +15,17 @@ const CORS_HEADERS = {
9
15
  "Access-Control-Allow-Headers": "Content-Type",
10
16
  };
11
17
 
18
+ const NO_CACHE_HEADERS = {
19
+ "Cache-Control": "no-store, no-cache, must-revalidate",
20
+ Pragma: "no-cache",
21
+ };
22
+
23
+ /** @param {string} [defaultApiUrl] */
24
+ function renderLoginPage(defaultApiUrl) {
25
+ const template = fs.readFileSync(LOGIN_PAGE_PATH, "utf8");
26
+ return template.replace("__DEFAULT_API_URL_JSON__", JSON.stringify(defaultApiUrl ?? ""));
27
+ }
28
+
12
29
  /** @param {import('node:http').ServerResponse} res @param {number} status @param {Record<string, unknown>} body */
13
30
  function sendJson(res, status, body) {
14
31
  res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", ...CORS_HEADERS });
@@ -37,95 +54,6 @@ function formatUpstreamError(err, apiUrl) {
37
54
  return msg || base.message;
38
55
  }
39
56
 
40
- const LOGIN_PAGE = String.raw`<!DOCTYPE html>
41
- <html lang="zh-CN">
42
- <head>
43
- <meta charset="UTF-8" />
44
- <meta name="viewport" content="width=device-width, initial-scale=1" />
45
- <title>Data Manager 登录</title>
46
- <style>
47
- * { box-sizing: border-box; }
48
- body {
49
- margin: 0; min-height: 100vh; display: grid; place-items: center;
50
- font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
51
- background: linear-gradient(135deg, #0f172a, #1e3a5f);
52
- color: #e2e8f0;
53
- }
54
- .card {
55
- width: min(400px, 92vw); padding: 2rem; border-radius: 16px;
56
- background: rgba(15, 23, 42, 0.9); border: 1px solid #334155;
57
- box-shadow: 0 20px 50px rgba(0,0,0,.35);
58
- }
59
- h1 { margin: 0 0 .25rem; font-size: 1.35rem; }
60
- p { margin: 0 0 1.5rem; color: #94a3b8; font-size: .9rem; }
61
- label { display: block; margin-bottom: .35rem; font-size: .85rem; color: #cbd5e1; }
62
- input {
63
- width: 100%; padding: .65rem .75rem; margin-bottom: 1rem;
64
- border: 1px solid #475569; border-radius: 8px; background: #0f172a; color: #f8fafc;
65
- font-size: 1rem;
66
- }
67
- input:focus { outline: 2px solid #3b82f6; border-color: #3b82f6; }
68
- button {
69
- width: 100%; padding: .75rem; border: none; border-radius: 8px;
70
- background: #2563eb; color: #fff; font-size: 1rem; font-weight: 600; cursor: pointer;
71
- }
72
- button:hover { background: #1d4ed8; }
73
- button:disabled { opacity: .6; cursor: not-allowed; }
74
- .msg { margin-top: 1rem; font-size: .9rem; min-height: 1.25rem; }
75
- .err { color: #f87171; }
76
- .ok { color: #4ade80; }
77
- .api { font-size: .75rem; color: #64748b; word-break: break-all; margin-top: 1rem; }
78
- </style>
79
- </head>
80
- <body>
81
- <div class="card">
82
- <h1>Data Manager</h1>
83
- <p>请输入账号和密码,登录成功后 CLI 将自动保存 token。</p>
84
- <form id="form">
85
- <label for="username">账号</label>
86
- <input id="username" name="username" autocomplete="username" required />
87
- <label for="password">密码</label>
88
- <input id="password" name="password" type="password" autocomplete="current-password" required />
89
- <button type="submit" id="btn">登录</button>
90
- </form>
91
- <div id="msg" class="msg"></div>
92
- <div class="api">API: __API_URL__</div>
93
- </div>
94
- <script>
95
- const form = document.getElementById('form');
96
- const msg = document.getElementById('msg');
97
- const btn = document.getElementById('btn');
98
- form.addEventListener('submit', async (e) => {
99
- e.preventDefault();
100
- msg.textContent = '';
101
- msg.className = 'msg';
102
- btn.disabled = true;
103
- btn.textContent = '登录中…';
104
- try {
105
- const res = await fetch('/api/login', {
106
- method: 'POST',
107
- headers: { 'Content-Type': 'application/json' },
108
- body: JSON.stringify({
109
- username: form.username.value,
110
- password: form.password.value,
111
- }),
112
- });
113
- const data = await res.json().catch(() => ({}));
114
- if (!res.ok) {
115
- throw new Error(data.message || data.error || ('HTTP ' + res.status));
116
- }
117
- window.location.href = '/success';
118
- } catch (err) {
119
- msg.textContent = err.message || '登录失败';
120
- msg.className = 'msg err';
121
- btn.disabled = false;
122
- btn.textContent = '登录';
123
- }
124
- });
125
- </script>
126
- </body>
127
- </html>`;
128
-
129
57
  const SUCCESS_PAGE = String.raw`<!DOCTYPE html>
130
58
  <html lang="zh-CN">
131
59
  <head>
@@ -152,18 +80,17 @@ const SUCCESS_PAGE = String.raw`<!DOCTYPE html>
152
80
  /**
153
81
  * @typedef {object} LoginResult
154
82
  * @property {string} token
83
+ * @property {string} apiUrl
155
84
  * @property {string} [username]
156
85
  * @property {number} [userId]
157
86
  * @property {number} [tenantId]
158
87
  * @property {string} [expiresIn]
159
88
  * @property {string} [expiresAt]
160
89
  * @property {string} loggedInAt
161
- * @property {string} loginUrl - The URL the user should visit to log in
90
+ * @property {string} loginUrl
162
91
  */
163
92
 
164
93
  /**
165
- * Start local login server, open browser, return token when login succeeds.
166
- * Modified for MCP: uses onLoginUrl callback instead of console.log.
167
94
  * @param {{ timeoutMs?: number, openBrowser?: boolean, apiUrl?: string, onLoginUrl?: (url: string) => void }} [options]
168
95
  * @returns {Promise<LoginResult>}
169
96
  */
@@ -171,7 +98,7 @@ export function runBrowserLogin(options = {}) {
171
98
  const timeoutMs = options.timeoutMs ?? 5 * 60 * 1000;
172
99
  const openBrowser = options.openBrowser !== false;
173
100
  const onLoginUrl = options.onLoginUrl ?? (() => {});
174
- const apiUrl = options.apiUrl ?? getApiUrl();
101
+ const defaultApiUrl = options.apiUrl ?? getSavedApiUrl() ?? "";
175
102
  const state = randomBytes(16).toString("hex");
176
103
 
177
104
  return new Promise((resolve, reject) => {
@@ -195,8 +122,11 @@ export function runBrowserLogin(options = {}) {
195
122
  }
196
123
 
197
124
  if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/login")) {
198
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
199
- res.end(LOGIN_PAGE.replace("__API_URL__", apiUrl));
125
+ res.writeHead(200, {
126
+ "Content-Type": "text/html; charset=utf-8",
127
+ ...NO_CACHE_HEADERS,
128
+ });
129
+ res.end(renderLoginPage(defaultApiUrl));
200
130
  return;
201
131
  }
202
132
 
@@ -204,14 +134,22 @@ export function runBrowserLogin(options = {}) {
204
134
  let body = "";
205
135
  req.on("data", (chunk) => { body += chunk; });
206
136
  req.on("end", async () => {
137
+ let loginApiUrl = defaultApiUrl;
207
138
  try {
208
- const { username, password } = JSON.parse(body || "{}");
139
+ const { username, password, apiUrl: rawApiUrl } = JSON.parse(body || "{}");
140
+ try {
141
+ loginApiUrl = parseApiUrl(rawApiUrl);
142
+ } catch (parseErr) {
143
+ const message = parseErr instanceof Error ? parseErr.message : String(parseErr);
144
+ sendJson(res, 400, { error: message, message });
145
+ return;
146
+ }
209
147
  if (!username || !password) {
210
148
  sendJson(res, 400, { error: "username and password required" });
211
149
  return;
212
150
  }
213
151
 
214
- const loginEndpoint = `${apiUrl}${LOGIN_PATH}`;
152
+ const loginEndpoint = `${loginApiUrl}${LOGIN_PATH}`;
215
153
  let loginRes;
216
154
  try {
217
155
  loginRes = await fetch(loginEndpoint, {
@@ -221,22 +159,20 @@ export function runBrowserLogin(options = {}) {
221
159
  });
222
160
  } catch (fetchErr) {
223
161
  sendJson(res, 502, {
224
- error: formatUpstreamError(fetchErr, apiUrl),
225
- message: formatUpstreamError(fetchErr, apiUrl),
162
+ error: formatUpstreamError(fetchErr, loginApiUrl),
163
+ message: formatUpstreamError(fetchErr, loginApiUrl),
226
164
  });
227
165
  return;
228
166
  }
229
167
 
230
168
  const data = await loginRes.json().catch(() => ({}));
231
169
 
232
- // API returns {code, message, data} — check code for errors
233
170
  if (!loginRes.ok || (data.code != null && data.code !== 0)) {
234
171
  const message = data.message ?? data.error ?? `Login failed (HTTP ${loginRes.status}, code ${data.code})`;
235
172
  sendJson(res, loginRes.ok ? 401 : loginRes.status, { error: message, message });
236
173
  return;
237
174
  }
238
175
 
239
- // Token may be at data.token, data.data.token, or data.data.access_token
240
176
  const inner = data.data ?? {};
241
177
  const token = data.token ?? inner.token ?? inner.access_token;
242
178
  if (!token) {
@@ -257,6 +193,7 @@ export function runBrowserLogin(options = {}) {
257
193
  cleanup();
258
194
  resolve({
259
195
  token,
196
+ apiUrl: loginApiUrl,
260
197
  username: inner.username ?? data.username ?? username,
261
198
  userId: inner.user_id ?? data.user_id,
262
199
  tenantId: inner.tenant_id ?? data.tenant_id,
@@ -265,7 +202,7 @@ export function runBrowserLogin(options = {}) {
265
202
  loginUrl: `http://127.0.0.1:${server?.address()?.port ?? "?"}/login`,
266
203
  });
267
204
  } catch (err) {
268
- const message = formatUpstreamError(err, apiUrl);
205
+ const message = formatUpstreamError(err, loginApiUrl);
269
206
  console.error("[data-mgr login]", message, err);
270
207
  sendJson(res, 500, { error: message, message });
271
208
  }
@@ -274,7 +211,7 @@ export function runBrowserLogin(options = {}) {
274
211
  }
275
212
 
276
213
  if (req.method === "GET" && url.pathname === "/success") {
277
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
214
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", ...NO_CACHE_HEADERS });
278
215
  res.end(SUCCESS_PAGE);
279
216
  return;
280
217
  }
@@ -292,6 +229,7 @@ export function runBrowserLogin(options = {}) {
292
229
  cleanup();
293
230
  resolve({
294
231
  token,
232
+ apiUrl: defaultApiUrl,
295
233
  username: url.searchParams.get("username") ?? undefined,
296
234
  loggedInAt: new Date().toISOString(),
297
235
  loginUrl: `http://127.0.0.1:${server?.address()?.port ?? "?"}/login`,
@@ -313,8 +251,6 @@ export function runBrowserLogin(options = {}) {
313
251
  }
314
252
 
315
253
  const loginUrl = `http://127.0.0.1:${addr.port}/login`;
316
-
317
- // Notify caller of the login URL (replaces console.log for MCP compatibility)
318
254
  onLoginUrl(loginUrl);
319
255
 
320
256
  if (openBrowser) {
@@ -358,9 +294,7 @@ function openUrl(url) {
358
294
  ["-NoProfile", "-Command", `Start-Process -FilePath '${url.replace(/'/g, "''")}'`],
359
295
  { detached: true, stdio: "ignore", windowsHide: true },
360
296
  ).unref();
361
- return;
362
297
  }
363
- // Browser open failed - caller already has the URL via onLoginUrl callback
364
298
  });
365
299
 
366
300
  child.unref();
package/lib/tools.js CHANGED
@@ -1,14 +1,52 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
1
4
  import { z } from "zod";
2
5
  import {
3
6
  loadConfig,
4
7
  saveConfig,
5
- getApiUrl,
8
+ getSavedApiUrl,
6
9
  hasValidToken,
7
10
  computeExpiresAt,
8
11
  getConfigPath,
9
12
  } from "./config.js";
10
13
  import { apiRequest, buildQuery } from "./api.js";
11
14
 
15
+ const LOGIN_SCRIPT = path.join(
16
+ path.dirname(fileURLToPath(import.meta.url)),
17
+ "..",
18
+ "bin",
19
+ "data-mgr-login.js",
20
+ );
21
+
22
+ /** @param {boolean} openBrowser */
23
+ function runLoginSubprocess(openBrowser) {
24
+ return new Promise((resolve, reject) => {
25
+ const args = [LOGIN_SCRIPT];
26
+ if (!openBrowser) args.push("--no-open");
27
+ const child = spawn(process.execPath, args, {
28
+ stdio: ["ignore", "pipe", "pipe"],
29
+ windowsHide: true,
30
+ });
31
+ let stdout = "";
32
+ let stderr = "";
33
+ child.stdout.on("data", (chunk) => { stdout += chunk; });
34
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
35
+ child.on("error", reject);
36
+ child.on("close", (code) => {
37
+ if (code !== 0) {
38
+ reject(new Error(stderr.trim() || `Login exited with code ${code}`));
39
+ return;
40
+ }
41
+ try {
42
+ resolve(JSON.parse(stdout.trim()));
43
+ } catch {
44
+ reject(new Error("Invalid login response from subprocess"));
45
+ }
46
+ });
47
+ });
48
+ }
49
+
12
50
  /**
13
51
  * Register all data-mgr tools on the MCP server.
14
52
  * @param {import("@modelcontextprotocol/sdk/server/mcp.js").McpServer} server
@@ -125,7 +163,7 @@ export function registerTools(server) {
125
163
  {},
126
164
  async () => {
127
165
  const cfg = loadConfig();
128
- const apiUrl = getApiUrl();
166
+ const apiUrl = getSavedApiUrl() ?? "(未配置,登录时填写)";
129
167
  const loggedIn = hasValidToken();
130
168
 
131
169
  const expiresAt =
@@ -165,26 +203,19 @@ export function registerTools(server) {
165
203
  // Tool 4: login — trigger browser login flow
166
204
  server.tool(
167
205
  "login",
168
- "Trigger the data-mgr browser login flow. Opens a browser window for the user to enter credentials. Returns login result on success. If browser cannot be opened, returns the URL for manual visit.",
206
+ "Trigger browser login. User enters API base URL, username, and password on the local login page; apiUrl and token are saved to ~/.data-mgr/config.json. Returns login URL if the browser cannot open.",
169
207
  {
170
208
  openBrowser: z.boolean().optional().describe("Whether to open browser automatically (default true)"),
171
209
  },
172
210
  async ({ openBrowser }) => {
173
211
  try {
174
- const { runBrowserLogin } = await import("./login-server.js");
175
-
176
- // Collect login URL from the callback
177
- let loginUrl = "";
178
- const result = await runBrowserLogin({
179
- openBrowser: openBrowser !== false,
180
- timeoutMs: 5 * 60 * 1000,
181
- onLoginUrl: (url) => { loginUrl = url; },
182
- });
212
+ const result = await runLoginSubprocess(openBrowser !== false);
213
+ const loginUrl = result.loginUrl;
183
214
 
184
215
  const expiresAt = computeExpiresAt(result.expiresIn, new Date(result.loggedInAt));
185
216
 
186
217
  saveConfig({
187
- apiUrl: getApiUrl(),
218
+ apiUrl: result.apiUrl,
188
219
  token: result.token,
189
220
  username: result.username,
190
221
  userId: result.userId,
@@ -195,6 +226,7 @@ export function registerTools(server) {
195
226
  });
196
227
 
197
228
  const lines = [
229
+ `API URL: ${result.apiUrl}`,
198
230
  `Logged in as: ${result.username ?? "(unknown)"}`,
199
231
  `User ID: ${result.userId}`,
200
232
  `Tenant ID: ${result.tenantId}`,
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@wsh19991219/mcp-server",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "description": "MCP server for Data Manager API — stdio transport, JWT auth",
5
5
  "type": "module",
6
6
  "main": "lib/tools.js",
7
7
  "bin": {
8
- "data-mgr-mcp": "bin/data-mgr-mcp.js"
8
+ "data-mgr-mcp": "bin/data-mgr-mcp.js",
9
+ "data-mgr-login": "bin/data-mgr-login.js"
9
10
  },
10
11
  "files": [
11
12
  "bin",