@bigbrain-work/mcp-connect 1.2.2 → 1.3.1
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/LICENSE +21 -0
- package/README.md +38 -58
- package/bin/mcp-connect.js +4 -4
- package/bin/shiliu.js +8 -0
- package/package.json +9 -5
- package/src/arguments.js +85 -19
- package/src/authorization.js +49 -0
- package/src/cli.js +435 -73
- package/src/configurators.js +118 -137
- package/src/constants.js +12 -6
- package/src/credentials.js +166 -148
- package/src/detection.js +16 -21
- package/src/device-auth-client.js +130 -0
- package/src/login-flow.js +39 -0
- package/src/proxy.js +61 -58
- package/src/remote-client.js +27 -10
- package/src/security.js +63 -0
- package/src/skill-refresh.js +202 -0
- package/src/status.js +117 -58
- package/src/token-store.js +116 -0
- package/src/updater.js +59 -0
package/src/proxy.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Server } from
|
|
2
|
-
import { StdioServerTransport } from
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import {
|
|
4
4
|
CallToolRequestSchema,
|
|
5
5
|
GetPromptRequestSchema,
|
|
@@ -8,85 +8,88 @@ import {
|
|
|
8
8
|
ListResourceTemplatesRequestSchema,
|
|
9
9
|
ListToolsRequestSchema,
|
|
10
10
|
ReadResourceRequestSchema,
|
|
11
|
-
} from
|
|
11
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { connectRemote } from
|
|
16
|
-
|
|
17
|
-
function resolveProxyApiKey({ home, platform = process.platform, env = process.env }) {
|
|
18
|
-
const apiKey = readPersistedApiKey({ home, platform, env })?.trim()
|
|
19
|
-
if (!apiKey) {
|
|
20
|
-
throw new Error(`未找到 API Key,请先运行 mcp-connect 安装,或设置 ${API_KEY_ENV}`)
|
|
21
|
-
}
|
|
22
|
-
return apiKey
|
|
23
|
-
}
|
|
13
|
+
import { resolveAuthorization } from "./authorization.js";
|
|
14
|
+
import { API_KEY_ENV, AUTH_URL, PACKAGE_VERSION } from "./constants.js";
|
|
15
|
+
import { connectRemote } from "./remote-client.js";
|
|
24
16
|
|
|
25
17
|
export async function runProxy({
|
|
26
18
|
home,
|
|
27
19
|
url,
|
|
20
|
+
authUrl = AUTH_URL,
|
|
28
21
|
platform = process.platform,
|
|
29
22
|
env = process.env,
|
|
23
|
+
tokenStore,
|
|
30
24
|
} = {}) {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
25
|
+
const authorization = await resolveAuthorization({
|
|
26
|
+
home,
|
|
27
|
+
platform,
|
|
28
|
+
env,
|
|
29
|
+
authUrl,
|
|
30
|
+
tokenStore,
|
|
31
|
+
});
|
|
32
|
+
if (!authorization.token) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`未找到登录凭据,请先运行 shiliu login,或设置 ${API_KEY_ENV}`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const remote = await connectRemote({ token: authorization.token, url });
|
|
38
|
+
const remoteCapabilities = remote.getServerCapabilities() ?? {};
|
|
39
|
+
const capabilities = {};
|
|
40
|
+
if (remoteCapabilities.tools) capabilities.tools = {};
|
|
41
|
+
if (remoteCapabilities.resources) capabilities.resources = {};
|
|
42
|
+
if (remoteCapabilities.prompts) capabilities.prompts = {};
|
|
38
43
|
|
|
39
44
|
const server = new Server(
|
|
40
|
-
{
|
|
41
|
-
name: 'shiliu-ai-mcp-proxy',
|
|
42
|
-
version: PACKAGE_VERSION,
|
|
43
|
-
},
|
|
45
|
+
{ name: "shiliu-ai-mcp-proxy", version: PACKAGE_VERSION },
|
|
44
46
|
{
|
|
45
47
|
capabilities,
|
|
46
|
-
instructions:
|
|
48
|
+
instructions:
|
|
49
|
+
"Authenticated stdio bridge for the Shiliu AI remote MCP service.",
|
|
47
50
|
},
|
|
48
|
-
)
|
|
51
|
+
);
|
|
49
52
|
|
|
50
53
|
if (remoteCapabilities.tools) {
|
|
51
|
-
server.setRequestHandler(ListToolsRequestSchema, (request) =>
|
|
52
|
-
remote.listTools(request.params)
|
|
53
|
-
)
|
|
54
|
-
server.setRequestHandler(CallToolRequestSchema, (request) =>
|
|
55
|
-
remote.callTool(request.params)
|
|
56
|
-
)
|
|
54
|
+
server.setRequestHandler(ListToolsRequestSchema, (request) =>
|
|
55
|
+
remote.listTools(request.params),
|
|
56
|
+
);
|
|
57
|
+
server.setRequestHandler(CallToolRequestSchema, (request) =>
|
|
58
|
+
remote.callTool(request.params),
|
|
59
|
+
);
|
|
57
60
|
}
|
|
58
61
|
if (remoteCapabilities.resources) {
|
|
59
|
-
server.setRequestHandler(ListResourcesRequestSchema, (request) =>
|
|
60
|
-
remote.listResources(request.params)
|
|
61
|
-
)
|
|
62
|
-
server.setRequestHandler(ListResourceTemplatesRequestSchema, (request) =>
|
|
63
|
-
remote.listResourceTemplates(request.params)
|
|
64
|
-
)
|
|
65
|
-
server.setRequestHandler(ReadResourceRequestSchema, (request) =>
|
|
66
|
-
remote.readResource(request.params)
|
|
67
|
-
)
|
|
62
|
+
server.setRequestHandler(ListResourcesRequestSchema, (request) =>
|
|
63
|
+
remote.listResources(request.params),
|
|
64
|
+
);
|
|
65
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, (request) =>
|
|
66
|
+
remote.listResourceTemplates(request.params),
|
|
67
|
+
);
|
|
68
|
+
server.setRequestHandler(ReadResourceRequestSchema, (request) =>
|
|
69
|
+
remote.readResource(request.params),
|
|
70
|
+
);
|
|
68
71
|
}
|
|
69
72
|
if (remoteCapabilities.prompts) {
|
|
70
|
-
server.setRequestHandler(ListPromptsRequestSchema, (request) =>
|
|
71
|
-
remote.listPrompts(request.params)
|
|
72
|
-
)
|
|
73
|
-
server.setRequestHandler(GetPromptRequestSchema, (request) =>
|
|
74
|
-
remote.getPrompt(request.params)
|
|
75
|
-
)
|
|
73
|
+
server.setRequestHandler(ListPromptsRequestSchema, (request) =>
|
|
74
|
+
remote.listPrompts(request.params),
|
|
75
|
+
);
|
|
76
|
+
server.setRequestHandler(GetPromptRequestSchema, (request) =>
|
|
77
|
+
remote.getPrompt(request.params),
|
|
78
|
+
);
|
|
76
79
|
}
|
|
77
80
|
|
|
78
|
-
let closing = false
|
|
81
|
+
let closing = false;
|
|
79
82
|
const shutdown = async () => {
|
|
80
|
-
if (closing) return
|
|
81
|
-
closing = true
|
|
82
|
-
await Promise.allSettled([server.close(), remote.close()])
|
|
83
|
-
}
|
|
84
|
-
process.once(
|
|
85
|
-
process.once(
|
|
83
|
+
if (closing) return;
|
|
84
|
+
closing = true;
|
|
85
|
+
await Promise.allSettled([server.close(), remote.close()]);
|
|
86
|
+
};
|
|
87
|
+
process.once("SIGINT", shutdown);
|
|
88
|
+
process.once("SIGTERM", shutdown);
|
|
86
89
|
try {
|
|
87
|
-
await server.connect(new StdioServerTransport())
|
|
90
|
+
await server.connect(new StdioServerTransport());
|
|
88
91
|
} catch (error) {
|
|
89
|
-
await shutdown()
|
|
90
|
-
throw error
|
|
92
|
+
await shutdown();
|
|
93
|
+
throw error;
|
|
91
94
|
}
|
|
92
95
|
}
|
package/src/remote-client.js
CHANGED
|
@@ -1,20 +1,37 @@
|
|
|
1
|
-
import { Client } from
|
|
2
|
-
import { StreamableHTTPClientTransport } from
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
3
|
|
|
4
|
-
import { PACKAGE_VERSION } from
|
|
4
|
+
import { PACKAGE_VERSION } from "./constants.js";
|
|
5
5
|
|
|
6
|
-
export async function connectRemote({ apiKey, url }) {
|
|
6
|
+
export async function connectRemote({ token, apiKey, url }) {
|
|
7
|
+
const authorizationToken = token || apiKey;
|
|
8
|
+
if (!authorizationToken) throw new Error("Missing authorization token");
|
|
7
9
|
const client = new Client({
|
|
8
|
-
name:
|
|
10
|
+
name: "shiliu-ai-cli",
|
|
9
11
|
version: PACKAGE_VERSION,
|
|
10
|
-
})
|
|
12
|
+
});
|
|
11
13
|
const transport = new StreamableHTTPClientTransport(new URL(url), {
|
|
12
14
|
requestInit: {
|
|
13
15
|
headers: {
|
|
14
|
-
Authorization: `Bearer ${
|
|
16
|
+
Authorization: `Bearer ${authorizationToken}`,
|
|
15
17
|
},
|
|
16
18
|
},
|
|
17
|
-
})
|
|
18
|
-
await client.connect(transport)
|
|
19
|
-
return client
|
|
19
|
+
});
|
|
20
|
+
await client.connect(transport);
|
|
21
|
+
return client;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function readToolCatalog({
|
|
25
|
+
token,
|
|
26
|
+
apiKey,
|
|
27
|
+
url,
|
|
28
|
+
connect = connectRemote,
|
|
29
|
+
}) {
|
|
30
|
+
const client = await connect({ token: token || apiKey, url });
|
|
31
|
+
try {
|
|
32
|
+
const result = await client.listTools();
|
|
33
|
+
return result.tools || [];
|
|
34
|
+
} finally {
|
|
35
|
+
await client.close();
|
|
36
|
+
}
|
|
20
37
|
}
|
package/src/security.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { AUTH_URL, MCP_URL } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]"]);
|
|
4
|
+
const USER_CODE_PATTERN = /^[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}$/u;
|
|
5
|
+
|
|
6
|
+
function normalizeEndpoint(value) {
|
|
7
|
+
const parsed = new URL(value);
|
|
8
|
+
if (parsed.username || parsed.password) {
|
|
9
|
+
throw new Error("服务地址不能包含用户名或密码");
|
|
10
|
+
}
|
|
11
|
+
if (parsed.hash) throw new Error("服务地址不能包含 URL 片段");
|
|
12
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/u, "") || "/";
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isSameEndpoint(actual, expected) {
|
|
17
|
+
return (
|
|
18
|
+
actual.protocol === expected.protocol &&
|
|
19
|
+
actual.hostname === expected.hostname &&
|
|
20
|
+
actual.port === expected.port &&
|
|
21
|
+
actual.pathname === expected.pathname &&
|
|
22
|
+
actual.search === expected.search
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function validateServiceUrl(value, expectedValue, label, { allowLocalhost }) {
|
|
27
|
+
let actual;
|
|
28
|
+
let expected;
|
|
29
|
+
try {
|
|
30
|
+
actual = normalizeEndpoint(value);
|
|
31
|
+
expected = normalizeEndpoint(expectedValue);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw new Error(`${label}无效:${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (isSameEndpoint(actual, expected)) return actual.toString();
|
|
37
|
+
if (
|
|
38
|
+
allowLocalhost &&
|
|
39
|
+
LOOPBACK_HOSTS.has(actual.hostname) &&
|
|
40
|
+
["http:", "https:"].includes(actual.protocol)
|
|
41
|
+
) {
|
|
42
|
+
return actual.toString();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
throw new Error(
|
|
46
|
+
`${label}仅允许石榴 AI 正式地址;本机测试请使用回环地址并显式添加 --allow-localhost`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function validateMcpUrl(value, options = {}) {
|
|
51
|
+
return validateServiceUrl(value, MCP_URL, "MCP 地址", options);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function validateAuthUrl(value, options = {}) {
|
|
55
|
+
return validateServiceUrl(value, AUTH_URL, "授权地址", options);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function normalizeDeviceUserCode(value) {
|
|
59
|
+
if (typeof value !== "string" || !USER_CODE_PATTERN.test(value)) {
|
|
60
|
+
throw new Error("授权服务返回的登录会话编号格式无效");
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
mkdir,
|
|
4
|
+
open,
|
|
5
|
+
readFile,
|
|
6
|
+
stat,
|
|
7
|
+
unlink,
|
|
8
|
+
writeFile,
|
|
9
|
+
} from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
export const SKILL_REFRESH_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
13
|
+
export const SHILIU_SKILL_NAME = "shiliu-ai-mcp";
|
|
14
|
+
const STALE_LOCK_MS = 15 * 60 * 1000;
|
|
15
|
+
|
|
16
|
+
function stateFile(home) {
|
|
17
|
+
return path.join(home, ".shiliu-ai", "skill-refresh.json");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function lockFile(home) {
|
|
21
|
+
return path.join(home, ".shiliu-ai", "skill-refresh.lock");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function acquireLock(home, retried = false) {
|
|
25
|
+
await mkdir(path.dirname(lockFile(home)), { recursive: true });
|
|
26
|
+
let handle;
|
|
27
|
+
try {
|
|
28
|
+
handle = await open(lockFile(home), "wx", 0o600);
|
|
29
|
+
await handle.writeFile(`${process.pid}\n`, "utf8");
|
|
30
|
+
} catch (error) {
|
|
31
|
+
await handle?.close();
|
|
32
|
+
if (error.code === "EEXIST") {
|
|
33
|
+
if (!retried) {
|
|
34
|
+
try {
|
|
35
|
+
const lockStat = await stat(lockFile(home));
|
|
36
|
+
if (Date.now() - lockStat.mtimeMs >= STALE_LOCK_MS) {
|
|
37
|
+
await unlink(lockFile(home));
|
|
38
|
+
return acquireLock(home, true);
|
|
39
|
+
}
|
|
40
|
+
} catch (lockError) {
|
|
41
|
+
if (lockError.code === "ENOENT") return acquireLock(home, true);
|
|
42
|
+
throw lockError;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
await handle.close();
|
|
50
|
+
return async () => {
|
|
51
|
+
try {
|
|
52
|
+
await unlink(lockFile(home));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error.code !== "ENOENT") throw error;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function readState(home) {
|
|
60
|
+
try {
|
|
61
|
+
const value = JSON.parse(await readFile(stateFile(home), "utf8"));
|
|
62
|
+
if (value?.version === 1 && value.scopes && typeof value.scopes === "object") {
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error.code !== "ENOENT" && error.name !== "SyntaxError") throw error;
|
|
67
|
+
}
|
|
68
|
+
return { version: 1, scopes: {} };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function writeState(home, state) {
|
|
72
|
+
const file = stateFile(home);
|
|
73
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
74
|
+
await writeFile(file, `${JSON.stringify(state, null, 2)}\n`, {
|
|
75
|
+
encoding: "utf8",
|
|
76
|
+
mode: 0o600,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function defaultRunUpdate({ cwd, platform = process.platform }) {
|
|
81
|
+
const command = platform === "win32" ? "npx.cmd" : "npx";
|
|
82
|
+
const result = spawnSync(
|
|
83
|
+
command,
|
|
84
|
+
["-y", "skills", "update", SHILIU_SKILL_NAME, "-y"],
|
|
85
|
+
{
|
|
86
|
+
cwd,
|
|
87
|
+
encoding: "utf8",
|
|
88
|
+
windowsHide: true,
|
|
89
|
+
shell: false,
|
|
90
|
+
maxBuffer: 1024 * 1024,
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
if (result.error) throw result.error;
|
|
94
|
+
if (result.status !== 0) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
result.stderr?.trim() ||
|
|
97
|
+
result.stdout?.trim() ||
|
|
98
|
+
`skills update 返回退出码 ${result.status}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function validTimestamp(value) {
|
|
104
|
+
const timestamp = Date.parse(value || "");
|
|
105
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function refreshShiliuSkill({
|
|
109
|
+
home,
|
|
110
|
+
cwd = process.cwd(),
|
|
111
|
+
now = Date.now(),
|
|
112
|
+
force = false,
|
|
113
|
+
intervalMs = SKILL_REFRESH_INTERVAL_MS,
|
|
114
|
+
runUpdate = defaultRunUpdate,
|
|
115
|
+
} = {}) {
|
|
116
|
+
if (!home) throw new Error("缺少用户目录,无法记录 Skill 检查时间");
|
|
117
|
+
const scope = path.resolve(cwd);
|
|
118
|
+
const state = await readState(home);
|
|
119
|
+
const previous = state.scopes[scope] || {};
|
|
120
|
+
const lastAttemptAt = validTimestamp(previous.lastAttemptAt);
|
|
121
|
+
if (!force && lastAttemptAt !== null && now - lastAttemptAt < intervalMs) {
|
|
122
|
+
return {
|
|
123
|
+
status: "skipped",
|
|
124
|
+
reason: "within_interval",
|
|
125
|
+
nextCheckAt: new Date(lastAttemptAt + intervalMs).toISOString(),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const releaseLock = await acquireLock(home);
|
|
130
|
+
if (!releaseLock) {
|
|
131
|
+
return {
|
|
132
|
+
status: "skipped",
|
|
133
|
+
reason: "refresh_in_progress",
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const latestState = await readState(home);
|
|
139
|
+
const latestPrevious = latestState.scopes[scope] || {};
|
|
140
|
+
const latestAttemptAt = validTimestamp(latestPrevious.lastAttemptAt);
|
|
141
|
+
if (
|
|
142
|
+
!force &&
|
|
143
|
+
latestAttemptAt !== null &&
|
|
144
|
+
now - latestAttemptAt < intervalMs
|
|
145
|
+
) {
|
|
146
|
+
return {
|
|
147
|
+
status: "skipped",
|
|
148
|
+
reason: "within_interval",
|
|
149
|
+
nextCheckAt: new Date(latestAttemptAt + intervalMs).toISOString(),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const attemptedAt = new Date(now).toISOString();
|
|
154
|
+
latestState.scopes[scope] = {
|
|
155
|
+
...latestPrevious,
|
|
156
|
+
lastAttemptAt: attemptedAt,
|
|
157
|
+
};
|
|
158
|
+
await writeState(home, latestState);
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
await runUpdate({ cwd: scope });
|
|
162
|
+
latestState.scopes[scope].lastSuccessAt = attemptedAt;
|
|
163
|
+
await writeState(home, latestState);
|
|
164
|
+
return {
|
|
165
|
+
status: "checked",
|
|
166
|
+
checkedAt: attemptedAt,
|
|
167
|
+
nextCheckAt: new Date(now + intervalMs).toISOString(),
|
|
168
|
+
};
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return {
|
|
171
|
+
status: "failed",
|
|
172
|
+
checkedAt: attemptedAt,
|
|
173
|
+
nextCheckAt: new Date(now + intervalMs).toISOString(),
|
|
174
|
+
message: error.message,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
} finally {
|
|
178
|
+
await releaseLock();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function printSkillRefresh(options = {}) {
|
|
183
|
+
const result = await refreshShiliuSkill(options);
|
|
184
|
+
if (options.json) {
|
|
185
|
+
console.log(JSON.stringify(result, null, 2));
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
if (result.status === "skipped") {
|
|
189
|
+
console.log(
|
|
190
|
+
result.reason === "refresh_in_progress"
|
|
191
|
+
? "另一个石榴 Skill 检查正在进行,本次继续使用已安装 Skill。"
|
|
192
|
+
: `石榴 Skill 在24小时内已检查,下次检查时间:${result.nextCheckAt}`,
|
|
193
|
+
);
|
|
194
|
+
} else if (result.status === "checked") {
|
|
195
|
+
console.log(`石榴 Skill 检查完成,下次检查时间:${result.nextCheckAt}`);
|
|
196
|
+
} else {
|
|
197
|
+
console.warn(
|
|
198
|
+
`石榴 Skill 检查失败:${result.message}。已进入24小时冷却,本次继续使用已安装 Skill。`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return result;
|
|
202
|
+
}
|
package/src/status.js
CHANGED
|
@@ -1,94 +1,153 @@
|
|
|
1
|
-
import { readFile } from
|
|
2
|
-
import path from
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { AUTH_URL, MCP_URL, PACKAGE_NAME, SERVER_NAME } from "./constants.js";
|
|
5
|
+
import { resolveAuthorization } from "./authorization.js";
|
|
6
|
+
import { readToolCatalog } from "./remote-client.js";
|
|
6
7
|
|
|
7
8
|
async function readText(filePath) {
|
|
8
9
|
try {
|
|
9
|
-
return await readFile(filePath,
|
|
10
|
+
return await readFile(filePath, "utf8");
|
|
10
11
|
} catch (error) {
|
|
11
|
-
if (error.code ===
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
throw error
|
|
12
|
+
if (error.code === "ENOENT") return "";
|
|
13
|
+
throw error;
|
|
15
14
|
}
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
async function hasServerConfig(filePath) {
|
|
19
|
-
const content = await readText(filePath)
|
|
20
|
-
return
|
|
18
|
+
const content = await readText(filePath);
|
|
19
|
+
return (
|
|
20
|
+
content.includes(SERVER_NAME) &&
|
|
21
|
+
(content.includes(MCP_URL) || content.includes(PACKAGE_NAME))
|
|
22
|
+
);
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export async function inspectLocalConfiguration(home) {
|
|
24
26
|
return {
|
|
25
|
-
codex: await hasServerConfig(path.join(home,
|
|
26
|
-
claude: await hasServerConfig(path.join(home,
|
|
27
|
-
cursor: await hasServerConfig(path.join(home,
|
|
28
|
-
}
|
|
27
|
+
codex: await hasServerConfig(path.join(home, ".codex", "config.toml")),
|
|
28
|
+
claude: await hasServerConfig(path.join(home, ".claude.json")),
|
|
29
|
+
cursor: await hasServerConfig(path.join(home, ".cursor", "mcp.json")),
|
|
30
|
+
};
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
export async function probeMcp(
|
|
32
|
-
|
|
33
|
+
export async function probeMcp(
|
|
34
|
+
token,
|
|
35
|
+
{ url = MCP_URL, readCatalog = readToolCatalog } = {},
|
|
36
|
+
) {
|
|
37
|
+
if (!token) {
|
|
33
38
|
return {
|
|
34
39
|
ok: false,
|
|
35
|
-
detail:
|
|
36
|
-
|
|
40
|
+
detail: "尚未登录",
|
|
41
|
+
toolCount: 0,
|
|
42
|
+
tools: [],
|
|
43
|
+
};
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
try {
|
|
40
|
-
const
|
|
41
|
-
method: 'POST',
|
|
42
|
-
headers: {
|
|
43
|
-
Accept: 'application/json, text/event-stream',
|
|
44
|
-
Authorization: `Bearer ${apiKey}`,
|
|
45
|
-
'Content-Type': 'application/json',
|
|
46
|
-
},
|
|
47
|
-
body: JSON.stringify({
|
|
48
|
-
jsonrpc: '2.0',
|
|
49
|
-
id: 1,
|
|
50
|
-
method: 'initialize',
|
|
51
|
-
params: {
|
|
52
|
-
protocolVersion: '2025-03-26',
|
|
53
|
-
capabilities: {},
|
|
54
|
-
clientInfo: {
|
|
55
|
-
name: 'bigbrain-work-mcp-connect',
|
|
56
|
-
version: '1.0.0',
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
}),
|
|
60
|
-
signal: AbortSignal.timeout(10000),
|
|
61
|
-
})
|
|
47
|
+
const tools = await readCatalog({ token, url });
|
|
62
48
|
return {
|
|
63
|
-
ok:
|
|
64
|
-
detail:
|
|
65
|
-
|
|
49
|
+
ok: true,
|
|
50
|
+
detail: `连接正常,tools/list 返回 ${tools.length} 个工具`,
|
|
51
|
+
toolCount: tools.length,
|
|
52
|
+
tools,
|
|
53
|
+
};
|
|
66
54
|
} catch (error) {
|
|
67
55
|
return {
|
|
68
56
|
ok: false,
|
|
69
57
|
detail: `连接失败:${error.message}`,
|
|
70
|
-
|
|
58
|
+
toolCount: 0,
|
|
59
|
+
tools: [],
|
|
60
|
+
};
|
|
71
61
|
}
|
|
72
62
|
}
|
|
73
63
|
|
|
74
64
|
function mark(ok) {
|
|
75
|
-
return ok ?
|
|
65
|
+
return ok ? "✓" : "○";
|
|
76
66
|
}
|
|
77
67
|
|
|
78
|
-
export async function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
68
|
+
export async function getStatus({
|
|
69
|
+
home,
|
|
70
|
+
platform = process.platform,
|
|
71
|
+
env = process.env,
|
|
72
|
+
url = MCP_URL,
|
|
73
|
+
authUrl = AUTH_URL,
|
|
74
|
+
readCatalog,
|
|
75
|
+
resolveAuth = resolveAuthorization,
|
|
76
|
+
} = {}) {
|
|
77
|
+
const authorization = await resolveAuth({ home, platform, env, authUrl });
|
|
78
|
+
const configs = await inspectLocalConfiguration(home);
|
|
79
|
+
const remote = await probeMcp(authorization.token, { url, readCatalog });
|
|
89
80
|
return {
|
|
90
|
-
|
|
81
|
+
credential: Boolean(authorization.token),
|
|
82
|
+
credentialSource: authorization.source,
|
|
91
83
|
configs,
|
|
92
84
|
remote,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function printStatus(options = {}) {
|
|
89
|
+
const result = await getStatus(options);
|
|
90
|
+
if (options.json) {
|
|
91
|
+
console.log(
|
|
92
|
+
JSON.stringify(
|
|
93
|
+
{
|
|
94
|
+
credential: result.credential,
|
|
95
|
+
credentialSource: result.credentialSource,
|
|
96
|
+
configs: result.configs,
|
|
97
|
+
remote: {
|
|
98
|
+
ok: result.remote.ok,
|
|
99
|
+
detail: result.remote.detail,
|
|
100
|
+
toolCount: result.remote.toolCount,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
null,
|
|
104
|
+
2,
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log(
|
|
111
|
+
`登录凭据 ${mark(result.credential)} ${result.credential ? `已设置(${result.credentialSource})` : "未设置"}`,
|
|
112
|
+
);
|
|
113
|
+
console.log(
|
|
114
|
+
`Codex 配置 ${mark(result.configs.codex)} ${result.configs.codex ? "已写入" : "未发现"}`,
|
|
115
|
+
);
|
|
116
|
+
console.log(
|
|
117
|
+
`Claude Code 配置 ${mark(result.configs.claude)} ${result.configs.claude ? "已写入" : "未发现"}`,
|
|
118
|
+
);
|
|
119
|
+
console.log(
|
|
120
|
+
`Cursor 配置 ${mark(result.configs.cursor)} ${result.configs.cursor ? "已写入" : "未发现"}`,
|
|
121
|
+
);
|
|
122
|
+
console.log(
|
|
123
|
+
`石榴 AI MCP ${mark(result.remote.ok)} ${result.remote.detail}`,
|
|
124
|
+
);
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function printTools({
|
|
129
|
+
home,
|
|
130
|
+
platform = process.platform,
|
|
131
|
+
env = process.env,
|
|
132
|
+
url = MCP_URL,
|
|
133
|
+
authUrl = AUTH_URL,
|
|
134
|
+
json = false,
|
|
135
|
+
readCatalog = readToolCatalog,
|
|
136
|
+
resolveAuth = resolveAuthorization,
|
|
137
|
+
} = {}) {
|
|
138
|
+
const authorization = await resolveAuth({ home, platform, env, authUrl });
|
|
139
|
+
if (!authorization.token) throw new Error("尚未登录,请先运行 shiliu login");
|
|
140
|
+
const tools = await readCatalog({ token: authorization.token, url });
|
|
141
|
+
|
|
142
|
+
if (json) {
|
|
143
|
+
console.log(JSON.stringify({ count: tools.length, tools }, null, 2));
|
|
144
|
+
return tools;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`石榴 AI MCP 当前提供 ${tools.length} 个工具:`);
|
|
148
|
+
for (const tool of tools) {
|
|
149
|
+
const summary = tool.description ? ` — ${tool.description}` : "";
|
|
150
|
+
console.log(`- ${tool.name}${summary}`);
|
|
93
151
|
}
|
|
152
|
+
return tools;
|
|
94
153
|
}
|