tyc-cli 0.3.5 → 0.3.7
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 +48 -42
- package/dist/commands/capabilities.js +32 -0
- package/dist/commands/category.js +15 -75
- package/dist/commands/coreCall.js +23 -0
- package/dist/commands/init.js +21 -38
- package/dist/commands/layers.js +13 -1
- package/dist/commands/login.js +129 -55
- package/dist/commands/output.js +60 -0
- package/dist/config.js +111 -11
- package/dist/coreClient.js +2 -2
- package/dist/index.js +73 -85
- package/dist/oauth.js +70 -0
- package/dist/oauthPending.js +41 -0
- package/dist/types.js +3 -4
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/dist/mcpClient.js +0 -146
- package/dist/session.js +0 -47
package/dist/commands/login.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { DEFAULT_MCP_URL, defaultCoreURL, loadConfig, resolveConfig, saveConfig, } from "../config.js";
|
|
1
|
+
import { DEFAULT_MCP_URL, applyOAuthTokenToConfig, defaultCoreURL, loadConfig, resolveConfig, saveConfig, } from "../config.js";
|
|
2
2
|
import { verifyCoreAuthEndpoint } from "../coreClient.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { buildAuthorizationURL, createPKCE, discoverAuthorizationServer, discoverProtectedResourceMetadata, exchangeAuthorizationCode, knownOAuthDefaultsForMcpURL, openBrowser, randomState, registerDynamicOAuthClient, startCallbackServer, } from "../oauth.js";
|
|
3
|
+
import { buildAuthorizationURL, buildCliCallbackRedirectUri, createPKCE, discoverAuthorizationServer, discoverProtectedResourceMetadata, exchangeAuthorizationCode, knownOAuthDefaultsForMcpURL, openBrowser, parseAuthorizationCallbackCode, randomState, registerDynamicOAuthClient, startCallbackServer, } from "../oauth.js";
|
|
4
|
+
import { clearPendingOAuthLogin, loadPendingOAuthLogin, savePendingOAuthLogin, } from "../oauthPending.js";
|
|
6
5
|
import { printLoginSuccessBanner } from "../logo.js";
|
|
7
6
|
function parsePort(raw) {
|
|
8
7
|
if (!raw)
|
|
@@ -13,10 +12,61 @@ function parsePort(raw) {
|
|
|
13
12
|
}
|
|
14
13
|
return n;
|
|
15
14
|
}
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
function normalizeCallbackPath(raw) {
|
|
16
|
+
const path = raw || "/oauth/callback";
|
|
17
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
18
|
+
}
|
|
19
|
+
function pickCallbackToken(opts) {
|
|
20
|
+
if (opts.callbackToken && opts.token) {
|
|
21
|
+
throw new Error("--callback-token 和 --token 只能指定一个。");
|
|
22
|
+
}
|
|
23
|
+
return opts.callbackToken || opts.token;
|
|
24
|
+
}
|
|
25
|
+
function saveOAuthTokenConfig(mcpURL, token, oauth, verbose) {
|
|
26
|
+
const cfg = loadConfig() || {};
|
|
27
|
+
cfg.url = mcpURL;
|
|
28
|
+
delete cfg.transport;
|
|
29
|
+
cfg.coreUrl = defaultCoreURL(mcpURL);
|
|
30
|
+
applyOAuthTokenToConfig(cfg, token, oauth);
|
|
31
|
+
saveConfig(cfg);
|
|
32
|
+
if (verbose) {
|
|
33
|
+
console.error(`> OAuth login state saved to ~/.tyc/config.json (url=${cfg.url})`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function verifySavedOAuthLogin(verify, verbose) {
|
|
37
|
+
if (!verify) {
|
|
38
|
+
printLoginSuccessBanner();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const resolved = resolveConfig();
|
|
42
|
+
await verifyCoreAuthEndpoint(resolved, verbose);
|
|
43
|
+
if (verbose) {
|
|
44
|
+
console.error("> Shared Core auth check passed");
|
|
45
|
+
}
|
|
46
|
+
printLoginSuccessBanner();
|
|
47
|
+
}
|
|
48
|
+
async function completePendingOAuthLogin(pending, callbackToken, verify, verbose) {
|
|
49
|
+
const code = parseAuthorizationCallbackCode(callbackToken, pending.state);
|
|
50
|
+
if (verbose) {
|
|
51
|
+
console.error("> authorization code provided; exchanging access token");
|
|
52
|
+
}
|
|
53
|
+
const token = await exchangeAuthorizationCode({
|
|
54
|
+
tokenEndpoint: pending.tokenEndpoint,
|
|
55
|
+
clientId: pending.clientId,
|
|
56
|
+
clientSecret: pending.clientSecret,
|
|
57
|
+
code,
|
|
58
|
+
redirectUri: pending.redirectUri,
|
|
59
|
+
codeVerifier: pending.codeVerifier,
|
|
60
|
+
resource: pending.resource,
|
|
61
|
+
});
|
|
62
|
+
saveOAuthTokenConfig(pending.mcpURL, token, {
|
|
63
|
+
tokenEndpoint: pending.tokenEndpoint,
|
|
64
|
+
clientId: pending.clientId,
|
|
65
|
+
clientSecret: pending.clientSecret,
|
|
66
|
+
resource: pending.resource,
|
|
67
|
+
}, verbose);
|
|
68
|
+
clearPendingOAuthLogin(verbose);
|
|
69
|
+
await verifySavedOAuthLogin(verify, verbose);
|
|
20
70
|
}
|
|
21
71
|
export function registerLoginCommand(program) {
|
|
22
72
|
program
|
|
@@ -34,9 +84,29 @@ export function registerLoginCommand(program) {
|
|
|
34
84
|
.option("--redirect-host <host>", "本地回调 host(默认 localhost)")
|
|
35
85
|
.option("--redirect-port <port>", "本地回调端口(默认 7078)")
|
|
36
86
|
.option("--callback-path <path>", "本地回调路径(默认 /oauth/callback)")
|
|
87
|
+
.option("--callback-token <code-or-url>", "继续非阻塞登录:传入授权回调 code 或包含 code 的完整 callback URL")
|
|
88
|
+
.option("--token <code-or-url>", "同 --callback-token(兼容别名;仍需传授权回调 code)")
|
|
37
89
|
.option("--no-open", "不自动打开浏览器,只打印授权 URL")
|
|
38
|
-
.option("--no-
|
|
90
|
+
.option("--no-block", "配合 --no-open:打印授权 URL 并保存 pending 状态后立即退出")
|
|
91
|
+
.option("--no-verify", "保存 token 后跳过 shared core 鉴权校验")
|
|
39
92
|
.action(async (opts) => {
|
|
93
|
+
const verbose = !!program.opts().verbose;
|
|
94
|
+
const callbackToken = pickCallbackToken(opts);
|
|
95
|
+
if (callbackToken) {
|
|
96
|
+
const pending = loadPendingOAuthLogin();
|
|
97
|
+
if (!pending) {
|
|
98
|
+
throw new Error("未找到待完成的 OAuth 登录状态。请先运行 tyc login --no-open --no-block。");
|
|
99
|
+
}
|
|
100
|
+
if (verbose) {
|
|
101
|
+
console.error(`> loaded pending OAuth login from ~/.tyc/oauth_pending.json (url=${pending.mcpURL})`);
|
|
102
|
+
}
|
|
103
|
+
await completePendingOAuthLogin(pending, callbackToken, opts.verify !== false && pending.verify !== false, verbose);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const noBlock = opts.block === false;
|
|
107
|
+
if (noBlock && opts.open !== false) {
|
|
108
|
+
throw new Error("--no-block 需要与 --no-open 一起使用。");
|
|
109
|
+
}
|
|
40
110
|
const current = loadConfig() || {};
|
|
41
111
|
const mcpURL = opts.url || process.env.TYC_MCP_ENDPOINT || current.url || DEFAULT_MCP_URL;
|
|
42
112
|
const knownDefaults = knownOAuthDefaultsForMcpURL(mcpURL);
|
|
@@ -45,8 +115,7 @@ export function registerLoginCommand(program) {
|
|
|
45
115
|
const scope = opts.scope || "mcp:tools.call";
|
|
46
116
|
const redirectHost = opts.redirectHost || "localhost";
|
|
47
117
|
const redirectPort = parsePort(opts.redirectPort || "7078");
|
|
48
|
-
const callbackPath = opts.callbackPath
|
|
49
|
-
const verbose = !!program.opts().verbose;
|
|
118
|
+
const callbackPath = normalizeCallbackPath(opts.callbackPath);
|
|
50
119
|
let issuer = opts.issuer || "";
|
|
51
120
|
let resource = opts.resource || "";
|
|
52
121
|
if (!opts.issuer || !opts.resource) {
|
|
@@ -108,16 +177,19 @@ export function registerLoginCommand(program) {
|
|
|
108
177
|
}
|
|
109
178
|
const state = randomState();
|
|
110
179
|
const pkce = createPKCE();
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
180
|
+
let callback;
|
|
181
|
+
const redirectUri = noBlock
|
|
182
|
+
? buildCliCallbackRedirectUri(issuer)
|
|
183
|
+
: (callback = await startCallbackServer({
|
|
184
|
+
host: redirectHost,
|
|
185
|
+
port: redirectPort,
|
|
186
|
+
path: callbackPath,
|
|
187
|
+
state,
|
|
188
|
+
timeoutMs: 5 * 60 * 1000,
|
|
189
|
+
})).redirectUri;
|
|
118
190
|
if (!clientId) {
|
|
119
191
|
if (!endpoints.registrationEndpoint) {
|
|
120
|
-
callback
|
|
192
|
+
callback?.close();
|
|
121
193
|
throw new Error("OAuth metadata 未提供 registration_endpoint,请用 --client-id 指定预注册客户端。");
|
|
122
194
|
}
|
|
123
195
|
if (verbose) {
|
|
@@ -128,12 +200,12 @@ export function registerLoginCommand(program) {
|
|
|
128
200
|
registeredClient = await registerDynamicOAuthClient({
|
|
129
201
|
registrationEndpoint: endpoints.registrationEndpoint,
|
|
130
202
|
clientName: "tyc-cli",
|
|
131
|
-
redirectUris: [
|
|
203
|
+
redirectUris: [redirectUri],
|
|
132
204
|
tokenEndpointAuthMethod: "none",
|
|
133
205
|
});
|
|
134
206
|
}
|
|
135
207
|
catch (err) {
|
|
136
|
-
callback
|
|
208
|
+
callback?.close();
|
|
137
209
|
throw err;
|
|
138
210
|
}
|
|
139
211
|
clientId = registeredClient.clientId;
|
|
@@ -145,14 +217,36 @@ export function registerLoginCommand(program) {
|
|
|
145
217
|
const authorizationURL = buildAuthorizationURL({
|
|
146
218
|
authorizationEndpoint: endpoints.authorizationEndpoint,
|
|
147
219
|
clientId,
|
|
148
|
-
redirectUri
|
|
220
|
+
redirectUri,
|
|
149
221
|
scope,
|
|
150
222
|
state,
|
|
151
223
|
codeChallenge: pkce.codeChallenge,
|
|
152
224
|
resource,
|
|
153
225
|
});
|
|
154
226
|
if (verbose) {
|
|
155
|
-
console.error(`> OAuth callback: ${
|
|
227
|
+
console.error(`> OAuth callback: ${redirectUri}`);
|
|
228
|
+
}
|
|
229
|
+
if (noBlock) {
|
|
230
|
+
savePendingOAuthLogin({
|
|
231
|
+
version: 1,
|
|
232
|
+
createdAt: Date.now(),
|
|
233
|
+
mcpURL,
|
|
234
|
+
tokenEndpoint: endpoints.tokenEndpoint,
|
|
235
|
+
clientId,
|
|
236
|
+
clientSecret,
|
|
237
|
+
redirectUri,
|
|
238
|
+
codeVerifier: pkce.codeVerifier,
|
|
239
|
+
resource,
|
|
240
|
+
state,
|
|
241
|
+
verify: opts.verify !== false,
|
|
242
|
+
});
|
|
243
|
+
console.error("请在浏览器打开以下 URL 完成登录:");
|
|
244
|
+
console.error(authorizationURL);
|
|
245
|
+
console.error("");
|
|
246
|
+
console.error("完成授权后,浏览器会打开天眼查 CLI 回调页。请按页面提示复制命令到终端执行。");
|
|
247
|
+
console.error("也可以复制完整回调 URL 后运行:");
|
|
248
|
+
console.error(`tyc login --callback-token "${redirectUri}?code=...&state=..."`);
|
|
249
|
+
return;
|
|
156
250
|
}
|
|
157
251
|
if (opts.open === false) {
|
|
158
252
|
console.error("请在浏览器打开以下 URL 完成登录:");
|
|
@@ -162,6 +256,9 @@ export function registerLoginCommand(program) {
|
|
|
162
256
|
console.error("正在打开浏览器完成 OAuth 登录...");
|
|
163
257
|
openBrowser(authorizationURL);
|
|
164
258
|
}
|
|
259
|
+
if (!callback) {
|
|
260
|
+
throw new Error("OAuth callback server not initialized");
|
|
261
|
+
}
|
|
165
262
|
const code = await callback.waitForCode();
|
|
166
263
|
if (verbose) {
|
|
167
264
|
console.error("> authorization code received; exchanging access token");
|
|
@@ -171,40 +268,17 @@ export function registerLoginCommand(program) {
|
|
|
171
268
|
clientId,
|
|
172
269
|
clientSecret,
|
|
173
270
|
code,
|
|
174
|
-
redirectUri
|
|
271
|
+
redirectUri,
|
|
175
272
|
codeVerifier: pkce.codeVerifier,
|
|
176
273
|
resource,
|
|
177
274
|
});
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
saveConfig(cfg);
|
|
187
|
-
clearSession();
|
|
188
|
-
if (verbose) {
|
|
189
|
-
console.error(`> OAuth login state saved to ~/.tyc/config.json (url=${cfg.url})`);
|
|
190
|
-
}
|
|
191
|
-
if (opts.verify === false) {
|
|
192
|
-
printLoginSuccessBanner();
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
const resolved = resolveConfig();
|
|
196
|
-
if (resolved.transport === "mcp") {
|
|
197
|
-
const sess = await ensureSession(resolved, { verbose });
|
|
198
|
-
if (verbose) {
|
|
199
|
-
console.error(`> MCP session initialized (sessionId=${sess.sessionId.slice(0, 16)}...)`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
await verifyCoreAuthEndpoint(resolved, verbose);
|
|
204
|
-
if (verbose) {
|
|
205
|
-
console.error("> Shared Core auth check passed");
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
printLoginSuccessBanner();
|
|
275
|
+
saveOAuthTokenConfig(mcpURL, token, {
|
|
276
|
+
tokenEndpoint: endpoints.tokenEndpoint,
|
|
277
|
+
clientId,
|
|
278
|
+
clientSecret,
|
|
279
|
+
resource,
|
|
280
|
+
}, verbose);
|
|
281
|
+
clearPendingOAuthLogin(verbose);
|
|
282
|
+
await verifySavedOAuthLogin(opts.verify !== false, verbose);
|
|
209
283
|
});
|
|
210
284
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsonToMarkdown } from "../utils/jsonToMarkdown.js";
|
|
2
|
+
import { applyTruncation, resolveOptionalInt, TRUNCATE_DEFAULTS, } from "../utils/truncate.js";
|
|
3
|
+
export function emitToolResult(result, opts, toolName) {
|
|
4
|
+
if (result.isError) {
|
|
5
|
+
const errText = extractText(result) || "未知业务错误";
|
|
6
|
+
console.error(errText);
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
const text = extractText(result);
|
|
10
|
+
if (text === null) {
|
|
11
|
+
console.error("tools/call 返回无可解析内容");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
let payload;
|
|
15
|
+
try {
|
|
16
|
+
payload = JSON.parse(text);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
emitWithTruncation(text.endsWith("\n") ? text.slice(0, -1) : text, opts);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
let rendered;
|
|
23
|
+
if (opts.md) {
|
|
24
|
+
rendered = jsonToMarkdown(payload, toolName);
|
|
25
|
+
}
|
|
26
|
+
else if (opts.compact) {
|
|
27
|
+
rendered = JSON.stringify(payload);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
rendered = JSON.stringify(payload, null, 2);
|
|
31
|
+
}
|
|
32
|
+
emitWithTruncation(rendered, opts);
|
|
33
|
+
}
|
|
34
|
+
function emitWithTruncation(rendered, opts) {
|
|
35
|
+
const head = resolveOptionalInt(opts.head, TRUNCATE_DEFAULTS.HEAD);
|
|
36
|
+
const tail = resolveOptionalInt(opts.tail, TRUNCATE_DEFAULTS.TAIL);
|
|
37
|
+
const threshold = resolveOptionalInt(opts.threshold, TRUNCATE_DEFAULTS.THRESHOLD);
|
|
38
|
+
const outputFile = typeof opts.outputFile === "string" && opts.outputFile.length > 0
|
|
39
|
+
? opts.outputFile
|
|
40
|
+
: undefined;
|
|
41
|
+
const r = applyTruncation(rendered, {
|
|
42
|
+
full: !!opts.full,
|
|
43
|
+
head,
|
|
44
|
+
tail,
|
|
45
|
+
threshold,
|
|
46
|
+
outputFile,
|
|
47
|
+
});
|
|
48
|
+
console.log(r.rendered);
|
|
49
|
+
for (const n of r.notices) {
|
|
50
|
+
process.stderr.write(n + "\n");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function extractText(result) {
|
|
54
|
+
const content = result.content || [];
|
|
55
|
+
for (const c of content) {
|
|
56
|
+
if (typeof c.text === "string" && c.text.length > 0)
|
|
57
|
+
return c.text;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// ~/.tyc/config.json 读写
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// 配置文件格式:
|
|
4
4
|
// { "url": "https://mcp.tianyancha.com/mcp",
|
|
5
5
|
// "headers": { "Authorization": "xxxx" },
|
|
6
|
-
// "transport": "core",
|
|
7
6
|
// "coreUrl": "https://mcp.tianyancha.com/v1/core/tools/call" }
|
|
8
7
|
//
|
|
9
|
-
// 默认端点指向天眼查公网 MCP Server 的 canonical 入口 /mcp
|
|
10
|
-
//
|
|
8
|
+
// 默认端点指向天眼查公网 MCP Server 的 canonical 入口 /mcp,CLI 实际调用
|
|
9
|
+
// shared core REST 端点;连接本地或私有环境可通过
|
|
11
10
|
// tyc init --url http://localhost:8080/mcp 或 TYC_MCP_ENDPOINT 环境变量覆盖。
|
|
12
11
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "node:fs";
|
|
13
12
|
import { join } from "node:path";
|
|
14
13
|
import { homedir } from "node:os";
|
|
14
|
+
import { exchangeRefreshToken } from "./oauth.js";
|
|
15
15
|
const CONFIG_DIR = join(homedir(), ".tyc");
|
|
16
16
|
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
17
17
|
export const DEFAULT_MCP_URL = "https://mcp.tianyancha.com/mcp";
|
|
@@ -35,11 +35,57 @@ export function saveConfig(config) {
|
|
|
35
35
|
}
|
|
36
36
|
catch { /* best effort */ }
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (
|
|
41
|
-
return "
|
|
42
|
-
return "
|
|
38
|
+
const OAUTH_REFRESH_SKEW_MS = 60 * 1000;
|
|
39
|
+
function normalizeTokenType(raw) {
|
|
40
|
+
if (!raw)
|
|
41
|
+
return "Bearer";
|
|
42
|
+
return raw.toLowerCase() === "bearer" ? "Bearer" : raw;
|
|
43
|
+
}
|
|
44
|
+
function jwtExpiresAt(accessToken) {
|
|
45
|
+
const parts = accessToken.split(".");
|
|
46
|
+
if (parts.length < 2)
|
|
47
|
+
return undefined;
|
|
48
|
+
try {
|
|
49
|
+
const normalized = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
50
|
+
const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
|
|
51
|
+
const payload = JSON.parse(Buffer.from(padded, "base64").toString("utf-8"));
|
|
52
|
+
if (typeof payload.exp === "number" && Number.isFinite(payload.exp)) {
|
|
53
|
+
return payload.exp * 1000;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Non-JWT access tokens are valid OAuth responses too; expires_in remains authoritative.
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
export function inferAccessTokenExpiresAt(token, now = Date.now()) {
|
|
62
|
+
if (typeof token.expires_in === "number" && Number.isFinite(token.expires_in) && token.expires_in > 0) {
|
|
63
|
+
return now + token.expires_in * 1000;
|
|
64
|
+
}
|
|
65
|
+
return jwtExpiresAt(token.access_token);
|
|
66
|
+
}
|
|
67
|
+
export function applyOAuthTokenToConfig(config, token, opts) {
|
|
68
|
+
const now = opts.now ?? Date.now();
|
|
69
|
+
const tokenType = normalizeTokenType(token.token_type);
|
|
70
|
+
const refreshToken = token.refresh_token || opts.refreshToken;
|
|
71
|
+
if (!config.headers)
|
|
72
|
+
config.headers = {};
|
|
73
|
+
config.headers.Authorization = `${tokenType} ${token.access_token}`;
|
|
74
|
+
if (!refreshToken) {
|
|
75
|
+
delete config.oauth;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
config.oauth = {
|
|
79
|
+
tokenEndpoint: opts.tokenEndpoint,
|
|
80
|
+
clientId: opts.clientId,
|
|
81
|
+
clientSecret: opts.clientSecret,
|
|
82
|
+
refreshToken,
|
|
83
|
+
resource: opts.resource,
|
|
84
|
+
scope: token.scope || opts.scope,
|
|
85
|
+
tokenType,
|
|
86
|
+
accessTokenExpiresAt: inferAccessTokenExpiresAt(token, now),
|
|
87
|
+
updatedAt: now,
|
|
88
|
+
};
|
|
43
89
|
}
|
|
44
90
|
export function defaultCoreURL(mcpURL) {
|
|
45
91
|
const u = new URL(mcpURL);
|
|
@@ -57,7 +103,6 @@ export function defaultCoreURL(mcpURL) {
|
|
|
57
103
|
export function resolveConfig() {
|
|
58
104
|
const cfg = loadConfig() || {};
|
|
59
105
|
const url = process.env.TYC_MCP_ENDPOINT || cfg.url || DEFAULT_MCP_URL;
|
|
60
|
-
const transport = normalizeTransport(process.env.TYC_TRANSPORT || cfg.transport);
|
|
61
106
|
const coreUrl = process.env.TYC_CORE_ENDPOINT || cfg.coreUrl || defaultCoreURL(url);
|
|
62
107
|
const headers = { ...(cfg.headers || {}) };
|
|
63
108
|
if (!headers.Authorization && process.env.TYC_AUTHORIZATION) {
|
|
@@ -67,5 +112,60 @@ export function resolveConfig() {
|
|
|
67
112
|
console.error("未配置 Authorization。请先运行:tyc init --authorization YOUR_API_KEY");
|
|
68
113
|
process.exit(1);
|
|
69
114
|
}
|
|
70
|
-
return { url, coreUrl,
|
|
115
|
+
return { url, coreUrl, headers, oauth: cfg.oauth ? { ...cfg.oauth } : undefined };
|
|
116
|
+
}
|
|
117
|
+
function hasBearerAuthorization(config) {
|
|
118
|
+
return /^Bearer\s+\S+/i.test(config.headers.Authorization || "");
|
|
119
|
+
}
|
|
120
|
+
function shouldRefreshOAuth(oauth, opts) {
|
|
121
|
+
if (opts.force)
|
|
122
|
+
return true;
|
|
123
|
+
if (!oauth.accessTokenExpiresAt)
|
|
124
|
+
return false;
|
|
125
|
+
const skewMs = opts.skewMs ?? OAUTH_REFRESH_SKEW_MS;
|
|
126
|
+
return oauth.accessTokenExpiresAt <= Date.now() + skewMs;
|
|
127
|
+
}
|
|
128
|
+
export async function refreshResolvedConfigOAuth(resolved, opts = {}) {
|
|
129
|
+
const stored = loadConfig() || {};
|
|
130
|
+
const oauth = stored.oauth;
|
|
131
|
+
if (!oauth?.refreshToken || !oauth.tokenEndpoint || !oauth.clientId) {
|
|
132
|
+
return { config: resolved, refreshed: false };
|
|
133
|
+
}
|
|
134
|
+
if (!hasBearerAuthorization(resolved)) {
|
|
135
|
+
return { config: resolved, refreshed: false };
|
|
136
|
+
}
|
|
137
|
+
if (!shouldRefreshOAuth(oauth, opts)) {
|
|
138
|
+
return { config: resolved, refreshed: false };
|
|
139
|
+
}
|
|
140
|
+
let token;
|
|
141
|
+
try {
|
|
142
|
+
token = await exchangeRefreshToken({
|
|
143
|
+
tokenEndpoint: oauth.tokenEndpoint,
|
|
144
|
+
clientId: oauth.clientId,
|
|
145
|
+
clientSecret: oauth.clientSecret,
|
|
146
|
+
refreshToken: oauth.refreshToken,
|
|
147
|
+
resource: oauth.resource,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
152
|
+
throw new Error(`OAuth refresh token 刷新失败,请重新运行 tyc login:${msg}`);
|
|
153
|
+
}
|
|
154
|
+
applyOAuthTokenToConfig(stored, token, {
|
|
155
|
+
tokenEndpoint: oauth.tokenEndpoint,
|
|
156
|
+
clientId: oauth.clientId,
|
|
157
|
+
clientSecret: oauth.clientSecret,
|
|
158
|
+
refreshToken: oauth.refreshToken,
|
|
159
|
+
resource: oauth.resource,
|
|
160
|
+
scope: oauth.scope,
|
|
161
|
+
});
|
|
162
|
+
saveConfig(stored);
|
|
163
|
+
if (opts.verbose) {
|
|
164
|
+
console.error("> OAuth access token refreshed");
|
|
165
|
+
}
|
|
166
|
+
return { config: resolveConfig(), refreshed: true };
|
|
167
|
+
}
|
|
168
|
+
export async function resolveConfigWithFreshOAuth(opts = {}) {
|
|
169
|
+
const resolved = resolveConfig();
|
|
170
|
+
return refreshResolvedConfigOAuth(resolved, opts);
|
|
71
171
|
}
|
package/dist/coreClient.js
CHANGED
|
@@ -72,6 +72,7 @@ export async function verifyCoreAuthEndpoint(cfg, verbose = false) {
|
|
|
72
72
|
}
|
|
73
73
|
export async function callCoreTool(cfg, name, args, opts) {
|
|
74
74
|
const verbose = !!opts?.verbose;
|
|
75
|
+
const format = opts?.format || "json";
|
|
75
76
|
const headers = {
|
|
76
77
|
"Content-Type": "application/json",
|
|
77
78
|
Accept: "application/json",
|
|
@@ -80,7 +81,7 @@ export async function callCoreTool(cfg, name, args, opts) {
|
|
|
80
81
|
const body = {
|
|
81
82
|
tool_name: name,
|
|
82
83
|
arguments: args,
|
|
83
|
-
format
|
|
84
|
+
format,
|
|
84
85
|
};
|
|
85
86
|
if (verbose) {
|
|
86
87
|
const masked = {};
|
|
@@ -88,7 +89,6 @@ export async function callCoreTool(cfg, name, args, opts) {
|
|
|
88
89
|
masked[k] = k.toLowerCase() === "authorization" ? maskToken(v) : v;
|
|
89
90
|
}
|
|
90
91
|
console.error(`> POST ${cfg.coreUrl}`);
|
|
91
|
-
console.error(`> transport: core`);
|
|
92
92
|
console.error(`> headers: ${JSON.stringify(masked)}`);
|
|
93
93
|
console.error(`> body: ${JSON.stringify(body)}`);
|
|
94
94
|
}
|