Skip to content

Commit 6bf89c8

Browse files
dwivnaclaude
andcommitted
3.0.4: built-in MCP server (mfn mcp) — stdio, zero new dependencies
Serves the whole toolkit over the Model Context Protocol for agent clients without shell access. Hand-rolled NDJSON JSON-RPC 2.0 on node built-ins; three tools (mfn_capabilities, mfn_run, mfn_help). Every call re-enters the CLI via execFile (no shell), so guardrails and the single-JSON-object contract apply unchanged. `update` deny-listed (no npm-install capability over MCP); `mfn mcp --json` self-describes without serving, keeping the universal --json contract and the black-box sweep intact. 11 conformance tests (143 total). npm keywords mcp/mcp-server/model-context-protocol; README + llms.txt wiring docs. Co-Authored-By: Claude Fable 5 <[email protected]>
1 parent 02de1c7 commit 6bf89c8

11 files changed

Lines changed: 525 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,34 @@ All notable changes to `@master4n/master-cli` (`mfn`) are documented here. The
44
format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
55
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [3.0.4] — 2026-06-11
8+
9+
### Added
10+
11+
- **`mfn mcp` — built-in Model Context Protocol server** (stdio transport,
12+
hand-rolled JSON-RPC on node built-ins, zero new dependencies). Exposes three
13+
tools to MCP-only clients: `mfn_capabilities` (the manifest), `mfn_run`
14+
(`{command, args[]}` — validated against the catalogue, spawned without a
15+
shell, `--json` enforced, results include `structuredContent`), and
16+
`mfn_help`. `update` is deny-listed so an MCP client can never gain
17+
npm-install capability; `mcp` itself is deny-listed (no recursion).
18+
`mfn mcp --json` emits the standard single-object envelope describing the
19+
server (transport, tools, client wiring) instead of starting it, so the
20+
universal CLI contract still holds. 11 new conformance tests.
21+
- npm keywords: `mcp`, `mcp-server`, `model-context-protocol`.
22+
23+
### Security
24+
25+
- **`hash -f` now refuses sensitive paths** (`SensitivePath`, exit 2), including
26+
via symlink. A digest is derived data, but a digest of a low-entropy secrets
27+
file (`.env`, `~/.aws/credentials`, …) can be brute-forced offline. Text and
28+
stdin hashing are unchanged.
29+
30+
### Fixed
31+
32+
- README said "43 headless, JSON-first commands"; the manifest ships 50 (now 51
33+
with `mcp`).
34+
735
## [3.0.3] — 2026-06-11
836

937
The agent release: 37 new commands (13 → 50), ~6× faster startup, real quality

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
![License](https://img.shields.io/npm/l/%40master4n%2Fmaster-cli)
1010
![Owner](https://img.shields.io/badge/Owner-Master4Novice-orange?style=flat)
1111

12-
**Master CLI for developers and AI agents.** 50 headless, JSON-first commands in
12+
**Master CLI for developers and AI agents.** 51 headless, JSON-first commands in
1313
three families: **token savers** (extract exactly what you need — one JSON field,
1414
a line range, a file outline — instead of dumping whole files into context),
1515
**exact computation** (BigInt math, semver, cron, regex, timezones — verified
@@ -54,6 +54,27 @@ npx -y @master4n/master-cli epoch 1622547800 --json
5454
- **Self-describing**`mfn capabilities --json` lists every command, and
5555
[`llms.txt`](./llms.txt) documents the full agent contract.
5656

57+
## MCP server (built in)
58+
59+
For agent clients that can't run shell commands, `mfn mcp` serves the whole
60+
toolkit over the [Model Context Protocol](https://modelcontextprotocol.io)
61+
(stdio transport, zero extra dependencies):
62+
63+
```jsonc
64+
// e.g. .mcp.json / claude_desktop_config.json / any MCP client
65+
{
66+
"mcpServers": {
67+
"mfn": { "command": "npx", "args": ["-y", "@master4n/master-cli", "mcp"] }
68+
}
69+
}
70+
```
71+
72+
Three tools: `mfn_capabilities` (the manifest), `mfn_run` (`{command, args[]}`
73+
runs any catalogued command and returns its single JSON object, guardrails
74+
included), and `mfn_help` (per-command flags). `update` is deny-listed so an
75+
MCP-only client can never install packages. `mfn mcp --json` describes the
76+
server without starting it.
77+
5778
## Quick start
5879

5980
```sh
@@ -63,7 +84,7 @@ mfn -v # version
6384
mfn capabilities --json # machine-readable manifest of all commands
6485
```
6586

66-
## Commands (50)
87+
## Commands (51)
6788

6889
Run `mfn capabilities` for the grouped list, `mfn <command> --help` for flags.
6990

@@ -125,6 +146,7 @@ Run `mfn capabilities` for the grouped list, `mfn <command> --help` for flags.
125146
| `id` / `hash` / `encode` / `random` | UUID v4/v7/nano · digests · codecs · CSPRNG | `mfn id -t uuid7 -n 3 --json` |
126147
| `sc` / `cts` | Fuzzy file find · directory tree | `mfn sc service --json` |
127148
| `capabilities` / `update` | Machine-readable manifest · self-update | `mfn capabilities --json` |
149+
| `mcp` | Serve every command over the Model Context Protocol (stdio) | `mfn mcp` |
128150

129151
### Examples
130152

llms.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# @master4n/master-cli (`mfn`)
22

3-
> 50 headless, JSON-first commands for AI agents and developers: extract instead
3+
> 51 headless, JSON-first commands for AI agents and developers: extract instead
44
> of dump (token savers), compute instead of guess (hallucination killers),
55
> one-call system/code/network facts, and cross-platform OS actions (clipboard,
66
> notifications, trash, processes). Every command behaves identically for a
@@ -12,6 +12,11 @@
1212
global install; with it installed, the binary is `mfn`.
1313
- **Discover commands:** `mfn capabilities --json` returns the full manifest
1414
(`{ name, version, bin, conventions, docs, categories, commands:[{name,category,summary,examples}] }`).
15+
- **MCP server built in:** `mfn mcp` serves every command over the Model Context
16+
Protocol (stdio) for MCP-only clients — tools `mfn_capabilities`, `mfn_run`
17+
`{command, args[]}`, `mfn_help`; `update` is deny-listed. Client config:
18+
`{ "command": "npx", "args": ["-y", "@master4n/master-cli", "mcp"] }`.
19+
`mfn mcp --json` describes the server without starting it.
1520
- **Machine output:** pass `--json`, OR just pipe the command (when stdout is not
1621
a TTY the CLI auto-emits JSON). Output is exactly one object on stdout:
1722
success → `{ "ok": true, ... }`, failure → `{ "ok": false, "error", "message" }`.

package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@master4n/master-cli",
3-
"version": "3.0.3",
4-
"description": "AI-agent-friendly command-line toolkit: timestamp/date conversion, JWT decoding, port killing, file finding, and directory trees — headless, --json, with a self-describing manifest.",
3+
"version": "3.0.4",
4+
"description": "AI-agent-friendly command-line toolkit: timestamp/date conversion, JWT decoding, port killing, file finding, and directory trees — headless, --json, with a self-describing manifest and a built-in MCP server (mfn mcp).",
55
"type": "module",
66
"scripts": {
77
"clean": "rimraf dist",
@@ -60,7 +60,10 @@
6060
"developer-cli",
6161
"agent-cli",
6262
"mfn",
63-
"master-cli"
63+
"master-cli",
64+
"mcp",
65+
"mcp-server",
66+
"model-context-protocol"
6467
],
6568
"author": "dwivna",
6669
"license": "MIT",

src/catalog.classic.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ export const CLASSIC_COMMANDS: readonly CommandInfo[] = [
88
summary: 'Self-describing manifest of every command an agent can call',
99
examples: ['mfn capabilities --json'],
1010
},
11+
{
12+
name: 'mcp',
13+
category: 'discovery',
14+
summary: 'Serve every command over the Model Context Protocol (stdio) for MCP clients',
15+
examples: ['mfn mcp', 'mfn mcp --json'],
16+
},
1117
{
1218
name: 'epoch',
1319
category: 'time',

src/commands/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import procs from './procs';
4848
import disk from './disk';
4949
import trash from './trash';
5050
import dns from './dns';
51+
import mcp from './mcp';
5152

5253
export {
5354
cts,
@@ -58,6 +59,7 @@ export {
5859
killProcess,
5960
update,
6061
capabilities,
62+
mcp,
6163
id,
6264
hash,
6365
encode,

src/commands/mcp.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* `mfn mcp` — serve every mfn command over the Model Context Protocol (stdio).
3+
*
4+
* For agent clients that cannot run shell commands (MCP-only hosts). Protocol
5+
* loop lives in utility/mcp-server.ts; tool definitions and execution in
6+
* utility/mcp-tools.ts.
7+
*
8+
* Contract note: `mfn mcp --json` does NOT start the server — it emits the
9+
* usual single {ok,...} object describing the server and how to wire it. The
10+
* non-TTY auto-JSON rule is deliberately ignored here: an MCP client talks to
11+
* the server over pipes, which is exactly the non-TTY case.
12+
*/
13+
import { withJsonFlag, emit } from '../utility/io';
14+
import { PROTOCOL_VERSION, serveMcp } from '../utility/mcp-server';
15+
import { TOOLS, RUNNABLE, DENYLIST } from '../utility/mcp-tools';
16+
17+
const command = 'mcp';
18+
const describe =
19+
'Serve every mfn command over the Model Context Protocol (stdio) — for MCP clients without shell access';
20+
21+
const builder = (yargs: any) =>
22+
withJsonFlag(yargs)
23+
.example('mfn mcp', 'start the MCP server (stdio, JSON-RPC per line)')
24+
.example('mfn mcp --json', 'describe the server + client wiring (does not start it)');
25+
26+
const handler = async (argv: any) => {
27+
if (argv.json) {
28+
emit(
29+
argv,
30+
{
31+
transport: 'stdio',
32+
protocolVersion: PROTOCOL_VERSION,
33+
tools: TOOLS.map((t) => t.name),
34+
commands: RUNNABLE.length,
35+
denied: Object.keys(DENYLIST),
36+
clientConfig: {
37+
command: 'npx',
38+
args: ['-y', '@master4n/master-cli', 'mcp'],
39+
},
40+
note: 'Run `mfn mcp` (no flags) to start the server.',
41+
},
42+
() => undefined,
43+
);
44+
return;
45+
}
46+
await serveMcp();
47+
};
48+
49+
const mcp = { command, describe, builder, handler };
50+
51+
export default mcp;

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
killProcess,
99
update,
1010
capabilities,
11+
mcp,
1112
id,
1213
hash,
1314
encode,
@@ -62,6 +63,7 @@ const add = (c: CliCommand) =>
6263

6364
/**Discovery */
6465
add(capabilities);
66+
add(mcp);
6567
/**Generators & codecs (zero-dependency primitives) */
6668
add(id);
6769
add(hash);

src/utility/mcp-server.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* The Model Context Protocol stdio server loop behind `mfn mcp`: one JSON-RPC
3+
* 2.0 message per line on stdin/stdout. Hand-rolled on node built-ins — no SDK
4+
* dependency. Tool definitions and execution live in mcp-tools.ts.
5+
*/
6+
import { createInterface } from 'node:readline';
7+
import { COMMANDS } from '../catalog';
8+
import pkg from '../../package.json';
9+
import { TOOLS, callTool } from './mcp-tools';
10+
11+
export const PROTOCOL_VERSION = '2025-06-18';
12+
13+
/** One line of JSON-RPC on stdout — the MCP stdio framing. */
14+
const send = (msg: Record<string, unknown>): void => {
15+
process.stdout.write(JSON.stringify(msg) + '\n');
16+
};
17+
18+
const reply = (id: unknown, result: Record<string, unknown>): void =>
19+
send({ jsonrpc: '2.0', id, result });
20+
21+
const replyError = (id: unknown, code: number, message: string): void =>
22+
send({ jsonrpc: '2.0', id, error: { code, message } });
23+
24+
async function handleMessage(msg: any): Promise<void> {
25+
const { id, method, params } = msg ?? {};
26+
const isRequest = id !== undefined && id !== null;
27+
28+
if (typeof method !== 'string') {
29+
if (isRequest) replyError(id, -32600, 'Invalid request: no method');
30+
return;
31+
}
32+
if (method.startsWith('notifications/')) return; // initialized, cancelled, …
33+
34+
switch (method) {
35+
case 'initialize': {
36+
const requested = params?.protocolVersion;
37+
reply(id, {
38+
protocolVersion: typeof requested === 'string' ? requested : PROTOCOL_VERSION,
39+
capabilities: { tools: {} },
40+
serverInfo: { name: pkg.name, version: pkg.version },
41+
instructions:
42+
`Wraps the mfn CLI (${COMMANDS.length} headless commands). Call mfn_capabilities ` +
43+
'first to discover commands, then mfn_run {command, args} to execute one — every ' +
44+
'result is a single JSON object ({ok:true,...} or {ok:false,error,message}). ' +
45+
'mfn_help returns per-command flags. Security guardrails (sensitive-path refusal, ' +
46+
'secret redaction, reversible deletes) are always on; `update` is not exposed.',
47+
});
48+
return;
49+
}
50+
case 'ping':
51+
reply(id, {});
52+
return;
53+
case 'tools/list':
54+
reply(id, { tools: TOOLS });
55+
return;
56+
case 'tools/call': {
57+
const name = String(params?.name ?? '');
58+
if (!TOOLS.some((t) => t.name === name)) {
59+
replyError(id, -32602, `Unknown tool "${name}"`);
60+
return;
61+
}
62+
reply(id, await callTool(name, params?.arguments ?? {}));
63+
return;
64+
}
65+
default:
66+
if (isRequest) replyError(id, -32601, `Method not found: ${method}`);
67+
}
68+
}
69+
70+
/** Serve until stdin closes (client disconnect). Never returns. */
71+
export async function serveMcp(): Promise<never> {
72+
process.stderr.write(`${pkg.name} v${pkg.version} — MCP server ready (stdio)\n`);
73+
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
74+
for await (const line of rl) {
75+
const trimmed = line.trim();
76+
if (!trimmed) continue;
77+
let msg: any;
78+
try {
79+
msg = JSON.parse(trimmed);
80+
} catch {
81+
replyError(null, -32700, 'Parse error: messages must be one JSON object per line');
82+
continue;
83+
}
84+
try {
85+
await handleMessage(msg);
86+
} catch (error) {
87+
const message = error instanceof Error ? error.message : String(error);
88+
replyError(msg?.id ?? null, -32603, `Internal error: ${message}`);
89+
}
90+
}
91+
process.exit(0); // stdin closed — client disconnected
92+
}

0 commit comments

Comments
 (0)