@hardfin/cli 0.0.2-dev.10 → 0.0.2-dev.11

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.
Files changed (3) hide show
  1. package/README.md +36 -0
  2. package/dist/cli.js +188 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -185,6 +185,42 @@ environment, so `hardfin login` works out of the box.
185
185
  | `clientId`, or `HARDFIN_CLIENT_ID` | the client this CLI names itself as | the seeded Hardfin CLI client |
186
186
  | `issuerUrl`, or `HARDFIN_ISSUER_URL` | the authorization server | the API's host root |
187
187
 
188
+ ## Serving an agent
189
+
190
+ ```sh
191
+ hardfin mcp
192
+ ```
193
+
194
+ It speaks the Model Context Protocol on standard input and output, so Claude, Codex or any
195
+ other client can run Hardfin commands.
196
+
197
+ ```sh
198
+ claude mcp add hardfin -- hardfin mcp
199
+ ```
200
+
201
+ ### One tool, and resources for the rest
202
+
203
+ Every tool a server lists sits in the agent's context for the entire session, so this server
204
+ offers one.
205
+
206
+ | Offered as | Holds | Costs context |
207
+ | --- | --- | --- |
208
+ | The `hardfin` tool | A list of arguments, such as `["asset", "list", "--limit", "5"]` | Always, and it is about 400 bytes |
209
+ | `hardfin://guide` | Every command, its flags, and the exit codes | Only when the agent reads it |
210
+ | `hardfin://commands` | The command tree as JSON | Only when the agent reads it |
211
+
212
+ Fifty tools would describe the same surface and crowd out the work. An agent that needs the
213
+ list reads a resource, or runs `["--help"]`.
214
+
215
+ A tool call runs the CLI itself, so an agent and a person get identical parsing, output and
216
+ exit codes.
217
+
218
+ ### What it signs in as
219
+
220
+ The server uses whatever credential this machine holds, so an agent acts as the person who
221
+ ran `hardfin login`. Give an agent an API key through `HARDFIN_API_KEY` when it should act
222
+ as an integration instead.
223
+
188
224
  ## Diagnosing a problem
189
225
 
190
226
  `hardfin status` prints everything a support request needs, and is the first thing to send.
package/dist/cli.js CHANGED
@@ -5,9 +5,10 @@ import { z } from "zod";
5
5
  import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
6
6
  import { dirname, join, resolve } from "node:path";
7
7
  import { arch, cpus, homedir, release, totalmem, type, version } from "node:os";
8
- import { spawnSync } from "node:child_process";
8
+ import { spawn, spawnSync } from "node:child_process";
9
9
  import { createServer } from "node:http";
10
10
  import { createHash, randomBytes } from "node:crypto";
11
+ import { createInterface } from "node:readline";
11
12
  //#region src/command/registry.ts
12
13
  /** ExitCode is what the process returns, and what an agent branches on. */
13
14
  const ExitCode = {
@@ -1310,6 +1311,190 @@ async function runLogout(input) {
1310
1311
  return ExitCode.OK;
1311
1312
  }
1312
1313
  //#endregion
1314
+ //#region src/mcp/server.ts
1315
+ const PROTOCOL_VERSION = "2025-06-18";
1316
+ const GUIDE_URI = "hardfin://guide";
1317
+ const COMMANDS_URI = "hardfin://commands";
1318
+ /**
1319
+ * One tool, because every tool a server lists sits in the agent's context for the whole
1320
+ * session. The commands themselves are resources, which cost nothing until one is read.
1321
+ */
1322
+ const TOOL = {
1323
+ name: "hardfin",
1324
+ description: "Run a Hardfin CLI command against the Hardfin API. Pass the arguments as a list, such as [\"asset\", \"list\", \"--limit\", \"5\"]. Run [\"--help\"] for the commands, or read the hardfin://guide resource.",
1325
+ inputSchema: {
1326
+ type: "object",
1327
+ properties: { args: {
1328
+ type: "array",
1329
+ items: { type: "string" },
1330
+ description: "The arguments to hardfin, without the program name"
1331
+ } },
1332
+ required: ["args"]
1333
+ }
1334
+ };
1335
+ /** toResponse answers one request, and answers nothing to a notification. */
1336
+ async function toResponse(request, commands, run) {
1337
+ const answer = (result) => ({
1338
+ jsonrpc: "2.0",
1339
+ id: request.id ?? null,
1340
+ result
1341
+ });
1342
+ switch (request.method) {
1343
+ case "initialize": return answer({
1344
+ protocolVersion: toProtocolVersion(request.params),
1345
+ capabilities: {
1346
+ tools: {},
1347
+ resources: {}
1348
+ },
1349
+ serverInfo: {
1350
+ name: "hardfin",
1351
+ version: version$1
1352
+ }
1353
+ });
1354
+ case "tools/list": return answer({ tools: [TOOL] });
1355
+ case "resources/list": return answer({ resources: [{
1356
+ uri: GUIDE_URI,
1357
+ name: "Hardfin CLI guide",
1358
+ description: "Every command, its flags, and the exit codes",
1359
+ mimeType: "text/markdown"
1360
+ }, {
1361
+ uri: COMMANDS_URI,
1362
+ name: "Hardfin CLI commands",
1363
+ description: "The command tree as JSON",
1364
+ mimeType: "application/json"
1365
+ }] });
1366
+ case "resources/read": return answer(toResource(String(request.params?.["uri"] ?? ""), commands));
1367
+ case "tools/call": return answer(await toToolResult(request.params, run));
1368
+ case "ping": return answer({});
1369
+ default:
1370
+ if (request.id === void 0 || request.id === null) return;
1371
+ return {
1372
+ jsonrpc: "2.0",
1373
+ id: request.id,
1374
+ error: {
1375
+ code: -32601,
1376
+ message: `${request.method} is not a method this server offers`
1377
+ }
1378
+ };
1379
+ }
1380
+ }
1381
+ function toProtocolVersion(params) {
1382
+ const asked = params?.["protocolVersion"];
1383
+ return typeof asked === "string" ? asked : PROTOCOL_VERSION;
1384
+ }
1385
+ function toResource(uri, commands) {
1386
+ if (uri === GUIDE_URI) return { contents: [{
1387
+ uri,
1388
+ mimeType: "text/markdown",
1389
+ text: toGuide(commands, version$1)
1390
+ }] };
1391
+ if (uri === COMMANDS_URI) return { contents: [{
1392
+ uri,
1393
+ mimeType: "application/json",
1394
+ text: JSON.stringify(toTree(commands), null, 2)
1395
+ }] };
1396
+ return {
1397
+ contents: [],
1398
+ isError: true
1399
+ };
1400
+ }
1401
+ /** toTree names every command and what it takes, without the schemas a tool list would carry. */
1402
+ function toTree(commands) {
1403
+ return commands.map((command) => ({
1404
+ name: command.name,
1405
+ summary: command.summary,
1406
+ arguments: command.arguments.map((argument) => argument.name),
1407
+ flags: command.flags.map((flag) => flag.valueName ? `--${flag.name} <${flag.valueName}>` : `--${flag.name}`),
1408
+ subcommands: command.subcommands ? toTree(command.subcommands) : void 0
1409
+ }));
1410
+ }
1411
+ async function toToolResult(params, run) {
1412
+ const args = (params?.["arguments"])?.args;
1413
+ if (!Array.isArray(args) || args.some((entry) => typeof entry !== "string")) return {
1414
+ content: [{
1415
+ type: "text",
1416
+ text: "args must be a list of strings, such as [\"asset\", \"list\"]"
1417
+ }],
1418
+ isError: true
1419
+ };
1420
+ const outcome = await run(args);
1421
+ return {
1422
+ content: [{
1423
+ type: "text",
1424
+ text: [outcome.stdout, outcome.stderr].filter((part) => part.trim() !== "").join("\n") || `hardfin exited ${outcome.code}`
1425
+ }],
1426
+ isError: outcome.code !== 0
1427
+ };
1428
+ }
1429
+ /** toCliRunner runs the CLI itself, so a tool call parses exactly as a terminal would. */
1430
+ function toCliRunner() {
1431
+ return (args) => new Promise((resolve) => {
1432
+ const child = spawn(process.execPath, [process.argv[1] ?? "", ...args], { env: {
1433
+ ...process.env,
1434
+ HARDFIN_NO_BROWSER: "1"
1435
+ } });
1436
+ let stdout = "";
1437
+ let stderr = "";
1438
+ child.stdout.on("data", (chunk) => {
1439
+ stdout += chunk.toString();
1440
+ });
1441
+ child.stderr.on("data", (chunk) => {
1442
+ stderr += chunk.toString();
1443
+ });
1444
+ child.on("close", (code) => resolve({
1445
+ stdout,
1446
+ stderr,
1447
+ code: code ?? 1
1448
+ }));
1449
+ });
1450
+ }
1451
+ /** serve answers requests on stdin until the client closes it. */
1452
+ async function serve(commands, run = toCliRunner()) {
1453
+ const lines = createInterface({ input: process.stdin });
1454
+ for await (const line of lines) {
1455
+ if (line.trim() === "") continue;
1456
+ let response;
1457
+ try {
1458
+ response = await toResponse(JSON.parse(line), commands, run);
1459
+ } catch (error) {
1460
+ response = {
1461
+ jsonrpc: "2.0",
1462
+ id: null,
1463
+ error: {
1464
+ code: -32700,
1465
+ message: error instanceof Error ? error.message : String(error)
1466
+ }
1467
+ };
1468
+ }
1469
+ if (response) process.stdout.write(`${JSON.stringify(response)}\n`);
1470
+ }
1471
+ }
1472
+ //#endregion
1473
+ //#region src/command/mcp.ts
1474
+ const mcpCommand = defineCommand({
1475
+ name: "mcp",
1476
+ summary: "Serve this CLI to an agent over the Model Context Protocol",
1477
+ description: "Speaks the Model Context Protocol on standard input and output. It offers one tool, because every tool an agent is told about occupies its context for the whole session, and serves the commands as resources the agent reads only when it needs them.",
1478
+ arguments: [],
1479
+ flags: [{
1480
+ name: "json",
1481
+ description: "Accepted for consistency, and ignored, because the protocol decides the output",
1482
+ schema: z.boolean()
1483
+ }],
1484
+ examples: [{
1485
+ description: "Register with an agent",
1486
+ command: "hardfin mcp"
1487
+ }, {
1488
+ description: "Add it to Claude Code",
1489
+ command: "claude mcp add hardfin -- hardfin mcp"
1490
+ }],
1491
+ run: runMcp
1492
+ });
1493
+ async function runMcp(input) {
1494
+ await serve(input.commands);
1495
+ return ExitCode.OK;
1496
+ }
1497
+ //#endregion
1313
1498
  //#region src/command/operation.ts
1314
1499
  const INPUT_FLAG = {
1315
1500
  name: "input",
@@ -2898,7 +3083,8 @@ const commands = [
2898
3083
  ...surfaceCommands,
2899
3084
  apiCommand,
2900
3085
  configCommand,
2901
- agentGuideCommand
3086
+ agentGuideCommand,
3087
+ mcpCommand
2902
3088
  ];
2903
3089
  //#endregion
2904
3090
  //#region src/command/validate.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardfin/cli",
3
- "version": "0.0.2-dev.10",
3
+ "version": "0.0.2-dev.11",
4
4
  "description": "Command line interface for the Hardfin API",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hardfin, Inc.",