Skip to content

Commit 73acb39

Browse files
Merge pull request #27 from modelstudioai/feat/mcp-command
Feat/mcp command
2 parents 3708ec2 + 4ae68ef commit 73acb39

10 files changed

Lines changed: 594 additions & 22 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: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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+
import { ensureApiKey } from "../../utils/ensure-key.ts";
12+
13+
function parseArgFlags(raw: string[]): Record<string, unknown> {
14+
const out: Record<string, unknown> = {};
15+
for (const item of raw) {
16+
const idx = item.indexOf("=");
17+
if (idx <= 0) {
18+
process.stderr.write(`Error: --arg must be in K=V form, got: ${item}\n`);
19+
process.exit(1);
20+
}
21+
const key = item.slice(0, idx).trim();
22+
const rawVal = item.slice(idx + 1);
23+
try {
24+
out[key] = JSON.parse(rawVal);
25+
} catch {
26+
out[key] = rawVal;
27+
}
28+
}
29+
return out;
30+
}
31+
32+
export default defineCommand({
33+
name: "mcp call",
34+
description: "Call a tool on an MCP server (tools/call)",
35+
usage: "bl mcp call <server-code>.<tool> [--arg k=v ...] [--json '{...}'] [--url <url>]",
36+
options: [
37+
{
38+
flag: "<server-code>.<tool>",
39+
description:
40+
"Server code and tool name joined by a dot, e.g. market-cmapi00073529.SmartStockSelection",
41+
required: true,
42+
},
43+
{
44+
flag: "--arg <kv>",
45+
description: "Tool argument (repeatable). Values parsed as JSON if possible, else string.",
46+
type: "array",
47+
},
48+
{
49+
flag: "--json <obj>",
50+
description: "Full arguments object as JSON; merged with --arg (arg wins).",
51+
},
52+
{
53+
flag: "--query <text>",
54+
description: "Shortcut for --arg query=<text> (mirrors many DashScope MCP tools).",
55+
},
56+
{ flag: "--url <url>", description: "Override the MCP endpoint URL (for non-Bailian servers)" },
57+
],
58+
examples: [
59+
'bl mcp call market-cmapi00073529.SmartStockSelection --query "筛选ROE>15%的消费股"',
60+
'bl mcp call market-cmapi00073529.FinQuery --json \'{"q":"贵州茅台","limit":5}\'',
61+
"bl mcp call market-cmapi00073529.SmartFundSelection --arg riskLevel=R3 --arg minScale=10",
62+
],
63+
async run(config: Config, flags: GlobalFlags) {
64+
const positional =
65+
((flags as Record<string, unknown>)._positional as string[] | undefined) ?? [];
66+
const target = positional[0];
67+
if (!target) failIfMissing("<server-code>.<tool>", "bl mcp call <server-code>.<tool>");
68+
69+
const dot = target!.indexOf(".");
70+
if (dot <= 0 || dot === target!.length - 1) {
71+
process.stderr.write(`Error: target must be <server-code>.<tool>, got "${target}".\n`);
72+
process.exit(1);
73+
}
74+
const serverCode = target!.slice(0, dot);
75+
const toolName = target!.slice(dot + 1);
76+
77+
let toolArgs: Record<string, unknown> = {};
78+
if (flags.json) {
79+
try {
80+
const parsed = JSON.parse(flags.json as string);
81+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
82+
process.stderr.write("Error: --json must decode to an object.\n");
83+
process.exit(1);
84+
}
85+
toolArgs = parsed as Record<string, unknown>;
86+
} catch (err) {
87+
process.stderr.write(`Error: --json is not valid JSON — ${(err as Error).message}\n`);
88+
process.exit(1);
89+
}
90+
}
91+
Object.assign(toolArgs, parseArgFlags((flags.arg as string[] | undefined) ?? []));
92+
if (flags.query !== undefined) toolArgs.query = flags.query;
93+
94+
const url = (flags.url as string) || bailianMcpUrl(config.baseUrl, serverCode);
95+
const format = detectOutputFormat(config.output);
96+
97+
if (config.dryRun) {
98+
emitResult(
99+
{
100+
server: serverCode,
101+
url,
102+
tool: toolName,
103+
arguments: toolArgs,
104+
},
105+
format,
106+
);
107+
return;
108+
}
109+
110+
await ensureApiKey(config);
111+
const client = new McpClient(config, url);
112+
await client.initialize();
113+
const result = await client.callTool(toolName, toolArgs);
114+
115+
if (result.isError) {
116+
const errText = result.content.map((c) => c.text || "").join("\n");
117+
process.stderr.write(`Tool error: ${errText}\n`);
118+
process.exit(1);
119+
}
120+
121+
emitResult(result, format);
122+
},
123+
});
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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
import { ensureApiKey } from "../../utils/ensure-key.ts";
12+
13+
export default defineCommand({
14+
name: "mcp tools",
15+
description: "List tools exposed by an MCP server (tools/list)",
16+
usage: "bl mcp tools <server-code> [--url <url>]",
17+
options: [
18+
{
19+
flag: "<server-code>",
20+
description: "Server code from `bl mcp list` (e.g. market-cmapi00073529)",
21+
required: true,
22+
},
23+
{ flag: "--url <url>", description: "Override the MCP endpoint URL (for non-Bailian servers)" },
24+
],
25+
examples: [
26+
"bl mcp tools market-cmapi00073529",
27+
"bl mcp tools market-cmapi00073529 --output json",
28+
"bl mcp tools my-server --url https://example.com/mcp",
29+
],
30+
async run(config: Config, flags: GlobalFlags) {
31+
const positional =
32+
((flags as Record<string, unknown>)._positional as string[] | undefined) ?? [];
33+
const code = positional[0];
34+
if (!code) failIfMissing("server-code", "bl mcp tools <server-code>");
35+
36+
const url = (flags.url as string) || bailianMcpUrl(config.baseUrl, code!);
37+
const format = detectOutputFormat(config.output);
38+
39+
if (config.dryRun) {
40+
emitResult({ server: code, url, action: "tools/list" }, format);
41+
return;
42+
}
43+
44+
await ensureApiKey(config);
45+
const client = new McpClient(config, url);
46+
await client.initialize();
47+
const tools = await client.listTools();
48+
emitResult({ server: code, url, tools }, format);
49+
},
50+
});

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,

packages/cli/src/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ const NO_AUTH_SETUP = [
6060
["app", "list"],
6161
["console", "call"],
6262
["usage", "free"],
63+
["mcp", "list"],
64+
["mcp", "tools"],
65+
["mcp", "call"],
6366
];
6467

6568
async function main() {

0 commit comments

Comments
 (0)