tyc-cli 0.3.5 → 0.3.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 +49 -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 +135 -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 +66 -0
- package/dist/oauthPending.js +41 -0
- package/dist/types.js +3 -4
- package/dist/version.js +1 -1
- package/package.json +1 -1
- 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, 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,67 @@ 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 buildLoopbackRedirectUri(host, port, path) {
|
|
20
|
+
if (port === 0) {
|
|
21
|
+
throw new Error("--no-block 不能与 --redirect-port 0 同时使用,请指定固定回调端口。");
|
|
22
|
+
}
|
|
23
|
+
return `http://${host}:${port}${path}`;
|
|
24
|
+
}
|
|
25
|
+
function pickCallbackToken(opts) {
|
|
26
|
+
if (opts.callbackToken && opts.token) {
|
|
27
|
+
throw new Error("--callback-token 和 --token 只能指定一个。");
|
|
28
|
+
}
|
|
29
|
+
return opts.callbackToken || opts.token;
|
|
30
|
+
}
|
|
31
|
+
function saveOAuthTokenConfig(mcpURL, token, oauth, verbose) {
|
|
32
|
+
const cfg = loadConfig() || {};
|
|
33
|
+
cfg.url = mcpURL;
|
|
34
|
+
delete cfg.transport;
|
|
35
|
+
cfg.coreUrl = defaultCoreURL(mcpURL);
|
|
36
|
+
applyOAuthTokenToConfig(cfg, token, oauth);
|
|
37
|
+
saveConfig(cfg);
|
|
38
|
+
if (verbose) {
|
|
39
|
+
console.error(`> OAuth login state saved to ~/.tyc/config.json (url=${cfg.url})`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function verifySavedOAuthLogin(verify, verbose) {
|
|
43
|
+
if (!verify) {
|
|
44
|
+
printLoginSuccessBanner();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const resolved = resolveConfig();
|
|
48
|
+
await verifyCoreAuthEndpoint(resolved, verbose);
|
|
49
|
+
if (verbose) {
|
|
50
|
+
console.error("> Shared Core auth check passed");
|
|
51
|
+
}
|
|
52
|
+
printLoginSuccessBanner();
|
|
53
|
+
}
|
|
54
|
+
async function completePendingOAuthLogin(pending, callbackToken, verify, verbose) {
|
|
55
|
+
const code = parseAuthorizationCallbackCode(callbackToken, pending.state);
|
|
56
|
+
if (verbose) {
|
|
57
|
+
console.error("> authorization code provided; exchanging access token");
|
|
58
|
+
}
|
|
59
|
+
const token = await exchangeAuthorizationCode({
|
|
60
|
+
tokenEndpoint: pending.tokenEndpoint,
|
|
61
|
+
clientId: pending.clientId,
|
|
62
|
+
clientSecret: pending.clientSecret,
|
|
63
|
+
code,
|
|
64
|
+
redirectUri: pending.redirectUri,
|
|
65
|
+
codeVerifier: pending.codeVerifier,
|
|
66
|
+
resource: pending.resource,
|
|
67
|
+
});
|
|
68
|
+
saveOAuthTokenConfig(pending.mcpURL, token, {
|
|
69
|
+
tokenEndpoint: pending.tokenEndpoint,
|
|
70
|
+
clientId: pending.clientId,
|
|
71
|
+
clientSecret: pending.clientSecret,
|
|
72
|
+
resource: pending.resource,
|
|
73
|
+
}, verbose);
|
|
74
|
+
clearPendingOAuthLogin(verbose);
|
|
75
|
+
await verifySavedOAuthLogin(verify, verbose);
|
|
20
76
|
}
|
|
21
77
|
export function registerLoginCommand(program) {
|
|
22
78
|
program
|
|
@@ -34,9 +90,29 @@ export function registerLoginCommand(program) {
|
|
|
34
90
|
.option("--redirect-host <host>", "本地回调 host(默认 localhost)")
|
|
35
91
|
.option("--redirect-port <port>", "本地回调端口(默认 7078)")
|
|
36
92
|
.option("--callback-path <path>", "本地回调路径(默认 /oauth/callback)")
|
|
93
|
+
.option("--callback-token <code-or-url>", "继续非阻塞登录:传入授权回调 code 或包含 code 的完整 callback URL")
|
|
94
|
+
.option("--token <code-or-url>", "同 --callback-token(兼容别名;仍需传授权回调 code)")
|
|
37
95
|
.option("--no-open", "不自动打开浏览器,只打印授权 URL")
|
|
38
|
-
.option("--no-
|
|
96
|
+
.option("--no-block", "配合 --no-open:打印授权 URL 并保存 pending 状态后立即退出")
|
|
97
|
+
.option("--no-verify", "保存 token 后跳过 shared core 鉴权校验")
|
|
39
98
|
.action(async (opts) => {
|
|
99
|
+
const verbose = !!program.opts().verbose;
|
|
100
|
+
const callbackToken = pickCallbackToken(opts);
|
|
101
|
+
if (callbackToken) {
|
|
102
|
+
const pending = loadPendingOAuthLogin();
|
|
103
|
+
if (!pending) {
|
|
104
|
+
throw new Error("未找到待完成的 OAuth 登录状态。请先运行 tyc login --no-open --no-block。");
|
|
105
|
+
}
|
|
106
|
+
if (verbose) {
|
|
107
|
+
console.error(`> loaded pending OAuth login from ~/.tyc/oauth_pending.json (url=${pending.mcpURL})`);
|
|
108
|
+
}
|
|
109
|
+
await completePendingOAuthLogin(pending, callbackToken, opts.verify !== false && pending.verify !== false, verbose);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const noBlock = opts.block === false;
|
|
113
|
+
if (noBlock && opts.open !== false) {
|
|
114
|
+
throw new Error("--no-block 需要与 --no-open 一起使用。");
|
|
115
|
+
}
|
|
40
116
|
const current = loadConfig() || {};
|
|
41
117
|
const mcpURL = opts.url || process.env.TYC_MCP_ENDPOINT || current.url || DEFAULT_MCP_URL;
|
|
42
118
|
const knownDefaults = knownOAuthDefaultsForMcpURL(mcpURL);
|
|
@@ -45,8 +121,7 @@ export function registerLoginCommand(program) {
|
|
|
45
121
|
const scope = opts.scope || "mcp:tools.call";
|
|
46
122
|
const redirectHost = opts.redirectHost || "localhost";
|
|
47
123
|
const redirectPort = parsePort(opts.redirectPort || "7078");
|
|
48
|
-
const callbackPath = opts.callbackPath
|
|
49
|
-
const verbose = !!program.opts().verbose;
|
|
124
|
+
const callbackPath = normalizeCallbackPath(opts.callbackPath);
|
|
50
125
|
let issuer = opts.issuer || "";
|
|
51
126
|
let resource = opts.resource || "";
|
|
52
127
|
if (!opts.issuer || !opts.resource) {
|
|
@@ -108,16 +183,19 @@ export function registerLoginCommand(program) {
|
|
|
108
183
|
}
|
|
109
184
|
const state = randomState();
|
|
110
185
|
const pkce = createPKCE();
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
186
|
+
let callback;
|
|
187
|
+
const redirectUri = noBlock
|
|
188
|
+
? buildLoopbackRedirectUri(redirectHost, redirectPort, callbackPath)
|
|
189
|
+
: (callback = await startCallbackServer({
|
|
190
|
+
host: redirectHost,
|
|
191
|
+
port: redirectPort,
|
|
192
|
+
path: callbackPath,
|
|
193
|
+
state,
|
|
194
|
+
timeoutMs: 5 * 60 * 1000,
|
|
195
|
+
})).redirectUri;
|
|
118
196
|
if (!clientId) {
|
|
119
197
|
if (!endpoints.registrationEndpoint) {
|
|
120
|
-
callback
|
|
198
|
+
callback?.close();
|
|
121
199
|
throw new Error("OAuth metadata 未提供 registration_endpoint,请用 --client-id 指定预注册客户端。");
|
|
122
200
|
}
|
|
123
201
|
if (verbose) {
|
|
@@ -128,12 +206,12 @@ export function registerLoginCommand(program) {
|
|
|
128
206
|
registeredClient = await registerDynamicOAuthClient({
|
|
129
207
|
registrationEndpoint: endpoints.registrationEndpoint,
|
|
130
208
|
clientName: "tyc-cli",
|
|
131
|
-
redirectUris: [
|
|
209
|
+
redirectUris: [redirectUri],
|
|
132
210
|
tokenEndpointAuthMethod: "none",
|
|
133
211
|
});
|
|
134
212
|
}
|
|
135
213
|
catch (err) {
|
|
136
|
-
callback
|
|
214
|
+
callback?.close();
|
|
137
215
|
throw err;
|
|
138
216
|
}
|
|
139
217
|
clientId = registeredClient.clientId;
|
|
@@ -145,14 +223,36 @@ export function registerLoginCommand(program) {
|
|
|
145
223
|
const authorizationURL = buildAuthorizationURL({
|
|
146
224
|
authorizationEndpoint: endpoints.authorizationEndpoint,
|
|
147
225
|
clientId,
|
|
148
|
-
redirectUri
|
|
226
|
+
redirectUri,
|
|
149
227
|
scope,
|
|
150
228
|
state,
|
|
151
229
|
codeChallenge: pkce.codeChallenge,
|
|
152
230
|
resource,
|
|
153
231
|
});
|
|
154
232
|
if (verbose) {
|
|
155
|
-
console.error(`> OAuth callback: ${
|
|
233
|
+
console.error(`> OAuth callback: ${redirectUri}`);
|
|
234
|
+
}
|
|
235
|
+
if (noBlock) {
|
|
236
|
+
savePendingOAuthLogin({
|
|
237
|
+
version: 1,
|
|
238
|
+
createdAt: Date.now(),
|
|
239
|
+
mcpURL,
|
|
240
|
+
tokenEndpoint: endpoints.tokenEndpoint,
|
|
241
|
+
clientId,
|
|
242
|
+
clientSecret,
|
|
243
|
+
redirectUri,
|
|
244
|
+
codeVerifier: pkce.codeVerifier,
|
|
245
|
+
resource,
|
|
246
|
+
state,
|
|
247
|
+
verify: opts.verify !== false,
|
|
248
|
+
});
|
|
249
|
+
console.error("请在浏览器打开以下 URL 完成登录:");
|
|
250
|
+
console.error(authorizationURL);
|
|
251
|
+
console.error("");
|
|
252
|
+
console.error("完成授权后,复制回调 URL 中的 code 参数,然后运行:");
|
|
253
|
+
console.error("tyc login --callback-token <code_or_callback_url>");
|
|
254
|
+
console.error("也可以传完整 callback URL:tyc login --callback-token \"http://localhost:7078/oauth/callback?code=...&state=...\"");
|
|
255
|
+
return;
|
|
156
256
|
}
|
|
157
257
|
if (opts.open === false) {
|
|
158
258
|
console.error("请在浏览器打开以下 URL 完成登录:");
|
|
@@ -162,6 +262,9 @@ export function registerLoginCommand(program) {
|
|
|
162
262
|
console.error("正在打开浏览器完成 OAuth 登录...");
|
|
163
263
|
openBrowser(authorizationURL);
|
|
164
264
|
}
|
|
265
|
+
if (!callback) {
|
|
266
|
+
throw new Error("OAuth callback server not initialized");
|
|
267
|
+
}
|
|
165
268
|
const code = await callback.waitForCode();
|
|
166
269
|
if (verbose) {
|
|
167
270
|
console.error("> authorization code received; exchanging access token");
|
|
@@ -171,40 +274,17 @@ export function registerLoginCommand(program) {
|
|
|
171
274
|
clientId,
|
|
172
275
|
clientSecret,
|
|
173
276
|
code,
|
|
174
|
-
redirectUri
|
|
277
|
+
redirectUri,
|
|
175
278
|
codeVerifier: pkce.codeVerifier,
|
|
176
279
|
resource,
|
|
177
280
|
});
|
|
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();
|
|
281
|
+
saveOAuthTokenConfig(mcpURL, token, {
|
|
282
|
+
tokenEndpoint: endpoints.tokenEndpoint,
|
|
283
|
+
clientId,
|
|
284
|
+
clientSecret,
|
|
285
|
+
resource,
|
|
286
|
+
}, verbose);
|
|
287
|
+
clearPendingOAuthLogin(verbose);
|
|
288
|
+
await verifySavedOAuthLogin(opts.verify !== false, verbose);
|
|
209
289
|
});
|
|
210
290
|
}
|
|
@@ -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
|
}
|