@wirecat/cli-core 0.461.0 → 0.463.0
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/CHANGELOG.md +41 -0
- package/dist/cli/bot/mcp.js +11 -3
- package/dist/cli/guide.js +1 -1
- package/dist/cli/mcp-server-command.d.ts +9 -4
- package/dist/cli/mcp-server-command.js +16 -15
- package/dist/cli/messenger/mcp-command.d.ts +3 -4
- package/dist/cli/messenger/mcp-command.js +42 -26
- package/dist/cli/messenger/messages-search-command.js +3 -3
- package/dist/cli/messenger/search-answers.js +2 -5
- package/dist/cli/skill-command.js +5 -1
- package/dist/kit/mcp-stdio.d.ts +2 -2
- package/dist/kit/mcp-stdio.js +2 -2
- package/dist/mcp/http/activity.d.ts +1 -1
- package/dist/mcp/http/activity.js +1 -1
- package/dist/mcp/http/oauth.js +1 -1
- package/dist/services/messages-search.js +4 -11
- package/package.json +1 -1
- package/parity.json +30 -22
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,47 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.463.0 — 27.10.2026
|
|
11
|
+
|
|
12
|
+
### Changed — may break callers
|
|
13
|
+
|
|
14
|
+
- **The MCP server is `mcp serve`; `mcp` alone only shows its commands**
|
|
15
|
+
([WC-CMD-3](https://github.com/WireCatLabs/cli-base/blob/main/docs/standards/commands.md#wc-cmd-3--give-a-group-without-a-leaf-only-its-help-never-a-hidden-default-action)).
|
|
16
|
+
`mcpCommand` gives the `mcp` group with `serve`, `revoke`, `config`, `add`, `check` and `server`; `--http`, `--port`,
|
|
17
|
+
`--public-url` and `--permission` move from `mcp` to `mcp serve`, and `mcp --revoke` is now its own command,
|
|
18
|
+
`mcp revoke`, with no flag left behind. An agent is set up with
|
|
19
|
+
`claude mcp add <cli> -- <cli> <profile> mcp serve`. `bot mcp` is a group too, and the bot's server is
|
|
20
|
+
`bot mcp serve`. `serverEntry`, so `mcp config`, `mcp add`, `mcp check` and `bot mcp config`, writes `mcp serve`,
|
|
21
|
+
the server of every profile starts `<cli> <profile> mcp serve` for each, and `mcp server start` runs
|
|
22
|
+
`mcp serve --http`. There is no alias: an old client entry that runs `<cli> <profile> mcp` gets the group's help and
|
|
23
|
+
exits 2 — print it again with `mcp config` or `mcp add`.
|
|
24
|
+
- **`mcpServerCommand` takes `--permission` only when asked.** `mcp server start` offered `--permission` to
|
|
25
|
+
every CLI and passed it to a server that might not take it; that server died before it listened and `start`
|
|
26
|
+
answered `provider_unavailable` with `retry.safe: true`, so an agent retried for ever. A CLI whose `mcp serve` takes
|
|
27
|
+
`--permission` passes `permission: true` (`mcpCommand` does); otherwise `start --permission` is refused as an unknown
|
|
28
|
+
option before anything starts. A `--permission` set on the CLI's root is still handed over. A server that stops
|
|
29
|
+
before it listens is now `retry.safe: false`. A CLI's own target names its server as `argv: [..., "mcp", "serve",
|
|
30
|
+
"--http"]` once its `mcp` is a group too.
|
|
31
|
+
|
|
32
|
+
## 0.462.0 — 27.10.2026
|
|
33
|
+
|
|
34
|
+
### Changed — may break callers
|
|
35
|
+
|
|
36
|
+
- **`skill show`, `skill status` and `skill install` answer `--json` in the one-thing envelope**
|
|
37
|
+
([WC-OUT-4](https://github.com/WireCatLabs/cli-base/blob/main/docs/standards/output.md#wc-out-4--answer-every-json-request-with-one-envelope-a-list-or-one-thing)),
|
|
38
|
+
`{ item, meta }`, and `--jsonl` as one item line and the end line, like every other command and as their declared
|
|
39
|
+
output schema already said; they printed the bare object. MCP tool answers change the same way. `commands --json`
|
|
40
|
+
does not change. A caller reads the old fields under `item`: the `install/windows.ps1` installers of tg-cli, max-cli
|
|
41
|
+
and whatsapp-cli read `$skill.written`, and tests in tg-cli (`src/archive.test.ts`), max-cli
|
|
42
|
+
(`src/commands/skill.test.ts`) and cli-template (`src/roots.test.ts`) read the bare shape.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- **An empty `messages` search no longer suggests `messages sync --all --background`.** `coverage.next` and the
|
|
47
|
+
`messages.sync` step now name one chat (`<cli> messages sync <chat>`: the selected chat, or the most active one
|
|
48
|
+
that was never fetched or is behind), and the search help says to offer it and run it only after the owner
|
|
49
|
+
approves. A sync downloads from the owner's account, so an agent must not start an unbounded one on its own.
|
|
50
|
+
|
|
10
51
|
## 0.461.0 — 27.10.2026
|
|
11
52
|
|
|
12
53
|
### Changed — may break callers
|
package/dist/cli/bot/mcp.js
CHANGED
|
@@ -12,8 +12,16 @@ import { botExamples } from "./examples.js";
|
|
|
12
12
|
/** `bot mcp`, mounted only for a messenger that hands in its `run` on `BotMessenger.mcp`. */
|
|
13
13
|
export const botMcpCommand = (bot) => {
|
|
14
14
|
const { app } = bot;
|
|
15
|
-
const command = annotate(new Command("mcp"), { effects:
|
|
16
|
-
.description(`serve this bot to an agent over MCP
|
|
15
|
+
const command = annotate(new Command("mcp"), { effects: LOCAL_READ, mcp: false })
|
|
16
|
+
.description(`serve this bot to an agent over MCP and set it up — \`claude mcp add sales-bot -- ${app.command} sales bot mcp serve\``)
|
|
17
|
+
.summary("serve this bot to an agent over MCP, and set that up");
|
|
18
|
+
command.addCommand(annotate(new Command("serve"), {
|
|
19
|
+
effects: REMOTE_WRITE,
|
|
20
|
+
lifetime: "persistent",
|
|
21
|
+
mcp: false,
|
|
22
|
+
examples: botExamples(bot, "mcp serve"),
|
|
23
|
+
})
|
|
24
|
+
.description(`serve this bot to an agent over MCP, on stdin and stdout — \`claude mcp add sales-bot -- ${app.command} sales bot mcp serve\``)
|
|
17
25
|
.summary("serve this bot to an agent over MCP, on stdin and stdout")
|
|
18
26
|
.action(async function () {
|
|
19
27
|
const context = botContext(this, bot);
|
|
@@ -35,7 +43,7 @@ export const botMcpCommand = (bot) => {
|
|
|
35
43
|
...(mcp.keyOf ? { keyOf: mcp.keyOf } : {}),
|
|
36
44
|
...(mcp.skill ? { skill: mcp.skill } : {}),
|
|
37
45
|
}, context.streams.diagnostic);
|
|
38
|
-
});
|
|
46
|
+
}));
|
|
39
47
|
command.addCommand(annotate(new Command("config"), { effects: LOCAL_READ, examples: botExamples(bot, "mcp config") })
|
|
40
48
|
.description("print the mcpServers entry for Claude Desktop, Cursor and others, with full paths; writes nothing")
|
|
41
49
|
.summary("print the mcpServers entry for an MCP client; writes nothing")
|
package/dist/cli/guide.js
CHANGED
|
@@ -291,7 +291,7 @@ export const checkStep = (program, run) => {
|
|
|
291
291
|
const beforeDashes = rest.slice(index, rest.includes("--") ? rest.indexOf("--") : undefined);
|
|
292
292
|
if (help && beforeDashes.some((word) => word === help.long || word === help.short))
|
|
293
293
|
return [];
|
|
294
|
-
// A group with an action of its own runs as typed
|
|
294
|
+
// A group with an action of its own runs as typed rather than asking for one of its children.
|
|
295
295
|
const runsItself = at._actionHandler != null;
|
|
296
296
|
if (at.commands.length > 0 && !runsItself)
|
|
297
297
|
return [`needs one of: ${at.commands.map((one) => one.name()).join(", ")}`];
|
|
@@ -10,7 +10,7 @@ export interface McpServerTarget {
|
|
|
10
10
|
envPrefix: string;
|
|
11
11
|
env: NodeJS.ProcessEnv;
|
|
12
12
|
tokenFile: string;
|
|
13
|
-
/** The words after the CLI's own entry that run `mcp --http` in the foreground, before `--port`. */
|
|
13
|
+
/** The words after the CLI's own entry that run `mcp serve --http` in the foreground, before `--port`. */
|
|
14
14
|
argv: string[];
|
|
15
15
|
/** Added to the server's environment: the profile, for a CLI that reads it from there. */
|
|
16
16
|
childEnv?: Record<string, string>;
|
|
@@ -19,17 +19,22 @@ export interface McpServerTarget {
|
|
|
19
19
|
export interface McpServerOptions {
|
|
20
20
|
/** How help shows the CLI before `mcp`: `tg personal`. */
|
|
21
21
|
example: string;
|
|
22
|
-
/** The CLI's own `mcp --http` default, when it is not 8765. */
|
|
22
|
+
/** The CLI's own `mcp serve --http` default, when it is not 8765. */
|
|
23
23
|
defaultPort?: number;
|
|
24
|
+
/**
|
|
25
|
+
* `start` takes `--permission` and hands it to the server: only for a CLI whose `mcp serve` takes it too,
|
|
26
|
+
* since a server that refuses the option dies before it listens.
|
|
27
|
+
*/
|
|
28
|
+
permission?: boolean;
|
|
24
29
|
target: (self: Command) => McpServerTarget;
|
|
25
30
|
/** The CLI's own answer: the item in `--json`, else the lines. */
|
|
26
31
|
answer: (self: Command, item: object, lines: string[]) => void;
|
|
27
32
|
system?: (self: Command) => ServerSystem | undefined;
|
|
28
33
|
}
|
|
29
34
|
/**
|
|
30
|
-
* `mcp --http` in the background: `start`, `stop`, `status`, as `server` does for `serve`. The server
|
|
35
|
+
* `mcp serve --http` in the background: `start`, `stop`, `status`, as `server` does for `serve`. The server
|
|
31
36
|
* keeps a lock beside its token file (`state.ts`) with its port, address and login code, so status
|
|
32
37
|
* survives the shell that started it, and `stop` ends only a process that carries this CLI's marker.
|
|
33
38
|
* None of it is offered over MCP: status holds a live login code.
|
|
34
39
|
*/
|
|
35
|
-
export declare const mcpServerCommand: ({ example, defaultPort, target, answer, system, }: McpServerOptions) => Command;
|
|
40
|
+
export declare const mcpServerCommand: ({ example, defaultPort, permission, target, answer, system, }: McpServerOptions) => Command;
|
|
@@ -40,28 +40,29 @@ const clientLines = (clients) => clients.length === 0
|
|
|
40
40
|
.join(", ")}.`,
|
|
41
41
|
];
|
|
42
42
|
/**
|
|
43
|
-
* `mcp --http` in the background: `start`, `stop`, `status`, as `server` does for `serve`. The server
|
|
43
|
+
* `mcp serve --http` in the background: `start`, `stop`, `status`, as `server` does for `serve`. The server
|
|
44
44
|
* keeps a lock beside its token file (`state.ts`) with its port, address and login code, so status
|
|
45
45
|
* survives the shell that started it, and `stop` ends only a process that carries this CLI's marker.
|
|
46
46
|
* None of it is offered over MCP: status holds a live login code.
|
|
47
47
|
*/
|
|
48
|
-
export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT, target, answer, system, }) => {
|
|
48
|
+
export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT, permission = false, target, answer, system, }) => {
|
|
49
49
|
const command = annotate(new Command("server"), { effects: LOCAL_READ, mcp: false })
|
|
50
50
|
.summary("run the HTTP MCP server in the background")
|
|
51
|
-
.description("start, stop and check `mcp --http` in the background; status shows the login code for a browser app");
|
|
51
|
+
.description("start, stop and check `mcp serve --http` in the background; status shows the login code for a browser app");
|
|
52
52
|
const machine = (self) => system?.(self) ?? thisMachine();
|
|
53
|
-
destroysNothing(annotate(command.command("start"), {
|
|
53
|
+
const start = destroysNothing(annotate(command.command("start"), {
|
|
54
54
|
effects: LOCAL_WRITE,
|
|
55
55
|
mcp: false,
|
|
56
56
|
examples: [`${example} mcp server start --public-url https://laptop.example.ts.net`],
|
|
57
57
|
}))
|
|
58
58
|
.summary("start the HTTP MCP server in the background")
|
|
59
|
-
.description("start `mcp --http` apart from this shell and answer once it listens, with its login code")
|
|
59
|
+
.description("start `mcp serve --http` apart from this shell and answer once it listens, with its login code")
|
|
60
60
|
.option("--port <port>", `the local port (default ${defaultPort})`)
|
|
61
|
-
.option("--public-url <url>", "the tunnel's https address the browser apps use, e.g. https://<name>.ts.net or https://<name>.ts.net/tg")
|
|
62
|
-
|
|
63
|
-
.
|
|
64
|
-
|
|
61
|
+
.option("--public-url <url>", "the tunnel's https address the browser apps use, e.g. https://<name>.ts.net or https://<name>.ts.net/tg");
|
|
62
|
+
if (permission)
|
|
63
|
+
start.option("--permission <key=level>", "make a permission stricter for this server only; repeat for more keys", (value, previous = []) => [...previous, value]);
|
|
64
|
+
start.action(async function () {
|
|
65
|
+
// A CLI with a root-wide `--permission` hands it over too, so the server is never looser than asked.
|
|
65
66
|
const flags = this.optsWithGlobals();
|
|
66
67
|
const one = target(this);
|
|
67
68
|
const url = publicUrlOf(flags.publicUrl, {
|
|
@@ -102,15 +103,15 @@ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT,
|
|
|
102
103
|
}
|
|
103
104
|
if (!alive(pid)) {
|
|
104
105
|
const last = readableLog(tail(log, 5)).trim();
|
|
105
|
-
throw new CliError("provider_unavailable", `mcp --http stopped before it listened${last ? `:\n${last}` : ""}
|
|
106
|
+
throw new CliError("provider_unavailable", `mcp serve --http stopped before it listened${last ? `:\n${last}` : ""}`, { retryable: false });
|
|
106
107
|
}
|
|
107
108
|
await sys.pause();
|
|
108
109
|
}
|
|
109
|
-
throw new CliError("timeout", `mcp --http is running but not listening yet — \`${one.invocation} mcp server status\`; its log: ${log}`);
|
|
110
|
+
throw new CliError("timeout", `mcp serve --http is running but not listening yet — \`${one.invocation} mcp server status\`; its log: ${log}`);
|
|
110
111
|
});
|
|
111
112
|
destroysNothing(annotate(command.command("stop"), { effects: LOCAL_WRITE, mcp: false, examples: [`${example} mcp server stop`] }))
|
|
112
113
|
.summary("stop the background HTTP MCP server")
|
|
113
|
-
.description("stop `mcp --http` — only one that `mcp server start` started")
|
|
114
|
+
.description("stop `mcp serve --http` — only one that `mcp server start` started")
|
|
114
115
|
.action(async function () {
|
|
115
116
|
const one = target(this);
|
|
116
117
|
const lock = readHttpLock(httpLockFile(one.tokenFile));
|
|
@@ -121,14 +122,14 @@ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT,
|
|
|
121
122
|
return;
|
|
122
123
|
}
|
|
123
124
|
if (byOf(one, lock.pid) !== "server") {
|
|
124
|
-
throw new CliError("validation_error", `mcp --http for ${one.invocation} (PID ${lock.pid}) was not started by \`${one.invocation} mcp server start\` — stop it where it runs`);
|
|
125
|
+
throw new CliError("validation_error", `mcp serve --http for ${one.invocation} (PID ${lock.pid}) was not started by \`${one.invocation} mcp server start\` — stop it where it runs`);
|
|
125
126
|
}
|
|
126
127
|
process.kill(lock.pid, "SIGTERM");
|
|
127
128
|
const sys = machine(this);
|
|
128
129
|
for (let look = 0; look < LOOKS && alive(lock.pid); look++)
|
|
129
130
|
await sys.pause();
|
|
130
131
|
if (alive(lock.pid))
|
|
131
|
-
throw new CliError("timeout", `mcp --http (PID ${lock.pid}) has not stopped yet`);
|
|
132
|
+
throw new CliError("timeout", `mcp serve --http (PID ${lock.pid}) has not stopped yet`);
|
|
132
133
|
answer(this, { profile: one.profile ?? null, stopped: true, pid: lock.pid }, [
|
|
133
134
|
`Stopped serving ${one.invocation} over HTTP (PID ${lock.pid}).`,
|
|
134
135
|
]);
|
|
@@ -139,7 +140,7 @@ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT,
|
|
|
139
140
|
examples: [`${example} mcp server status --json`],
|
|
140
141
|
})
|
|
141
142
|
.summary("whether the HTTP MCP server runs, its login code and the apps logged in")
|
|
142
|
-
.description("whether `mcp --http` runs, on which port and address, the login code for a new browser app, and the apps logged in")
|
|
143
|
+
.description("whether `mcp serve --http` runs, on which port and address, the login code for a new browser app, and the apps logged in")
|
|
143
144
|
.action(function () {
|
|
144
145
|
const one = target(this);
|
|
145
146
|
const lock = readHttpLock(httpLockFile(one.tokenFile));
|
|
@@ -17,11 +17,10 @@ export interface McpFlags {
|
|
|
17
17
|
http?: boolean;
|
|
18
18
|
port?: string;
|
|
19
19
|
publicUrl?: string;
|
|
20
|
-
revoke?: boolean;
|
|
21
20
|
}
|
|
22
|
-
/** The token file of `<tool> mcp --http` for every profile: no profile name can start with `_`. */
|
|
21
|
+
/** The token file of `<tool> mcp serve --http` for every profile: no profile name can start with `_`. */
|
|
23
22
|
export declare const ALL_PROFILES = "_all";
|
|
24
|
-
/** Where `mcp --http` keeps the hashes of the tokens it issued — per profile, in the state folder. */
|
|
23
|
+
/** Where `mcp serve --http` keeps the hashes of the tokens it issued — per profile, in the state folder. */
|
|
25
24
|
export declare const httpTokenFile: (app: AppIdentity, profile: string, env: NodeJS.ProcessEnv) => string;
|
|
26
25
|
export declare const mcpCommand: (messenger: Messenger) => Command;
|
|
27
26
|
/**
|
|
@@ -44,7 +43,7 @@ export declare const redactTail: (text: string, home: string | undefined) => str
|
|
|
44
43
|
* servers with a trimmed environment leaves it out (measured 2026-09-28). Credentials are never copied.
|
|
45
44
|
*/
|
|
46
45
|
export declare const serverEntry: (app: AppIdentity, { profile, flags, execPath, scriptPath, env, }: {
|
|
47
|
-
/** Left out, the entry is `<tool> mcp`, the server of every profile. */
|
|
46
|
+
/** Left out, the entry is `<tool> mcp serve`, the server of every profile. */
|
|
48
47
|
profile?: string;
|
|
49
48
|
flags?: McpFlags;
|
|
50
49
|
execPath: string;
|
|
@@ -19,31 +19,36 @@ import { nextStep } from "../steps.js";
|
|
|
19
19
|
import { recalledAccount } from "./accounts.js";
|
|
20
20
|
import { messengerContext } from "./context.js";
|
|
21
21
|
const DEFAULT_PORT = MCP_HTTP_DEFAULT_PORT;
|
|
22
|
-
/** The token file of `<tool> mcp --http` for every profile: no profile name can start with `_`. */
|
|
22
|
+
/** The token file of `<tool> mcp serve --http` for every profile: no profile name can start with `_`. */
|
|
23
23
|
export const ALL_PROFILES = "_all";
|
|
24
|
-
/** Where `mcp --http` keeps the hashes of the tokens it issued — per profile, in the state folder. */
|
|
24
|
+
/** Where `mcp serve --http` keeps the hashes of the tokens it issued — per profile, in the state folder. */
|
|
25
25
|
export const httpTokenFile = (app, profile, env) => join(resolvePaths({ appName: app.appName, prefix: app.envPrefix, env }).state, "mcp-http", `${profile}.json`);
|
|
26
26
|
const withFlags = (command) => command.option("--permission <key=level>", "make a permission stricter for this server only, like messages.send=deny; it never loosens the profile; repeat for more keys", (value, previous = []) => [...previous, value]);
|
|
27
27
|
export const mcpCommand = (messenger) => {
|
|
28
28
|
const { app } = messenger;
|
|
29
|
-
const command =
|
|
29
|
+
const command = annotate(new Command("mcp"), { effects: LOCAL_READ, mcp: false })
|
|
30
|
+
.summary("serve this profile's commands to an AI agent, and set that up")
|
|
31
|
+
.description(`serve this profile to an agent over MCP and set it up — \`claude mcp add ${app.command} -- ${app.command} <profile> mcp serve\``);
|
|
32
|
+
command.addCommand(withFlags(annotate(new Command("serve"), {
|
|
33
|
+
effects: REMOTE_WRITE,
|
|
34
|
+
lifetime: "persistent",
|
|
35
|
+
mcp: false,
|
|
36
|
+
examples: [
|
|
37
|
+
`${app.command} personal mcp serve`,
|
|
38
|
+
`${app.command} personal mcp serve --http --public-url https://laptop.example.ts.net`,
|
|
39
|
+
],
|
|
40
|
+
})
|
|
30
41
|
.summary("serve this profile's commands to an AI agent")
|
|
31
|
-
.description(`serve this profile to an agent over MCP, on stdin and stdout — \`claude mcp add ${app.command} -- ${app.command} <profile> mcp\`; with no profile, \`${app.command} mcp\` serves every profile and each tool asks which`))
|
|
42
|
+
.description(`serve this profile to an agent over MCP, on stdin and stdout — \`claude mcp add ${app.command} -- ${app.command} <profile> mcp serve\`; with no profile, \`${app.command} mcp serve\` serves every profile and each tool asks which`))
|
|
32
43
|
.option("--http", "serve over HTTP on 127.0.0.1 for ChatGPT and Claude in the browser, behind your tunnel; the profile's permissions decide")
|
|
33
44
|
.option("--port <port>", `the local port for --http (default ${DEFAULT_PORT})`)
|
|
34
45
|
.option("--public-url <url>", "the tunnel's https address the browser apps use, e.g. https://<name>.ts.net or https://<name>.ts.net/tg")
|
|
35
|
-
.
|
|
36
|
-
command.action(async function () {
|
|
46
|
+
.action(async function () {
|
|
37
47
|
const flags = this.optsWithGlobals();
|
|
38
48
|
const context = messengerContext(this, messenger, { needsProfile: false });
|
|
39
|
-
|
|
49
|
+
const profile = namedProfile(context.settings);
|
|
50
|
+
if (profile === undefined)
|
|
40
51
|
return serveEveryProfile(this, context, messenger, flags);
|
|
41
|
-
if (flags.revoke) {
|
|
42
|
-
const { revokeAll } = await import("../../mcp/http/oauth.js");
|
|
43
|
-
revokeAll(httpTokenFile(app, context.settings.profile, context.env));
|
|
44
|
-
renderAnswer(this, context, itemOf({ revoked: true, profile: context.settings.profile }));
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
52
|
if (flags.http) {
|
|
48
53
|
const publicUrl = publicUrlOf(flags.publicUrl, {
|
|
49
54
|
missing: "--http",
|
|
@@ -51,7 +56,7 @@ export const mcpCommand = (messenger) => {
|
|
|
51
56
|
path: true,
|
|
52
57
|
});
|
|
53
58
|
const { serveOverHttpUntilStopped } = await import("../../mcp/server.js");
|
|
54
|
-
const tokenFile = httpTokenFile(app,
|
|
59
|
+
const tokenFile = httpTokenFile(app, profile, context.env);
|
|
55
60
|
await serveOverHttpUntilStopped(this, context, messenger, {}, {
|
|
56
61
|
publicUrl,
|
|
57
62
|
port: portOf(flags.port, DEFAULT_PORT),
|
|
@@ -64,9 +69,24 @@ export const mcpCommand = (messenger) => {
|
|
|
64
69
|
// Loaded here, not at the top: every other command would otherwise pay for the SDK.
|
|
65
70
|
const { serveOverStdio } = await import("../../mcp/server.js");
|
|
66
71
|
await serveOverStdio(this, context, messenger, {});
|
|
67
|
-
});
|
|
72
|
+
}));
|
|
73
|
+
command.addCommand(annotate(new Command("revoke"), {
|
|
74
|
+
effects: LOCAL_WRITE,
|
|
75
|
+
mcp: false,
|
|
76
|
+
examples: [`${app.command} personal mcp revoke`],
|
|
77
|
+
})
|
|
78
|
+
.summary("forget every login given to a browser app")
|
|
79
|
+
.description(`forget every login \`mcp serve --http\` gave a browser app, of this profile or, with no profile, of the server of every profile; each app must log in again`)
|
|
80
|
+
.action(async function () {
|
|
81
|
+
const context = messengerContext(this, messenger, { needsProfile: false });
|
|
82
|
+
const profile = namedProfile(context.settings);
|
|
83
|
+
const { revokeAll } = await import("../../mcp/http/oauth.js");
|
|
84
|
+
revokeAll(httpTokenFile(app, profile ?? ALL_PROFILES, context.env));
|
|
85
|
+
renderAnswer(this, context, itemOf({ revoked: true, profile: profile ?? null }));
|
|
86
|
+
}));
|
|
68
87
|
command.addCommand(mcpServerCommand({
|
|
69
88
|
example: `${app.command} personal`,
|
|
89
|
+
permission: true,
|
|
70
90
|
target: (self) => {
|
|
71
91
|
const { settings, env } = messengerContext(self, messenger, { needsProfile: false });
|
|
72
92
|
if (namedProfile(settings) === undefined)
|
|
@@ -75,7 +95,7 @@ export const mcpCommand = (messenger) => {
|
|
|
75
95
|
envPrefix: app.envPrefix,
|
|
76
96
|
env,
|
|
77
97
|
tokenFile: httpTokenFile(app, ALL_PROFILES, env),
|
|
78
|
-
argv: ["mcp", "--http"],
|
|
98
|
+
argv: ["mcp", "serve", "--http"],
|
|
79
99
|
version: app.version,
|
|
80
100
|
};
|
|
81
101
|
return {
|
|
@@ -84,7 +104,7 @@ export const mcpCommand = (messenger) => {
|
|
|
84
104
|
envPrefix: app.envPrefix,
|
|
85
105
|
env,
|
|
86
106
|
tokenFile: httpTokenFile(app, settings.profile, env),
|
|
87
|
-
argv: ["mcp", "--http"],
|
|
107
|
+
argv: ["mcp", "serve", "--http"],
|
|
88
108
|
childEnv: { [`${app.envPrefix}_PROFILE`]: settings.profile },
|
|
89
109
|
version: app.version,
|
|
90
110
|
};
|
|
@@ -194,22 +214,17 @@ export const mcpCommand = (messenger) => {
|
|
|
194
214
|
return command;
|
|
195
215
|
};
|
|
196
216
|
/**
|
|
197
|
-
* `<tool> mcp` with no profile: every profile on this computer, each call in that profile's own
|
|
217
|
+
* `<tool> mcp serve` with no profile: every profile on this computer, each call in that profile's own
|
|
218
|
+
* `<tool> <profile> mcp serve`
|
|
198
219
|
* (`src/mcp/all-profiles.ts`). Nothing picks a profile: each tool takes one, and the agent is told to ask.
|
|
199
220
|
*/
|
|
200
221
|
const serveEveryProfile = async (self, context, messenger, flags) => {
|
|
201
222
|
const { app } = messenger;
|
|
202
223
|
const { env } = context;
|
|
203
224
|
const tokenFile = httpTokenFile(app, ALL_PROFILES, env);
|
|
204
|
-
if (flags.revoke) {
|
|
205
|
-
const { revokeAll } = await import("../../mcp/http/oauth.js");
|
|
206
|
-
revokeAll(tokenFile);
|
|
207
|
-
renderAnswer(self, context, itemOf({ revoked: true, profile: null }));
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
225
|
const profiles = () => messenger.resolveSettings(self.optsWithGlobals(), { env, needsProfile: false, readOnly: true }).profilesHere;
|
|
211
226
|
if (profiles().length === 0)
|
|
212
|
-
throw new CliError("configuration_error", `no ${app.command} profile on this computer yet — set one up, then run \`${app.command} mcp\` again`);
|
|
227
|
+
throw new CliError("configuration_error", `no ${app.command} profile on this computer yet — set one up, then run \`${app.command} mcp serve\` again`);
|
|
213
228
|
const given = environmentOf(self).mcp ?? {};
|
|
214
229
|
const execPath = given.execPath ?? process.execPath;
|
|
215
230
|
const scriptPath = given.scriptPath ?? realpathSync(process.argv[1] ?? "");
|
|
@@ -220,7 +235,7 @@ const serveEveryProfile = async (self, context, messenger, flags) => {
|
|
|
220
235
|
version: app.version,
|
|
221
236
|
name: messenger.name ?? app.command,
|
|
222
237
|
profiles,
|
|
223
|
-
child: (profile) => ({ command: execPath, args: [scriptPath, profile, "mcp", ...tightened], env }),
|
|
238
|
+
child: (profile) => ({ command: execPath, args: [scriptPath, profile, "mcp", "serve", ...tightened], env }),
|
|
224
239
|
instructions: instructions({
|
|
225
240
|
command: app.command,
|
|
226
241
|
name: messenger.name ?? app.command,
|
|
@@ -337,6 +352,7 @@ export const serverEntry = (app, { profile, flags = {}, execPath, scriptPath, en
|
|
|
337
352
|
scriptPath,
|
|
338
353
|
...(profile === undefined ? [] : [profile]),
|
|
339
354
|
"mcp",
|
|
355
|
+
"serve",
|
|
340
356
|
...(flags.permission ?? []).flatMap((entry) => ["--permission", entry]),
|
|
341
357
|
],
|
|
342
358
|
...(Object.keys(directories).length > 0 ? { env: directories } : {}),
|
|
@@ -43,9 +43,9 @@ export const messagesSearchCommand = (messenger) => {
|
|
|
43
43
|
.option("--timezone <zone>", "the IANA timezone for calendar date boundaries")
|
|
44
44
|
.addHelpText("after", SAVE_HELP)
|
|
45
45
|
.addHelpText("after", `${syncsFirst(messenger)
|
|
46
|
-
? `Search reads the local archive: a chat never fetched is not searched.
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
? `Search reads the local archive: a chat never fetched is not searched. The answer's coverage says what was
|
|
47
|
+
searched and coverage.next names one chat that would improve it. A sync downloads from the owner's account, so
|
|
48
|
+
offer \`${messenger.app.command} messages sync <chat>\` and run it only once the owner approves.`
|
|
49
49
|
: `Search reads the local archive: what \`${messenger.app.command} serve\` has kept since the account was linked.
|
|
50
50
|
Keep serve running so new messages are searchable.`} Notes are
|
|
51
51
|
\`${messenger.app.command} search notes\`, messages and notes at once \`${messenger.app.command} search all\`; for mail,
|
|
@@ -72,12 +72,9 @@ export const coverageGaps = (found, shown) => {
|
|
|
72
72
|
const stale = found.refreshed?.complete === false;
|
|
73
73
|
if (!coverage?.next && !stale)
|
|
74
74
|
return { next: [], missing: [] };
|
|
75
|
+
const chat = coverage?.chat ?? coverage?.attention[0]?.chatId;
|
|
75
76
|
return {
|
|
76
|
-
next: [
|
|
77
|
-
nextStep("messages.sync", {
|
|
78
|
-
args: coverage?.chat === undefined ? { all: true, background: true } : { chat: coverage.chat },
|
|
79
|
-
}),
|
|
80
|
-
],
|
|
77
|
+
next: chat === undefined ? [] : [nextStep("messages.sync", { args: { chat } })],
|
|
81
78
|
missing: [
|
|
82
79
|
{
|
|
83
80
|
field: "coverage",
|
|
@@ -2,6 +2,7 @@ import { annotate } from "@wirecat/cli-base/commands";
|
|
|
2
2
|
import { skillCommand as sharedSkillCommand } from "@wirecat/cli-base/skill";
|
|
3
3
|
import { environmentOf, outputFor } from "./context.js";
|
|
4
4
|
import { grantsAuthority } from "./effects.js";
|
|
5
|
+
import { printItem } from "./envelope.js";
|
|
5
6
|
/**
|
|
6
7
|
* `skill show` and `skill install`. `skill` is the CLI's own SKILL.md, shipped beside `dist/` in the
|
|
7
8
|
* package and in a checkout alike, so the skill printed is always this version's.
|
|
@@ -9,7 +10,10 @@ import { grantsAuthority } from "./effects.js";
|
|
|
9
10
|
export const skillCommand = (app, skill) => {
|
|
10
11
|
const command = sharedSkillCommand(app, skill, (command) => {
|
|
11
12
|
const { env } = environmentOf(command);
|
|
12
|
-
|
|
13
|
+
const output = outputFor(command);
|
|
14
|
+
// cli-base prints each answer bare; the envelope is cli-core's.
|
|
15
|
+
const renderer = { ...output.renderer, result: (value) => printItem(output, value) };
|
|
16
|
+
return { ...output, renderer, ...(env ? { env } : {}) };
|
|
13
17
|
}, SHARED_SKILLS);
|
|
14
18
|
// An installed skill is instructions every later agent follows.
|
|
15
19
|
const install = command.commands.find((sub) => sub.name() === "install");
|
package/dist/kit/mcp-stdio.d.ts
CHANGED
|
@@ -19,8 +19,8 @@ export interface McpStdio {
|
|
|
19
19
|
close: () => Promise<number>;
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
* A CLI's `mcp` driven over its stdin and stdout, through its own `run()`, with no process and no SDK:
|
|
23
|
-
* `start` passes the two streams to `run(["<profile>", "mcp"], { ...yourOptions, ...streams })`. It has
|
|
22
|
+
* A CLI's `mcp serve` driven over its stdin and stdout, through its own `run()`, with no process and no SDK:
|
|
23
|
+
* `start` passes the two streams to `run(["<profile>", "mcp", "serve"], { ...yourOptions, ...streams })`. It has
|
|
24
24
|
* already answered `initialize` when this resolves.
|
|
25
25
|
*/
|
|
26
26
|
export declare const mcpOverStdio: (start: (streams: McpStdioStreams) => Promise<number>, { protocolVersion }?: {
|
package/dist/kit/mcp-stdio.js
CHANGED
|
@@ -8,8 +8,8 @@ const parsedOrText = (text) => {
|
|
|
8
8
|
}
|
|
9
9
|
};
|
|
10
10
|
/**
|
|
11
|
-
* A CLI's `mcp` driven over its stdin and stdout, through its own `run()`, with no process and no SDK:
|
|
12
|
-
* `start` passes the two streams to `run(["<profile>", "mcp"], { ...yourOptions, ...streams })`. It has
|
|
11
|
+
* A CLI's `mcp serve` driven over its stdin and stdout, through its own `run()`, with no process and no SDK:
|
|
12
|
+
* `start` passes the two streams to `run(["<profile>", "mcp", "serve"], { ...yourOptions, ...streams })`. It has
|
|
13
13
|
* already answered `initialize` when this resolves.
|
|
14
14
|
*/
|
|
15
15
|
export const mcpOverStdio = async (start, { protocolVersion = "2025-06-18" } = {}) => {
|
|
@@ -15,7 +15,7 @@ export declare const readActivity: (path: string) => Activity;
|
|
|
15
15
|
export declare const calledTools: (body: unknown) => string[];
|
|
16
16
|
/**
|
|
17
17
|
* Keeps the activity file of one running server. A call is written at once, so status shows it while the owner
|
|
18
|
-
* watches; other requests at most every two seconds. Its own file, not the token file: `mcp
|
|
18
|
+
* watches; other requests at most every two seconds. Its own file, not the token file: `mcp revoke` rewrites that
|
|
19
19
|
* from another process.
|
|
20
20
|
*/
|
|
21
21
|
export declare const activityRecorder: (path: string, now?: () => number) => {
|
|
@@ -30,7 +30,7 @@ export const calledTools = (body) => (Array.isArray(body) ? body : [body]).flatM
|
|
|
30
30
|
});
|
|
31
31
|
/**
|
|
32
32
|
* Keeps the activity file of one running server. A call is written at once, so status shows it while the owner
|
|
33
|
-
* watches; other requests at most every two seconds. Its own file, not the token file: `mcp
|
|
33
|
+
* watches; other requests at most every two seconds. Its own file, not the token file: `mcp revoke` rewrites that
|
|
34
34
|
* from another process.
|
|
35
35
|
*/
|
|
36
36
|
export const activityRecorder = (path, now = Date.now) => {
|
package/dist/mcp/http/oauth.js
CHANGED
|
@@ -132,7 +132,7 @@ export const ownerLogin = ({ issuer, resource, file, appName, onCode, now = () =
|
|
|
132
132
|
while (stored.clients.length > MAX_CLIENTS) {
|
|
133
133
|
const idle = stored.clients.findIndex((one) => !holding.has(one.id));
|
|
134
134
|
if (idle === -1)
|
|
135
|
-
return oauthError("invalid_client_metadata", "too many logged-in apps — run mcp
|
|
135
|
+
return oauthError("invalid_client_metadata", "too many logged-in apps — run mcp revoke first");
|
|
136
136
|
stored.clients.splice(idle, 1);
|
|
137
137
|
}
|
|
138
138
|
store.write(stored);
|
|
@@ -235,12 +235,9 @@ export const coverageOf = async (store, { scopeAccounts, selectedChat }, messeng
|
|
|
235
235
|
const completeness = [];
|
|
236
236
|
const stats = new Map();
|
|
237
237
|
let inventoryComplete = scopeAccounts.length > 0;
|
|
238
|
-
let ownInventory = true;
|
|
239
238
|
for (const selected of scopeAccounts) {
|
|
240
239
|
if (!(await store.sync.syncState(selected, CHAT_LIST_KEY))) {
|
|
241
240
|
inventoryComplete = false;
|
|
242
|
-
if (messenger.provider === undefined || selected.provider === messenger.provider)
|
|
243
|
-
ownInventory = false;
|
|
244
241
|
}
|
|
245
242
|
const chatIds = selectedChat
|
|
246
243
|
? selected.provider === selectedChat.account.provider && selected.account === selectedChat.account.account
|
|
@@ -274,15 +271,11 @@ export const coverageOf = async (store, { scopeAccounts, selectedChat }, messeng
|
|
|
274
271
|
const behindCount = own.filter(behind).length;
|
|
275
272
|
// A chat held back to a window stays partial for good, so only what a fetch would change is advised.
|
|
276
273
|
const command = messenger.app?.command;
|
|
277
|
-
|
|
274
|
+
// One named chat, never `--all`: a download is the owner's call and stays bounded to what they approve.
|
|
275
|
+
const syncChat = selectedChat ? (neverFetched || behindCount ? selectedChat : null) : (attention[0] ?? null);
|
|
276
|
+
const next = command === undefined || !syncChat || !syncsOneChat(messenger)
|
|
278
277
|
? null
|
|
279
|
-
:
|
|
280
|
-
? (neverFetched || behindCount) && syncsOneChat(messenger)
|
|
281
|
-
? `${command} messages sync ${selectedChat.chatId}`
|
|
282
|
-
: null
|
|
283
|
-
: (neverFetched || behindCount || !ownInventory) && messenger.history !== "store"
|
|
284
|
-
? `${command} messages sync --all --background`
|
|
285
|
-
: null;
|
|
278
|
+
: `${command} messages sync ${syncChat.chatId}`;
|
|
286
279
|
const state = selectedChat && completeness.length > 0 && completeness.every(({ state }) => state === "complete")
|
|
287
280
|
? "complete"
|
|
288
281
|
: completeness.some(({ state }) => state !== "unknown")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.463.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|
package/parity.json
CHANGED
|
@@ -789,7 +789,7 @@
|
|
|
789
789
|
},
|
|
790
790
|
"--port": {
|
|
791
791
|
"value": "<port>",
|
|
792
|
-
"meaning": "the local port behind the tunnel, for mcp --http or bot watch --public-url"
|
|
792
|
+
"meaning": "the local port behind the tunnel, for mcp serve --http or bot watch --public-url"
|
|
793
793
|
},
|
|
794
794
|
"--priority": {
|
|
795
795
|
"value": "<n>",
|
|
@@ -804,7 +804,7 @@
|
|
|
804
804
|
},
|
|
805
805
|
"--public-url": {
|
|
806
806
|
"value": "<url>",
|
|
807
|
-
"meaning": "the tunnel's https address: the browser apps reach mcp --http through it, a messenger delivers to bot watch"
|
|
807
|
+
"meaning": "the tunnel's https address: the browser apps reach mcp serve --http through it, a messenger delivers to bot watch"
|
|
808
808
|
},
|
|
809
809
|
"--question": {
|
|
810
810
|
"meaning": "match only questions"
|
|
@@ -861,9 +861,6 @@
|
|
|
861
861
|
"value": "<n>",
|
|
862
862
|
"meaning": "the revision you read before editing"
|
|
863
863
|
},
|
|
864
|
-
"--revoke": {
|
|
865
|
-
"meaning": "forget every login given to a browser app over --http"
|
|
866
|
-
},
|
|
867
864
|
"--revote": {
|
|
868
865
|
"meaning": "people may change their vote"
|
|
869
866
|
},
|
|
@@ -1732,7 +1729,6 @@
|
|
|
1732
1729
|
},
|
|
1733
1730
|
"bot mcp": {
|
|
1734
1731
|
"in": ["max", "tg"],
|
|
1735
|
-
"options": {},
|
|
1736
1732
|
"planned": {
|
|
1737
1733
|
"wa": "whatsapp-cli: wa bot API, plans/2026-10-22-wa-bot-api"
|
|
1738
1734
|
}
|
|
@@ -1744,6 +1740,13 @@
|
|
|
1744
1740
|
"wa": "whatsapp-cli: wa bot API, plans/2026-10-22-wa-bot-api"
|
|
1745
1741
|
}
|
|
1746
1742
|
},
|
|
1743
|
+
"bot mcp serve": {
|
|
1744
|
+
"in": ["max", "tg"],
|
|
1745
|
+
"options": {},
|
|
1746
|
+
"planned": {
|
|
1747
|
+
"wa": "whatsapp-cli: wa bot API, plans/2026-10-22-wa-bot-api"
|
|
1748
|
+
}
|
|
1749
|
+
},
|
|
1747
1750
|
"bot me": {
|
|
1748
1751
|
"in": ["max", "tg"],
|
|
1749
1752
|
"planned": {
|
|
@@ -3138,22 +3141,7 @@
|
|
|
3138
3141
|
}
|
|
3139
3142
|
},
|
|
3140
3143
|
"mcp": {
|
|
3141
|
-
"in": "all"
|
|
3142
|
-
"options": {
|
|
3143
|
-
"--http": "all",
|
|
3144
|
-
"--permission": "all",
|
|
3145
|
-
"--port": "all",
|
|
3146
|
-
"--public-url": "all",
|
|
3147
|
-
"--revoke": "all",
|
|
3148
|
-
"--yes": {
|
|
3149
|
-
"in": [],
|
|
3150
|
-
"planned": {
|
|
3151
|
-
"max": "P7",
|
|
3152
|
-
"tg": "P7",
|
|
3153
|
-
"wa": "P7"
|
|
3154
|
-
}
|
|
3155
|
-
}
|
|
3156
|
-
}
|
|
3144
|
+
"in": "all"
|
|
3157
3145
|
},
|
|
3158
3146
|
"mcp add": {
|
|
3159
3147
|
"in": "all",
|
|
@@ -3182,6 +3170,26 @@
|
|
|
3182
3170
|
}
|
|
3183
3171
|
}
|
|
3184
3172
|
},
|
|
3173
|
+
"mcp revoke": {
|
|
3174
|
+
"in": "all"
|
|
3175
|
+
},
|
|
3176
|
+
"mcp serve": {
|
|
3177
|
+
"in": "all",
|
|
3178
|
+
"options": {
|
|
3179
|
+
"--http": "all",
|
|
3180
|
+
"--permission": "all",
|
|
3181
|
+
"--port": "all",
|
|
3182
|
+
"--public-url": "all",
|
|
3183
|
+
"--yes": {
|
|
3184
|
+
"in": [],
|
|
3185
|
+
"planned": {
|
|
3186
|
+
"max": "P7",
|
|
3187
|
+
"tg": "P7",
|
|
3188
|
+
"wa": "P7"
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
},
|
|
3185
3193
|
"mcp server": {
|
|
3186
3194
|
"in": "all"
|
|
3187
3195
|
},
|