Skip to content

Commit c188441

Browse files
committed
fix: preserve offline dry-run validation before auth
1 parent 4dfe3ac commit c188441

2 files changed

Lines changed: 82 additions & 57 deletions

File tree

packages/commands/src/commands/mcp/call.ts

Lines changed: 76 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,50 @@ import {
44
BailianError,
55
bailianMcpPath,
66
detectOutputFormat,
7+
type FlagsDef,
8+
type ParsedFlags,
79
} from "bailian-cli-core";
810
import { emitResult } from "bailian-cli-runtime";
911

12+
const CALL_FLAGS = {
13+
target: {
14+
type: "string",
15+
valueHint: "<server.tool>",
16+
description:
17+
"Server code and tool name joined by a dot, e.g. market-cmapi00073529.SmartStockSelection",
18+
required: true,
19+
},
20+
arg: {
21+
type: "array",
22+
valueHint: "<kv>",
23+
description: "Tool argument (repeatable). Values parsed as JSON if possible, else string.",
24+
},
25+
json: {
26+
type: "string",
27+
valueHint: "<obj>",
28+
description: "Full arguments object as JSON; merged with --arg (arg wins).",
29+
},
30+
query: {
31+
type: "string",
32+
valueHint: "<text>",
33+
description: "Shortcut for --arg query=<text> (mirrors many DashScope MCP tools).",
34+
},
35+
url: {
36+
type: "string",
37+
valueHint: "<url>",
38+
description: "Override the MCP endpoint URL (for non-Bailian servers)",
39+
},
40+
} satisfies FlagsDef;
41+
type CallFlags = ParsedFlags<typeof CALL_FLAGS>;
42+
43+
function parseTarget(target: string): { serverCode: string; toolName: string } {
44+
const dot = target.indexOf(".");
45+
if (dot <= 0 || dot === target.length - 1) {
46+
throw new UsageError(`target must be <server-code>.<tool>, got "${target}".`);
47+
}
48+
return { serverCode: target.slice(0, dot), toolName: target.slice(dot + 1) };
49+
}
50+
1051
function parseArgFlags(raw: string[]): Record<string, unknown> {
1152
const out: Record<string, unknown> = {};
1253
for (const item of raw) {
@@ -25,70 +66,52 @@ function parseArgFlags(raw: string[]): Record<string, unknown> {
2566
return out;
2667
}
2768

69+
function parseJsonArg(raw: string): Record<string, unknown> {
70+
let parsed: unknown;
71+
try {
72+
parsed = JSON.parse(raw);
73+
} catch (err) {
74+
throw new UsageError(`--json is not valid JSON — ${(err as Error).message}`);
75+
}
76+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
77+
throw new UsageError("--json must decode to an object.");
78+
}
79+
return parsed as Record<string, unknown>;
80+
}
81+
82+
function buildToolArgs(flags: CallFlags): Record<string, unknown> {
83+
const toolArgs = flags.json ? parseJsonArg(flags.json) : {};
84+
Object.assign(toolArgs, parseArgFlags(flags.arg ?? []));
85+
if (flags.query !== undefined) toolArgs.query = flags.query;
86+
return toolArgs;
87+
}
88+
89+
function validateCallFlags(flags: CallFlags): string | undefined {
90+
try {
91+
parseTarget(flags.target);
92+
buildToolArgs(flags);
93+
} catch (err) {
94+
if (err instanceof UsageError) return err.message;
95+
throw err;
96+
}
97+
return undefined;
98+
}
99+
28100
export default defineCommand({
29101
description: "Call a tool on an MCP server (tools/call)",
30102
auth: "apiKey",
31103
usageArgs: "--target <server.tool> [--arg k=v ...] [--json '{...}'] [--url <url>]",
32-
flags: {
33-
target: {
34-
type: "string",
35-
valueHint: "<server.tool>",
36-
description:
37-
"Server code and tool name joined by a dot, e.g. market-cmapi00073529.SmartStockSelection",
38-
required: true,
39-
},
40-
arg: {
41-
type: "array",
42-
valueHint: "<kv>",
43-
description: "Tool argument (repeatable). Values parsed as JSON if possible, else string.",
44-
},
45-
json: {
46-
type: "string",
47-
valueHint: "<obj>",
48-
description: "Full arguments object as JSON; merged with --arg (arg wins).",
49-
},
50-
query: {
51-
type: "string",
52-
valueHint: "<text>",
53-
description: "Shortcut for --arg query=<text> (mirrors many DashScope MCP tools).",
54-
},
55-
url: {
56-
type: "string",
57-
valueHint: "<url>",
58-
description: "Override the MCP endpoint URL (for non-Bailian servers)",
59-
},
60-
},
104+
flags: CALL_FLAGS,
61105
exampleArgs: [
62106
'--target market-cmapi00073529.SmartStockSelection --query "Screen consumer stocks with ROE > 15%"',
63107
'--target market-cmapi00073529.FinQuery --json \'{"q":"Guizhou Maotai","limit":5}\'',
64108
"--target market-cmapi00073529.SmartFundSelection --arg riskLevel=R3 --arg minScale=10",
65109
],
110+
validate: validateCallFlags,
66111
async run(ctx) {
67112
const { settings, flags } = ctx;
68-
const target = flags.target;
69-
70-
const dot = target.indexOf(".");
71-
if (dot <= 0 || dot === target.length - 1) {
72-
throw new UsageError(`target must be <server-code>.<tool>, got "${target}".`);
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-
let parsed: unknown;
80-
try {
81-
parsed = JSON.parse(flags.json);
82-
} catch (err) {
83-
throw new UsageError(`--json is not valid JSON — ${(err as Error).message}`);
84-
}
85-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
86-
throw new UsageError("--json must decode to an object.");
87-
}
88-
toolArgs = parsed as Record<string, unknown>;
89-
}
90-
Object.assign(toolArgs, parseArgFlags(flags.arg ?? []));
91-
if (flags.query !== undefined) toolArgs.query = flags.query;
113+
const { serverCode, toolName } = parseTarget(flags.target);
114+
const toolArgs = buildToolArgs(flags);
92115

93116
const url = flags.url || ctx.client.url(bailianMcpPath(serverCode));
94117
const format = detectOutputFormat(settings.output);

packages/core/src/client/client.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { ApiKeyCredential, ConsoleCredential } from "../auth/types.ts";
33
import { BailianError } from "../errors/base.ts";
44
import { ExitCode } from "../errors/codes.ts";
55
import { request, requestJson, type HttpDeps, type RequestOpts } from "./http.ts";
6-
import { resolveFileUrl } from "../files/upload.ts";
6+
import { isLocalFile, resolveFileUrl } from "../files/upload.ts";
77
import { McpClient } from "./mcp.ts";
88
import { callConsoleGateway } from "../console/gateway.ts";
99

@@ -25,9 +25,10 @@ export interface ClientRequestOpts extends Omit<RequestOpts, "url" | "noAuth"> {
2525
/**
2626
* A command's network surface: call its methods to reach the API — the
2727
* credential and base URL are already baked in, so commands never handle tokens
28-
* or baseUrl. Model methods (`request`/`requestJson`/`uploadFile`/`mcp`) need an
29-
* api key; `console` needs a console token; calling one without its credential
30-
* throws.
28+
* or baseUrl. Model methods (`request`/`requestJson`/`mcp`) need an api key;
29+
* `uploadFile` only needs one for local files, and passes URLs through without
30+
* credentials. `console` needs a console token; calling one without its
31+
* credential throws.
3132
*/
3233
export class Client {
3334
constructor(private readonly deps: ClientDeps) {}
@@ -72,6 +73,7 @@ export class Client {
7273

7374
/** Resolve a file arg: upload a local path to OSS (returns oss:// URL), or pass a URL through. */
7475
uploadFile(source: string, model: string, opts: { signal?: AbortSignal } = {}): Promise<string> {
76+
if (!isLocalFile(source)) return Promise.resolve(source);
7577
return resolveFileUrl(source, this.requireApi().token, model, opts);
7678
}
7779

0 commit comments

Comments
 (0)