Skip to content

Commit 73d9d7e

Browse files
committed
feat(cli): add bl mcp command group (list/tools/call)
- `bl mcp list` — list MCP servers enabled under the current Bailian account via console gateway PageList (always activated=1). - `bl mcp tools <server-code>` — list tools exposed by a server. - `bl mcp call <server-code>.<tool>` — invoke a tool. Accepts `--json`, repeatable `--arg k=v` (JSON-parsed when possible) and `--query` sugar; `--url` overrides the endpoint for non-Bailian MCPs. - core: export `bailianMcpUrl(baseUrl, code)` building `/api/v1/mcps/<code>/mcp`; `McpClient` now takes a full URL. - `bl search web` switches to `mcpWebSearchEndpoint` directly. - e2e: `mcp.e2e.test.ts` covering help, dry-run, arg-merge semantics, invalid-input paths, and one live `tools/list` against WebSearch.
1 parent 03d1c48 commit 73d9d7e

8 files changed

Lines changed: 586 additions & 19 deletions

File tree

packages/cli/src/commands/catalog.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ import memoryDelete from "./memory/delete.ts";
2727
import memoryProfileCreate from "./memory/profile-create.ts";
2828
import memoryProfileGet from "./memory/profile-get.ts";
2929
import knowledgeRetrieve from "./knowledge/retrieve.ts";
30+
import mcpCall from "./mcp/call.ts";
31+
import mcpList from "./mcp/list.ts";
32+
import mcpTools from "./mcp/tools.ts";
3033
import searchWeb from "./search/web.ts";
3134
import speechSynthesize from "./speech/synthesize.ts";
3235
import speechRecognize from "./speech/recognize.ts";
@@ -61,6 +64,9 @@ export const commands: Record<string, Command> = {
6164
"memory profile create": memoryProfileCreate,
6265
"memory profile get": memoryProfileGet,
6366
"knowledge retrieve": knowledgeRetrieve,
67+
"mcp list": mcpList,
68+
"mcp tools": mcpTools,
69+
"mcp call": mcpCall,
6470
"search web": searchWeb,
6571
"speech synthesize": speechSynthesize,
6672
"speech recognize": speechRecognize,
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import {
2+
defineCommand,
3+
McpClient,
4+
bailianMcpUrl,
5+
detectOutputFormat,
6+
type Config,
7+
type GlobalFlags,
8+
} from "bailian-cli-core";
9+
import { failIfMissing } from "../../output/prompt.ts";
10+
import { emitResult } from "../../output/output.ts";
11+
12+
function parseArgFlags(raw: string[]): Record<string, unknown> {
13+
const out: Record<string, unknown> = {};
14+
for (const item of raw) {
15+
const idx = item.indexOf("=");
16+
if (idx <= 0) {
17+
process.stderr.write(`Error: --arg must be in K=V form, got: ${item}\n`);
18+
process.exit(1);
19+
}
20+
const key = item.slice(0, idx).trim();
21+
const rawVal = item.slice(idx + 1);
22+
try {
23+
out[key] = JSON.parse(rawVal);
24+
} catch {
25+
out[key] = rawVal;
26+
}
27+
}
28+
return out;
29+
}
30+
31+
export default defineCommand({
32+
name: "mcp call",
33+
description: "Call a tool on an MCP server (tools/call)",
34+
usage: "bl mcp call <server-code>.<tool> [--arg k=v ...] [--json '{...}'] [--url <url>]",
35+
options: [
36+
{
37+
flag: "<server-code>.<tool>",
38+
description:
39+
"Server code and tool name joined by a dot, e.g. market-cmapi00073529.SmartStockSelection",
40+
required: true,
41+
},
42+
{
43+
flag: "--arg <kv>",
44+
description: "Tool argument (repeatable). Values parsed as JSON if possible, else string.",
45+
type: "array",
46+
},
47+
{
48+
flag: "--json <obj>",
49+
description: "Full arguments object as JSON; merged with --arg (arg wins).",
50+
},
51+
{
52+
flag: "--query <text>",
53+
description: "Shortcut for --arg query=<text> (mirrors many DashScope MCP tools).",
54+
},
55+
{ flag: "--url <url>", description: "Override the MCP endpoint URL (for non-Bailian servers)" },
56+
],
57+
examples: [
58+
'bl mcp call market-cmapi00073529.SmartStockSelection --query "筛选ROE>15%的消费股"',
59+
'bl mcp call market-cmapi00073529.FinQuery --json \'{"q":"贵州茅台","limit":5}\'',
60+
"bl mcp call market-cmapi00073529.SmartFundSelection --arg riskLevel=R3 --arg minScale=10",
61+
],
62+
async run(config: Config, flags: GlobalFlags) {
63+
const positional =
64+
((flags as Record<string, unknown>)._positional as string[] | undefined) ?? [];
65+
const target = positional[0];
66+
if (!target) failIfMissing("<server-code>.<tool>", "bl mcp call <server-code>.<tool>");
67+
68+
const dot = target!.indexOf(".");
69+
if (dot <= 0 || dot === target!.length - 1) {
70+
process.stderr.write(`Error: target must be <server-code>.<tool>, got "${target}".\n`);
71+
process.exit(1);
72+
}
73+
const serverCode = target!.slice(0, dot);
74+
const toolName = target!.slice(dot + 1);
75+
76+
let toolArgs: Record<string, unknown> = {};
77+
if (flags.json) {
78+
try {
79+
const parsed = JSON.parse(flags.json as string);
80+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
81+
process.stderr.write("Error: --json must decode to an object.\n");
82+
process.exit(1);
83+
}
84+
toolArgs = parsed as Record<string, unknown>;
85+
} catch (err) {
86+
process.stderr.write(`Error: --json is not valid JSON — ${(err as Error).message}\n`);
87+
process.exit(1);
88+
}
89+
}
90+
Object.assign(toolArgs, parseArgFlags((flags.arg as string[] | undefined) ?? []));
91+
if (flags.query !== undefined) toolArgs.query = flags.query;
92+
93+
const url = (flags.url as string) || bailianMcpUrl(config.baseUrl, serverCode);
94+
const format = detectOutputFormat(config.output);
95+
96+
if (config.dryRun) {
97+
emitResult(
98+
{
99+
server: serverCode,
100+
url,
101+
tool: toolName,
102+
arguments: toolArgs,
103+
},
104+
format,
105+
);
106+
return;
107+
}
108+
109+
const client = new McpClient(config, url);
110+
await client.initialize();
111+
const result = await client.callTool(toolName, toolArgs);
112+
113+
if (result.isError) {
114+
const errText = result.content.map((c) => c.text || "").join("\n");
115+
process.stderr.write(`Tool error: ${errText}\n`);
116+
process.exit(1);
117+
}
118+
119+
emitResult(result, format);
120+
},
121+
});
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import {
2+
defineCommand,
3+
callConsoleGateway,
4+
resolveConsoleGatewayCredential,
5+
detectOutputFormat,
6+
BailianError,
7+
ExitCode,
8+
type Config,
9+
type GlobalFlags,
10+
} from "bailian-cli-core";
11+
import { emitResult } from "../../output/output.ts";
12+
13+
const MCP_LIST_API = "zeldaEasy.broadscope-bailian.mcp-server.PageList";
14+
15+
interface ServerSummary {
16+
code: string;
17+
name: string;
18+
description?: string;
19+
type: string;
20+
source?: string;
21+
bizType?: string;
22+
installType?: string;
23+
streamable: boolean;
24+
}
25+
26+
export default defineCommand({
27+
name: "mcp list",
28+
description: "List MCP servers activated under your Bailian account",
29+
usage: "bl mcp list [flags]",
30+
options: [
31+
{ flag: "--name <text>", description: "Filter by server name (substring match)" },
32+
{
33+
flag: "--type <type>",
34+
description: "Server type: OFFICIAL | PRIVATE (default: OFFICIAL)",
35+
},
36+
{ flag: "--page <n>", description: "Page number (default: 1)", type: "number" },
37+
{ flag: "--page-size <n>", description: "Results per page (default: 30)", type: "number" },
38+
{ flag: "--region <region>", description: "API region (default: cn-beijing)" },
39+
],
40+
examples: ["bl mcp list", "bl mcp list --name 金融", "bl mcp list --output json"],
41+
async run(config: Config, flags: GlobalFlags) {
42+
const serverName = (flags.name as string) || "";
43+
const type = (flags.type as string) || "OFFICIAL";
44+
const pageNo = (flags.page as number) || 1;
45+
const pageSize = (flags.pageSize as number) || 30;
46+
const region = (flags.region as string) || "cn-beijing";
47+
const format = detectOutputFormat(config.output);
48+
49+
const data = {
50+
reqDTO: {
51+
type,
52+
displayTools: false,
53+
activated: 1,
54+
pageNo,
55+
pageSize,
56+
serverName,
57+
},
58+
};
59+
60+
if (config.dryRun) {
61+
emitResult({ api: MCP_LIST_API, data, region }, format);
62+
return;
63+
}
64+
65+
const credential = await resolveConsoleGatewayCredential(config);
66+
67+
const result = (await callConsoleGateway(config, credential.token, {
68+
api: MCP_LIST_API,
69+
data,
70+
region,
71+
})) as Record<string, unknown>;
72+
73+
const dataField = (result?.data as Record<string, unknown> | undefined) ?? {};
74+
if (dataField.success === false) {
75+
const code = (dataField.errorCode as string | undefined) ?? "UnknownError";
76+
const msg = (dataField.errorMsg as string | undefined) ?? code;
77+
const hint =
78+
code === "BailianGateway.Login.NotLogined"
79+
? "Run `bl auth login --console` to refresh your console session."
80+
: undefined;
81+
throw new BailianError(`Console gateway: ${msg}`, ExitCode.AUTH, hint);
82+
}
83+
const dataV2 = (dataField.DataV2 as Record<string, unknown> | undefined) ?? {};
84+
const inner =
85+
(dataV2.data as { data?: { mcpServerDetailList?: unknown[]; total?: number } } | undefined)
86+
?.data ?? {};
87+
const list = (inner.mcpServerDetailList ?? []) as Array<Record<string, unknown>>;
88+
const total = (inner.total as number) ?? 0;
89+
90+
const servers: ServerSummary[] = list.map((item) => ({
91+
code: (item.serverCode as string | undefined) ?? "",
92+
name: (item.serverName as string | undefined) ?? "",
93+
description: item.description as string | undefined,
94+
type: (item.type as string | undefined) ?? "",
95+
source: item.source as string | undefined,
96+
bizType: item.bizType as string | undefined,
97+
installType: item.installType as string | undefined,
98+
streamable: item.streamable === true,
99+
}));
100+
101+
emitResult({ total, servers }, format);
102+
},
103+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {
2+
defineCommand,
3+
McpClient,
4+
bailianMcpUrl,
5+
detectOutputFormat,
6+
type Config,
7+
type GlobalFlags,
8+
} from "bailian-cli-core";
9+
import { failIfMissing } from "../../output/prompt.ts";
10+
import { emitResult } from "../../output/output.ts";
11+
12+
export default defineCommand({
13+
name: "mcp tools",
14+
description: "List tools exposed by an MCP server (tools/list)",
15+
usage: "bl mcp tools <server-code> [--url <url>]",
16+
options: [
17+
{
18+
flag: "<server-code>",
19+
description: "Server code from `bl mcp list` (e.g. market-cmapi00073529)",
20+
required: true,
21+
},
22+
{ flag: "--url <url>", description: "Override the MCP endpoint URL (for non-Bailian servers)" },
23+
],
24+
examples: [
25+
"bl mcp tools market-cmapi00073529",
26+
"bl mcp tools market-cmapi00073529 --output json",
27+
"bl mcp tools my-server --url https://example.com/mcp",
28+
],
29+
async run(config: Config, flags: GlobalFlags) {
30+
const positional =
31+
((flags as Record<string, unknown>)._positional as string[] | undefined) ?? [];
32+
const code = positional[0];
33+
if (!code) failIfMissing("server-code", "bl mcp tools <server-code>");
34+
35+
const url = (flags.url as string) || bailianMcpUrl(config.baseUrl, code!);
36+
const format = detectOutputFormat(config.output);
37+
38+
if (config.dryRun) {
39+
emitResult({ server: code, url, action: "tools/list" }, format);
40+
return;
41+
}
42+
43+
const client = new McpClient(config, url);
44+
await client.initialize();
45+
const tools = await client.listTools();
46+
emitResult({ server: code, url, tools }, format);
47+
},
48+
});

packages/cli/src/commands/search/web.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
defineCommand,
3-
mcpWebSearchEndpoint,
43
detectOutputFormat,
4+
mcpWebSearchEndpoint,
55
type Config,
66
type GlobalFlags,
77
isInteractive,

0 commit comments

Comments
 (0)