tyc-cli 0.3.3 → 0.3.5
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/CHANGELOG.md +13 -0
- package/README.md +33 -17
- package/dist/catalog.json +380 -243
- package/dist/commands/category.js +4 -1
- package/dist/commands/init.js +32 -6
- package/dist/commands/layers.js +5 -5
- package/dist/commands/login.js +210 -0
- package/dist/config.js +30 -6
- package/dist/coreClient.js +121 -0
- package/dist/index.js +13 -7
- package/dist/logo.js +24 -0
- package/dist/oauth.js +364 -0
- package/package.json +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getCategories, getToolsByGroup } from "../registry.js";
|
|
2
2
|
import { resolveConfig } from "../config.js";
|
|
3
|
+
import { callCoreTool } from "../coreClient.js";
|
|
3
4
|
import { callTool } from "../mcpClient.js";
|
|
4
5
|
import { jsonToMarkdown } from "../utils/jsonToMarkdown.js";
|
|
5
6
|
import { applyTruncation, resolveOptionalInt, TRUNCATE_DEFAULTS, } from "../utils/truncate.js";
|
|
@@ -53,7 +54,9 @@ function bindMethod(catCmd, tool, program) {
|
|
|
53
54
|
const opts = program.opts();
|
|
54
55
|
const verbose = !!opts.verbose;
|
|
55
56
|
try {
|
|
56
|
-
const result =
|
|
57
|
+
const result = cfg.transport === "mcp"
|
|
58
|
+
? await callTool(cfg, tool.name, args, { verbose })
|
|
59
|
+
: await callCoreTool(cfg, tool.name, args, { verbose });
|
|
57
60
|
emit(result, opts, tool.name);
|
|
58
61
|
}
|
|
59
62
|
catch (err) {
|
package/dist/commands/init.js
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
|
-
import { DEFAULT_MCP_URL, loadConfig, resolveConfig, saveConfig } from "../config.js";
|
|
1
|
+
import { DEFAULT_MCP_URL, defaultCoreURL, loadConfig, resolveConfig, saveConfig, } from "../config.js";
|
|
2
|
+
import { verifyCoreEndpoint } from "../coreClient.js";
|
|
2
3
|
import { clearSession } from "../session.js";
|
|
3
4
|
import { ensureSession } from "../mcpClient.js";
|
|
5
|
+
function parseTransport(raw) {
|
|
6
|
+
if (!raw)
|
|
7
|
+
return undefined;
|
|
8
|
+
const v = raw.trim().toLowerCase();
|
|
9
|
+
if (v === "core" || v === "mcp")
|
|
10
|
+
return v;
|
|
11
|
+
throw new Error(`invalid --transport: ${raw} (expected core or mcp)`);
|
|
12
|
+
}
|
|
4
13
|
export function registerInitCommand(program) {
|
|
5
14
|
program
|
|
6
15
|
.command("init")
|
|
7
16
|
.description("配置 MCP endpoint / Authorization(保存到 ~/.tyc/config.json,并校验连通性)")
|
|
8
17
|
.option("--authorization <token>", "tyc OpenAPI Authorization(写入 headers.Authorization)")
|
|
9
18
|
.option("--url <url>", `MCP endpoint(默认 ${DEFAULT_MCP_URL})`)
|
|
19
|
+
.option("--core-url <url>", "Shared Core HTTP endpoint(默认从 --url 推导 /v1/core/tools/call)")
|
|
20
|
+
.option("--transport <core|mcp>", "默认调用通道(默认 core;mcp 保留旧 session 链路)")
|
|
10
21
|
.option("--header <kv...>", "自定义 header,格式 K=V,可重复;留空清除该 key")
|
|
11
22
|
.option("--clear-session", "仅清除本地 MCP session 缓存(~/.tyc/session.json)")
|
|
12
23
|
.option("--no-verify", "跳过连通性校验(不发 initialize)")
|
|
@@ -18,6 +29,15 @@ export function registerInitCommand(program) {
|
|
|
18
29
|
cfg.url = opts.url;
|
|
19
30
|
if (!cfg.url)
|
|
20
31
|
cfg.url = DEFAULT_MCP_URL;
|
|
32
|
+
const transport = parseTransport(opts.transport);
|
|
33
|
+
if (transport)
|
|
34
|
+
cfg.transport = transport;
|
|
35
|
+
if (!cfg.transport)
|
|
36
|
+
cfg.transport = "core";
|
|
37
|
+
if (opts.coreUrl)
|
|
38
|
+
cfg.coreUrl = opts.coreUrl;
|
|
39
|
+
if (!cfg.coreUrl)
|
|
40
|
+
cfg.coreUrl = defaultCoreURL(cfg.url);
|
|
21
41
|
if (opts.authorization)
|
|
22
42
|
cfg.headers.Authorization = opts.authorization;
|
|
23
43
|
if (opts.header && opts.header.length > 0) {
|
|
@@ -43,20 +63,26 @@ export function registerInitCommand(program) {
|
|
|
43
63
|
if (opts.clearSession) {
|
|
44
64
|
console.log("已清除 ~/.tyc/session.json");
|
|
45
65
|
}
|
|
46
|
-
console.log(`已保存 ~/.tyc/config.json (url=${cfg.url})`);
|
|
66
|
+
console.log(`已保存 ~/.tyc/config.json (transport=${cfg.transport}, url=${cfg.url}, coreUrl=${cfg.coreUrl})`);
|
|
47
67
|
if (opts.verify === false) {
|
|
48
68
|
return;
|
|
49
69
|
}
|
|
50
|
-
//
|
|
70
|
+
// core 模式只探活 stateless endpoint;mcp 模式才 initialize 并缓存 session。
|
|
51
71
|
try {
|
|
52
72
|
const resolved = resolveConfig();
|
|
53
73
|
const verbose = !!program.opts().verbose;
|
|
54
|
-
|
|
55
|
-
|
|
74
|
+
if (resolved.transport === "mcp") {
|
|
75
|
+
const sess = await ensureSession(resolved, { verbose });
|
|
76
|
+
console.log(`已建立 MCP session(sessionId=${sess.sessionId.slice(0, 16)}…)`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
await verifyCoreEndpoint(resolved, verbose);
|
|
80
|
+
console.log("Shared Core endpoint 校验通过");
|
|
81
|
+
}
|
|
56
82
|
}
|
|
57
83
|
catch (err) {
|
|
58
84
|
const msg = err instanceof Error ? err.message : String(err);
|
|
59
|
-
console.error(
|
|
85
|
+
console.error(`连通性校验失败:${msg}`);
|
|
60
86
|
console.error("配置已保存;确认 --url 可达后重新运行 tyc init,或加 --no-verify 跳过校验。");
|
|
61
87
|
process.exit(1);
|
|
62
88
|
}
|
package/dist/commands/layers.js
CHANGED
|
@@ -12,7 +12,7 @@ const LAYER_SPECS = [
|
|
|
12
12
|
title: "Overview · 概要层",
|
|
13
13
|
summary: "**6 个 facet 队长,每 facet 1 个总览**:company-base / 风险 / 知产 / 经营信用 / 历史 / 董监高 各 1 个 L1 工具,单次调用聚合该 facet 多子维度。",
|
|
14
14
|
trigger: '"这家公司整体怎么样 / 风险如何 / 知产实力 / 经营状况 / 历史变更 / 这个人是谁"',
|
|
15
|
-
contract: "Agent 按 facet 选 1 个 L1 调用即得 _summary
|
|
15
|
+
contract: "Agent 按 facet 选 1 个 L1 调用即得 _summary;下一跳可用 tyc L2 list 或 MCP get_company_capabilities 确认工具。",
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
layer: "L2",
|
|
@@ -63,7 +63,7 @@ function registerLayers(program) {
|
|
|
63
63
|
recommended_call_order: [
|
|
64
64
|
`0. Anchor at L0 — feed the user's raw company name into the ${getLayerCount("L0")} L0 tool (search_companies); lock onto a USCC + official name`,
|
|
65
65
|
`1. Ascend to L1 — pick ONE of the ${getLayerCount("L1")} overview tools with the anchored USCC`,
|
|
66
|
-
"2. Read _summary
|
|
66
|
+
"2. Read _summary and pick the next dimension via tyc layer lists or MCP get_company_capabilities",
|
|
67
67
|
`3. Drill to L2 for a specific dimension (items + totals; ${getLayerCount("L2")} tools); when unsure which L2, default to the facet's L2 优先 (★, see l2_priority)`,
|
|
68
68
|
`4. Reach L3 for id-based detail, search_*, or vertical SKILL tools (${getLayerCount("L3")} tools)`,
|
|
69
69
|
],
|
|
@@ -130,7 +130,7 @@ function renderLayersText() {
|
|
|
130
130
|
lines.push("Rationale: LLM tool-selection accuracy collapses beyond 30 tools.");
|
|
131
131
|
lines.push(` tyc carves L0 into a single entity-resolution tool, so the default LLM`);
|
|
132
132
|
lines.push(` surface is L0 + L1 = ${defaultSurface} tools (≤ 15-tool exposure limit);`);
|
|
133
|
-
lines.push(" L2/L3 are discovered on demand via _summary
|
|
133
|
+
lines.push(" L2/L3 are discovered on demand via _summary, layer lists, or get_company_capabilities.");
|
|
134
134
|
lines.push("");
|
|
135
135
|
for (const s of LAYER_SPECS) {
|
|
136
136
|
const n = getLayerCount(s.layer);
|
|
@@ -153,7 +153,7 @@ function renderLayersText() {
|
|
|
153
153
|
lines.push("Recommended call order for agents:");
|
|
154
154
|
lines.push(` 0. Anchor at L0 — feed the user's raw company name into the ${getLayerCount("L0")} L0 tool (search_companies)`);
|
|
155
155
|
lines.push(` 1. Ascend to L1 — one of the ${getLayerCount("L1")} overview tools, with the anchored USCC`);
|
|
156
|
-
lines.push(" 2. Read _summary
|
|
156
|
+
lines.push(" 2. Read _summary, then choose the next dimension via layer lists or get_company_capabilities");
|
|
157
157
|
lines.push(` 3. Drill to L2 for a specific dimension (${getLayerCount("L2")} tools); unsure which L2 → default to that facet's L2★ 优先 (see L2 block above)`);
|
|
158
158
|
lines.push(` 4. Reach L3 for id-based detail / search_* / vertical SKILL (${getLayerCount("L3")} tools)`);
|
|
159
159
|
return lines.join("\n");
|
|
@@ -188,7 +188,7 @@ function renderLayersMarkdown() {
|
|
|
188
188
|
lines.push("");
|
|
189
189
|
lines.push(`0. Anchor at L0 — feed the user's raw company name into the ${getLayerCount("L0")} L0 tool (\`search_companies\`); lock onto a USCC + official name.`);
|
|
190
190
|
lines.push(`1. Ascend to L1 — pick ONE of the ${getLayerCount("L1")} overview tools with the anchored USCC.`);
|
|
191
|
-
lines.push("2. Read `_summary
|
|
191
|
+
lines.push("2. Read `_summary`, then choose the next dimension via layer lists or MCP `get_company_capabilities`.");
|
|
192
192
|
lines.push(`3. Drill to L2 for a specific dimension (items + totals; ${getLayerCount("L2")} tools). Unsure which L2 → default to that facet's L2★ 优先 (table above).`);
|
|
193
193
|
lines.push(`4. Reach L3 for id-based detail, \`search_*\`, or vertical SKILL tools (${getLayerCount("L3")} tools).`);
|
|
194
194
|
lines.push("");
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { DEFAULT_MCP_URL, defaultCoreURL, loadConfig, resolveConfig, saveConfig, } from "../config.js";
|
|
2
|
+
import { verifyCoreAuthEndpoint } from "../coreClient.js";
|
|
3
|
+
import { clearSession } from "../session.js";
|
|
4
|
+
import { ensureSession } from "../mcpClient.js";
|
|
5
|
+
import { buildAuthorizationURL, createPKCE, discoverAuthorizationServer, discoverProtectedResourceMetadata, exchangeAuthorizationCode, knownOAuthDefaultsForMcpURL, openBrowser, randomState, registerDynamicOAuthClient, startCallbackServer, } from "../oauth.js";
|
|
6
|
+
import { printLoginSuccessBanner } from "../logo.js";
|
|
7
|
+
function parsePort(raw) {
|
|
8
|
+
if (!raw)
|
|
9
|
+
return 0;
|
|
10
|
+
const n = Number(raw);
|
|
11
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535) {
|
|
12
|
+
throw new Error(`invalid --redirect-port: ${raw}`);
|
|
13
|
+
}
|
|
14
|
+
return n;
|
|
15
|
+
}
|
|
16
|
+
function normalizeTokenType(raw) {
|
|
17
|
+
if (!raw)
|
|
18
|
+
return "Bearer";
|
|
19
|
+
return raw.toLowerCase() === "bearer" ? "Bearer" : raw;
|
|
20
|
+
}
|
|
21
|
+
export function registerLoginCommand(program) {
|
|
22
|
+
program
|
|
23
|
+
.command("login")
|
|
24
|
+
.description("通过 OAuth 浏览器登录并自动写入 ~/.tyc/config.json")
|
|
25
|
+
.option("--url <url>", `MCP endpoint(默认 ${DEFAULT_MCP_URL},或沿用当前配置)`)
|
|
26
|
+
.option("--issuer <url>", "OAuth authorization server issuer(默认从 MCP metadata 发现)")
|
|
27
|
+
.option("--client-id <id>", "OAuth client_id(默认通过 DCR 自动注册)")
|
|
28
|
+
.option("--client-secret <secret>", "OAuth client_secret(默认 TYC_OAUTH_CLIENT_SECRET;公共 CLI 通常不需要)")
|
|
29
|
+
.option("--scope <scope>", "OAuth scope(默认 mcp:tools.call)")
|
|
30
|
+
.option("--resource <url>", "OAuth resource/audience(默认从 MCP metadata 发现)")
|
|
31
|
+
.option("--resource-metadata-url <url>", "手动指定 MCP protected resource metadata URL")
|
|
32
|
+
.option("--authorization-endpoint <url>", "手动指定 OAuth authorization endpoint")
|
|
33
|
+
.option("--token-endpoint <url>", "手动指定 OAuth token endpoint")
|
|
34
|
+
.option("--redirect-host <host>", "本地回调 host(默认 localhost)")
|
|
35
|
+
.option("--redirect-port <port>", "本地回调端口(默认 7078)")
|
|
36
|
+
.option("--callback-path <path>", "本地回调路径(默认 /oauth/callback)")
|
|
37
|
+
.option("--no-open", "不自动打开浏览器,只打印授权 URL")
|
|
38
|
+
.option("--no-verify", "保存 token 后跳过 initialize 连通性校验")
|
|
39
|
+
.action(async (opts) => {
|
|
40
|
+
const current = loadConfig() || {};
|
|
41
|
+
const mcpURL = opts.url || process.env.TYC_MCP_ENDPOINT || current.url || DEFAULT_MCP_URL;
|
|
42
|
+
const knownDefaults = knownOAuthDefaultsForMcpURL(mcpURL);
|
|
43
|
+
let clientId = opts.clientId || process.env.TYC_OAUTH_CLIENT_ID || "";
|
|
44
|
+
let clientSecret = opts.clientSecret || process.env.TYC_OAUTH_CLIENT_SECRET;
|
|
45
|
+
const scope = opts.scope || "mcp:tools.call";
|
|
46
|
+
const redirectHost = opts.redirectHost || "localhost";
|
|
47
|
+
const redirectPort = parsePort(opts.redirectPort || "7078");
|
|
48
|
+
const callbackPath = opts.callbackPath || "/oauth/callback";
|
|
49
|
+
const verbose = !!program.opts().verbose;
|
|
50
|
+
let issuer = opts.issuer || "";
|
|
51
|
+
let resource = opts.resource || "";
|
|
52
|
+
if (!opts.issuer || !opts.resource) {
|
|
53
|
+
if (verbose) {
|
|
54
|
+
console.error(`> discovering MCP OAuth metadata: ${mcpURL}`);
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const protectedResource = await discoverProtectedResourceMetadata(mcpURL, opts.resourceMetadataUrl);
|
|
58
|
+
if (verbose) {
|
|
59
|
+
console.error(`> protected resource metadata: ${protectedResource.metadataURL}`);
|
|
60
|
+
}
|
|
61
|
+
const metadataIssuer = protectedResource.metadata.authorization_servers?.[0];
|
|
62
|
+
if (!issuer && metadataIssuer) {
|
|
63
|
+
issuer = metadataIssuer;
|
|
64
|
+
}
|
|
65
|
+
resource ||= protectedResource.metadata.resource || mcpURL;
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
issuer ||= knownDefaults.issuer || "";
|
|
69
|
+
resource ||= knownDefaults.resource || "";
|
|
70
|
+
if (!issuer || !resource) {
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
if (verbose) {
|
|
74
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
75
|
+
console.error(`> metadata discovery failed, using known defaults: ${msg}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else if (verbose) {
|
|
80
|
+
console.error("> using explicit --issuer and --resource; skip protected resource metadata discovery");
|
|
81
|
+
}
|
|
82
|
+
if (!issuer) {
|
|
83
|
+
throw new Error("MCP metadata 未提供 authorization_servers,请用 --issuer 指定。");
|
|
84
|
+
}
|
|
85
|
+
// 双端点都手动指定时跳过自动发现;否则 metadata 发现失败会直接报错
|
|
86
|
+
// (discovery 不再猜测端点,见 discoverAuthorizationServer)。
|
|
87
|
+
let endpoints;
|
|
88
|
+
if (opts.authorizationEndpoint && opts.tokenEndpoint) {
|
|
89
|
+
endpoints = {
|
|
90
|
+
authorizationEndpoint: opts.authorizationEndpoint,
|
|
91
|
+
tokenEndpoint: opts.tokenEndpoint,
|
|
92
|
+
issuer,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
endpoints = await discoverAuthorizationServer(issuer);
|
|
97
|
+
endpoints.authorizationEndpoint =
|
|
98
|
+
opts.authorizationEndpoint || endpoints.authorizationEndpoint;
|
|
99
|
+
endpoints.tokenEndpoint = opts.tokenEndpoint || endpoints.tokenEndpoint;
|
|
100
|
+
}
|
|
101
|
+
resource ||= endpoints.resourceDocumentation || mcpURL;
|
|
102
|
+
if (verbose) {
|
|
103
|
+
console.error(`> authorization endpoint: ${endpoints.authorizationEndpoint}`);
|
|
104
|
+
console.error(`> token endpoint: ${endpoints.tokenEndpoint}`);
|
|
105
|
+
if (endpoints.registrationEndpoint) {
|
|
106
|
+
console.error(`> registration endpoint: ${endpoints.registrationEndpoint}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const state = randomState();
|
|
110
|
+
const pkce = createPKCE();
|
|
111
|
+
const callback = await startCallbackServer({
|
|
112
|
+
host: redirectHost,
|
|
113
|
+
port: redirectPort,
|
|
114
|
+
path: callbackPath.startsWith("/") ? callbackPath : `/${callbackPath}`,
|
|
115
|
+
state,
|
|
116
|
+
timeoutMs: 5 * 60 * 1000,
|
|
117
|
+
});
|
|
118
|
+
if (!clientId) {
|
|
119
|
+
if (!endpoints.registrationEndpoint) {
|
|
120
|
+
callback.close();
|
|
121
|
+
throw new Error("OAuth metadata 未提供 registration_endpoint,请用 --client-id 指定预注册客户端。");
|
|
122
|
+
}
|
|
123
|
+
if (verbose) {
|
|
124
|
+
console.error("> registering OAuth client via Dynamic Client Registration");
|
|
125
|
+
}
|
|
126
|
+
let registeredClient;
|
|
127
|
+
try {
|
|
128
|
+
registeredClient = await registerDynamicOAuthClient({
|
|
129
|
+
registrationEndpoint: endpoints.registrationEndpoint,
|
|
130
|
+
clientName: "tyc-cli",
|
|
131
|
+
redirectUris: [callback.redirectUri],
|
|
132
|
+
tokenEndpointAuthMethod: "none",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
callback.close();
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
clientId = registeredClient.clientId;
|
|
140
|
+
clientSecret ||= registeredClient.clientSecret;
|
|
141
|
+
if (verbose) {
|
|
142
|
+
console.error(`> registered client_id: ${clientId}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const authorizationURL = buildAuthorizationURL({
|
|
146
|
+
authorizationEndpoint: endpoints.authorizationEndpoint,
|
|
147
|
+
clientId,
|
|
148
|
+
redirectUri: callback.redirectUri,
|
|
149
|
+
scope,
|
|
150
|
+
state,
|
|
151
|
+
codeChallenge: pkce.codeChallenge,
|
|
152
|
+
resource,
|
|
153
|
+
});
|
|
154
|
+
if (verbose) {
|
|
155
|
+
console.error(`> OAuth callback: ${callback.redirectUri}`);
|
|
156
|
+
}
|
|
157
|
+
if (opts.open === false) {
|
|
158
|
+
console.error("请在浏览器打开以下 URL 完成登录:");
|
|
159
|
+
console.error(authorizationURL);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
console.error("正在打开浏览器完成 OAuth 登录...");
|
|
163
|
+
openBrowser(authorizationURL);
|
|
164
|
+
}
|
|
165
|
+
const code = await callback.waitForCode();
|
|
166
|
+
if (verbose) {
|
|
167
|
+
console.error("> authorization code received; exchanging access token");
|
|
168
|
+
}
|
|
169
|
+
const token = await exchangeAuthorizationCode({
|
|
170
|
+
tokenEndpoint: endpoints.tokenEndpoint,
|
|
171
|
+
clientId,
|
|
172
|
+
clientSecret,
|
|
173
|
+
code,
|
|
174
|
+
redirectUri: callback.redirectUri,
|
|
175
|
+
codeVerifier: pkce.codeVerifier,
|
|
176
|
+
resource,
|
|
177
|
+
});
|
|
178
|
+
const cfg = loadConfig() || {};
|
|
179
|
+
cfg.url = mcpURL;
|
|
180
|
+
if (!cfg.transport)
|
|
181
|
+
cfg.transport = "core";
|
|
182
|
+
if (!cfg.coreUrl)
|
|
183
|
+
cfg.coreUrl = defaultCoreURL(mcpURL);
|
|
184
|
+
cfg.headers = { ...(cfg.headers || {}) };
|
|
185
|
+
cfg.headers.Authorization = `${normalizeTokenType(token.token_type)} ${token.access_token}`;
|
|
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();
|
|
209
|
+
});
|
|
210
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
// ~/.tyc/config.json 读写
|
|
2
2
|
//
|
|
3
3
|
// 配置文件格式与通用 MCP 客户端保持一致:
|
|
4
|
-
// { "url": "https://mcp.tianyancha.com/
|
|
5
|
-
// "headers": { "Authorization": "xxxx" }
|
|
4
|
+
// { "url": "https://mcp.tianyancha.com/mcp",
|
|
5
|
+
// "headers": { "Authorization": "xxxx" },
|
|
6
|
+
// "transport": "core",
|
|
7
|
+
// "coreUrl": "https://mcp.tianyancha.com/v1/core/tools/call" }
|
|
6
8
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
+
// 默认端点指向天眼查公网 MCP Server 的 canonical 入口 /mcp(/v1 是兼容别名,
|
|
10
|
+
// coreUrl 的 REST 端点仍只挂在 /v1/core 下);连接本地或私有环境可通过
|
|
11
|
+
// tyc init --url http://localhost:8080/mcp 或 TYC_MCP_ENDPOINT 环境变量覆盖。
|
|
9
12
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "node:fs";
|
|
10
13
|
import { join } from "node:path";
|
|
11
14
|
import { homedir } from "node:os";
|
|
12
15
|
const CONFIG_DIR = join(homedir(), ".tyc");
|
|
13
16
|
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
14
|
-
export const DEFAULT_MCP_URL = "https://mcp.tianyancha.com/
|
|
17
|
+
export const DEFAULT_MCP_URL = "https://mcp.tianyancha.com/mcp";
|
|
15
18
|
export function loadConfig() {
|
|
16
19
|
if (!existsSync(CONFIG_FILE))
|
|
17
20
|
return null;
|
|
@@ -32,9 +35,30 @@ export function saveConfig(config) {
|
|
|
32
35
|
}
|
|
33
36
|
catch { /* best effort */ }
|
|
34
37
|
}
|
|
38
|
+
function normalizeTransport(raw) {
|
|
39
|
+
const v = (raw || "").trim().toLowerCase();
|
|
40
|
+
if (v === "mcp")
|
|
41
|
+
return "mcp";
|
|
42
|
+
return "core";
|
|
43
|
+
}
|
|
44
|
+
export function defaultCoreURL(mcpURL) {
|
|
45
|
+
const u = new URL(mcpURL);
|
|
46
|
+
const path = u.pathname.replace(/\/+$/, "");
|
|
47
|
+
if (path === "" || path === "/v1" || path === "/mcp") {
|
|
48
|
+
u.pathname = "/v1/core/tools/call";
|
|
49
|
+
}
|
|
50
|
+
else if (!path.endsWith("/core/tools/call")) {
|
|
51
|
+
u.pathname = `${path}/core/tools/call`;
|
|
52
|
+
}
|
|
53
|
+
u.search = "";
|
|
54
|
+
u.hash = "";
|
|
55
|
+
return u.toString();
|
|
56
|
+
}
|
|
35
57
|
export function resolveConfig() {
|
|
36
58
|
const cfg = loadConfig() || {};
|
|
37
59
|
const url = process.env.TYC_MCP_ENDPOINT || cfg.url || DEFAULT_MCP_URL;
|
|
60
|
+
const transport = normalizeTransport(process.env.TYC_TRANSPORT || cfg.transport);
|
|
61
|
+
const coreUrl = process.env.TYC_CORE_ENDPOINT || cfg.coreUrl || defaultCoreURL(url);
|
|
38
62
|
const headers = { ...(cfg.headers || {}) };
|
|
39
63
|
if (!headers.Authorization && process.env.TYC_AUTHORIZATION) {
|
|
40
64
|
headers.Authorization = process.env.TYC_AUTHORIZATION;
|
|
@@ -43,5 +67,5 @@ export function resolveConfig() {
|
|
|
43
67
|
console.error("未配置 Authorization。请先运行:tyc init --authorization YOUR_API_KEY");
|
|
44
68
|
process.exit(1);
|
|
45
69
|
}
|
|
46
|
-
return { url, headers };
|
|
70
|
+
return { url, coreUrl, transport, headers };
|
|
47
71
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
function maskToken(v) {
|
|
2
|
+
if (!v)
|
|
3
|
+
return "";
|
|
4
|
+
if (v.length <= 8)
|
|
5
|
+
return "****";
|
|
6
|
+
return v.slice(0, 4) + "****" + v.slice(-4);
|
|
7
|
+
}
|
|
8
|
+
function coreSiblingURL(coreUrl, siblingPath) {
|
|
9
|
+
const u = new URL(coreUrl);
|
|
10
|
+
const path = u.pathname.replace(/\/+$/, "");
|
|
11
|
+
if (path.endsWith("/tools/call")) {
|
|
12
|
+
u.pathname = path.slice(0, -"/tools/call".length) + siblingPath;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
u.pathname = path + siblingPath;
|
|
16
|
+
}
|
|
17
|
+
u.search = "";
|
|
18
|
+
u.hash = "";
|
|
19
|
+
return u.toString();
|
|
20
|
+
}
|
|
21
|
+
function readyURL(coreUrl) {
|
|
22
|
+
return coreSiblingURL(coreUrl, "/ready");
|
|
23
|
+
}
|
|
24
|
+
function authReadyURL(coreUrl) {
|
|
25
|
+
return coreSiblingURL(coreUrl, "/auth/ready");
|
|
26
|
+
}
|
|
27
|
+
export async function verifyCoreEndpoint(cfg, verbose = false) {
|
|
28
|
+
const url = readyURL(cfg.coreUrl);
|
|
29
|
+
if (verbose)
|
|
30
|
+
console.error(`> GET ${url}`);
|
|
31
|
+
const resp = await fetch(url, {
|
|
32
|
+
method: "GET",
|
|
33
|
+
headers: { Accept: "text/plain" },
|
|
34
|
+
});
|
|
35
|
+
const text = await resp.text();
|
|
36
|
+
if (verbose) {
|
|
37
|
+
console.error(`< ${resp.status} ${resp.statusText}`);
|
|
38
|
+
console.error(`< body: ${text.slice(0, 500)}`);
|
|
39
|
+
}
|
|
40
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
41
|
+
throw new Error(`core endpoint 校验失败:HTTP ${resp.status}\n${text.slice(0, 500)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function verifyCoreAuthEndpoint(cfg, verbose = false) {
|
|
45
|
+
const url = authReadyURL(cfg.coreUrl);
|
|
46
|
+
if (verbose)
|
|
47
|
+
console.error(`> GET ${url}`);
|
|
48
|
+
const headers = {
|
|
49
|
+
Accept: "application/json, text/plain",
|
|
50
|
+
...cfg.headers,
|
|
51
|
+
};
|
|
52
|
+
const resp = await fetch(url, {
|
|
53
|
+
method: "GET",
|
|
54
|
+
headers,
|
|
55
|
+
});
|
|
56
|
+
const text = await resp.text();
|
|
57
|
+
if (verbose) {
|
|
58
|
+
console.error(`< ${resp.status} ${resp.statusText}`);
|
|
59
|
+
console.error(`< body: ${text.slice(0, 500)}`);
|
|
60
|
+
}
|
|
61
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
62
|
+
let desc = text.slice(0, 500);
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(text);
|
|
65
|
+
desc = String(parsed.error_description || parsed.error || desc);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// keep raw text
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`core/auth/ready HTTP ${resp.status}\n${desc}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export async function callCoreTool(cfg, name, args, opts) {
|
|
74
|
+
const verbose = !!opts?.verbose;
|
|
75
|
+
const headers = {
|
|
76
|
+
"Content-Type": "application/json",
|
|
77
|
+
Accept: "application/json",
|
|
78
|
+
...cfg.headers,
|
|
79
|
+
};
|
|
80
|
+
const body = {
|
|
81
|
+
tool_name: name,
|
|
82
|
+
arguments: args,
|
|
83
|
+
format: "json",
|
|
84
|
+
};
|
|
85
|
+
if (verbose) {
|
|
86
|
+
const masked = {};
|
|
87
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
88
|
+
masked[k] = k.toLowerCase() === "authorization" ? maskToken(v) : v;
|
|
89
|
+
}
|
|
90
|
+
console.error(`> POST ${cfg.coreUrl}`);
|
|
91
|
+
console.error(`> transport: core`);
|
|
92
|
+
console.error(`> headers: ${JSON.stringify(masked)}`);
|
|
93
|
+
console.error(`> body: ${JSON.stringify(body)}`);
|
|
94
|
+
}
|
|
95
|
+
const resp = await fetch(cfg.coreUrl, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers,
|
|
98
|
+
body: JSON.stringify(body),
|
|
99
|
+
});
|
|
100
|
+
const text = await resp.text();
|
|
101
|
+
if (verbose) {
|
|
102
|
+
console.error(`< ${resp.status} ${resp.statusText}`);
|
|
103
|
+
console.error(`< body: ${text.slice(0, 2000)}`);
|
|
104
|
+
}
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(text);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
throw new Error(`core 响应不是 JSON:HTTP ${resp.status}\n${text.slice(0, 500)}`);
|
|
111
|
+
}
|
|
112
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
113
|
+
const desc = parsed.error_description || parsed.error || text.slice(0, 500);
|
|
114
|
+
throw new Error(`core/tools/call HTTP ${resp.status}\n${desc}`);
|
|
115
|
+
}
|
|
116
|
+
const content = parsed.content;
|
|
117
|
+
const contentText = typeof content === "string" ? content : JSON.stringify(content ?? {});
|
|
118
|
+
return {
|
|
119
|
+
content: [{ type: "text", text: contentText }],
|
|
120
|
+
};
|
|
121
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { registerInitCommand } from "./commands/init.js";
|
|
4
|
+
import { registerLoginCommand } from "./commands/login.js";
|
|
4
5
|
import { registerCategoryCommands } from "./commands/category.js";
|
|
5
6
|
import { registerLayerCommands } from "./commands/layers.js";
|
|
6
7
|
import { getCategories, getLayerCount, getPriorityTools, getTotalCount, } from "./registry.js";
|
|
@@ -16,7 +17,7 @@ ARCHITECTURE — Layered Tool Discovery for LLM Agents
|
|
|
16
17
|
entity-resolution layer:
|
|
17
18
|
|
|
18
19
|
┌─ L0 Resolve ${pad(getLayerCount("L0"), 3)} tool entity resolution · 简称/曾用名/模糊名 → 精确企业
|
|
19
|
-
├─ L1 Overview ${pad(getLayerCount("L1"), 3)} tools one captain per facet · returns _summary
|
|
20
|
+
├─ L1 Overview ${pad(getLayerCount("L1"), 3)} tools one captain per facet · returns _summary for routing
|
|
20
21
|
├─ L2 Drill-down ${pad(getLayerCount("L2"), 3)} tools one-hop into a specific data dimension (★ = L2 优先, see below)
|
|
21
22
|
└─ L3 Specialized ${pad(getLayerCount("L3"), 3)} tools id-based detail · search_* · vertical SKILLs
|
|
22
23
|
|
|
@@ -26,8 +27,9 @@ ARCHITECTURE — Layered Tool Discovery for LLM Agents
|
|
|
26
27
|
指代) into \`search_companies\`; lock onto one entity
|
|
27
28
|
with a USCC + official name before anything else.
|
|
28
29
|
1. Ascend to L1. Pick ONE of the ${getLayerCount("L1")} overview tools with the
|
|
29
|
-
anchored USCC. Read
|
|
30
|
-
|
|
30
|
+
anchored USCC. Read _summary, then use layer lists or
|
|
31
|
+
MCP get_company_capabilities for the next tool.
|
|
32
|
+
2. Descend to L2. Pick a specific dimension
|
|
31
33
|
(shareholders / litigation / patents / annual reports …).
|
|
32
34
|
If unsure which L2, default to that facet's L2★ 优先 (below).
|
|
33
35
|
3. Land on L3. When L2 returns a list item with an \`id\`, a \`search_*\`
|
|
@@ -66,7 +68,7 @@ OUTPUT FORMATS (mutually exclusive, priority: --md > --compact > --pretty / defa
|
|
|
66
68
|
--pretty same as default (kept for backward compatibility / explicit intent)
|
|
67
69
|
--compact compact single-line JSON (pipe / jq friendly; old default behavior)
|
|
68
70
|
--md Markdown tables (human + agent on-screen)
|
|
69
|
-
--verbose also print MCP request details to stderr (orthogonal to above)
|
|
71
|
+
--verbose also print core/MCP request details to stderr (orthogonal to above)
|
|
70
72
|
|
|
71
73
|
OUTPUT TRUNCATION & DUMP (orthogonal — apply to any sub-command)
|
|
72
74
|
|
|
@@ -86,8 +88,11 @@ OUTPUT TRUNCATION & DUMP (orthogonal — apply to any sub-command)
|
|
|
86
88
|
|
|
87
89
|
SETUP
|
|
88
90
|
|
|
89
|
-
tyc
|
|
90
|
-
tyc
|
|
91
|
+
tyc login OAuth browser login
|
|
92
|
+
tyc login --url <MCP_URL> OAuth for local / pre / self-hosted
|
|
93
|
+
tyc init --authorization <KEY> API-key compatibility path (default transport: core)
|
|
94
|
+
tyc init --transport mcp --authorization <KEY> legacy MCP session transport
|
|
95
|
+
tyc init --url <MCP_URL> --authorization <KEY> local / self-hosted service
|
|
91
96
|
|
|
92
97
|
Every tool returns tyc OpenAPI native English keys verbatim. The MCP Server
|
|
93
98
|
handles multi-source merge, Asia/Shanghai timestamp formatting, _summary
|
|
@@ -119,7 +124,7 @@ const program = new Command()
|
|
|
119
124
|
.option("--pretty", "缩进 JSON 输出(默认行为,flag 保留以保持向后兼容)")
|
|
120
125
|
.option("--md", "Markdown 表格化输出(适合人类阅读 / Agent 上屏)")
|
|
121
126
|
.option("--compact", "紧凑单行 JSON(管道 / jq 场景;为旧默认行为)")
|
|
122
|
-
.option("--verbose", "打印 MCP 请求详情到 stderr")
|
|
127
|
+
.option("--verbose", "打印 core/MCP 请求详情到 stderr")
|
|
123
128
|
.option("--head [n]", "仅输出前 N 行(不传值默认 50;与 --tail 同时给则同时输出两端)")
|
|
124
129
|
.option("--tail [m]", "仅输出后 M 行(不传值默认 20;与 --head 同时给则同时输出两端)")
|
|
125
130
|
.option("--full", "强制输出完整内容(最高优先级,覆盖 --head/--tail/--threshold)")
|
|
@@ -127,6 +132,7 @@ const program = new Command()
|
|
|
127
132
|
.option("--threshold <bytes>", "字节截断主开关:超过该字节数则从头按字节截断;不传则永不截断")
|
|
128
133
|
.addHelpText("after", helpText);
|
|
129
134
|
registerInitCommand(program);
|
|
135
|
+
registerLoginCommand(program);
|
|
130
136
|
registerLayerCommands(program);
|
|
131
137
|
registerCategoryCommands(program);
|
|
132
138
|
program.parseAsync(process.argv).catch((err) => {
|
package/dist/logo.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const TY_LOGO = `\x1b[38;2;22;135;245m ▄▄▄████████▄▄▄
|
|
2
|
+
▄▄█████████▀▀▀▀▀▀▀▀▀
|
|
3
|
+
▄████████▀▀▄▄▄▄██████▄▄▄▄
|
|
4
|
+
▄████████████████████████████▄
|
|
5
|
+
▄███████████████████▀▀▀▀▀▀▀████▄
|
|
6
|
+
▄██████████████▀▀ ███
|
|
7
|
+
████████████▀▀ ▄▄▀ █▀
|
|
8
|
+
███████████▀ ▄▄██▀
|
|
9
|
+
█████████▀ ▄█████ ▄
|
|
10
|
+
███████▀ ▄██████▀ ▄█
|
|
11
|
+
██████▀ ▄███████ █ ▄███
|
|
12
|
+
████ █████████ █▄ ▄▄▄▄▄▄▄▄▄█████
|
|
13
|
+
▀██ ██████████ ██▄ ▀▀██████████▀
|
|
14
|
+
▀ ███████████▄ ████▄ ▀▀▀▀▀▀▀▀
|
|
15
|
+
█████████████ ██████▄▄▄
|
|
16
|
+
▀███████████▄ ▀██████████▀
|
|
17
|
+
▀▀█████████▄ ▀██████▀▀
|
|
18
|
+
▀▀▀██████ ▀▀▀\x1b[0m`;
|
|
19
|
+
export const LOGIN_SUCCESS_BANNER = `登录成功
|
|
20
|
+
${TY_LOGO}
|
|
21
|
+
公平看清世界`;
|
|
22
|
+
export function printLoginSuccessBanner() {
|
|
23
|
+
console.log(LOGIN_SUCCESS_BANNER);
|
|
24
|
+
}
|