@sprid/cli 0.1.6 → 0.1.7

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/src/tools.mjs ADDED
@@ -0,0 +1,85 @@
1
+ // The server's tools, called the way an MCP client calls them. Every command
2
+ // built on this runs through the same handler, scopes and tenant checks as an
3
+ // agent's call, so the CLI cannot drift from what MCP does: REST = MCP = CLI.
4
+ //
5
+ // /api/mcp is stateless JSON-RPC, so a single POST per call is enough; no
6
+ // initialize handshake is needed.
7
+
8
+ import { readFile } from "node:fs/promises";
9
+ import { resolve } from "node:path";
10
+ import { UsageError } from "./args.mjs";
11
+ import { ApiError } from "./http.mjs";
12
+
13
+ const MCP_PATH = "/api/mcp?surface=all";
14
+
15
+ async function rpc(ctx, method, params) {
16
+ const res = await ctx.api().post(MCP_PATH, { jsonrpc: "2.0", id: 1, method, params });
17
+ if (res?.error) throw new ApiError(0, res.error.message ?? `The server refused ${method}.`);
18
+ return res?.result;
19
+ }
20
+
21
+ /** One tool's result, exactly as its handler returned it. */
22
+ export async function callTool(ctx, name, args = {}) {
23
+ const result = await rpc(ctx, "tools/call", { name, arguments: args });
24
+ const text = result?.content?.find((c) => c.type === "text")?.text;
25
+ if (text === undefined) return result?.structuredContent ?? null;
26
+ try {
27
+ return JSON.parse(text);
28
+ } catch {
29
+ return text;
30
+ }
31
+ }
32
+
33
+ /** Every tool this token's scopes allow, with its input schema. */
34
+ export async function listTools(ctx) {
35
+ return (await rpc(ctx, "tools/list", {}))?.tools ?? [];
36
+ }
37
+
38
+ /** `--file args.json`, `--args '{"a":1}'`, or both (the flag wins per key). */
39
+ export async function toolArgs(ctx) {
40
+ let args = {};
41
+ if (typeof ctx.flags.file === "string") args = await readJson(ctx, ctx.flags.file);
42
+ if (typeof ctx.flags.args === "string") {
43
+ try {
44
+ args = { ...args, ...JSON.parse(ctx.flags.args) };
45
+ } catch {
46
+ throw new UsageError("--args must be a JSON object, such as --args '{\"account\":\"myapp\"}'.");
47
+ }
48
+ }
49
+ return args;
50
+ }
51
+
52
+ export async function readJson(ctx, path) {
53
+ const full = resolve(ctx.cwd, path);
54
+ let text;
55
+ try {
56
+ text = await readFile(full, "utf8");
57
+ } catch {
58
+ throw new UsageError(`Cannot read ${path}.`);
59
+ }
60
+ try {
61
+ return JSON.parse(text);
62
+ } catch {
63
+ throw new UsageError(`${path} is not valid JSON.`);
64
+ }
65
+ }
66
+
67
+ /** A comma list flag as an array; undefined when the flag is absent. */
68
+ export function listFlag(value) {
69
+ if (typeof value !== "string") return undefined;
70
+ const items = value.split(",").map((v) => v.trim()).filter(Boolean);
71
+ return items.length ? items : undefined;
72
+ }
73
+
74
+ /** A positive integer positional, or a usage error naming the command. */
75
+ export function needInt(arg, usage) {
76
+ const n = Number(arg);
77
+ if (!Number.isInteger(n) || n <= 0) throw new UsageError(usage);
78
+ return n;
79
+ }
80
+
81
+ /** Human output for results with no bespoke renderer. */
82
+ export function show(ctx, result) {
83
+ if (ctx.json) ctx.out(result);
84
+ else ctx.print(JSON.stringify(result, null, 2));
85
+ }