@wirecat/cli-core 0.469.0 → 0.471.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 CHANGED
@@ -5,6 +5,55 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
5
5
  minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
6
6
  shape of this file.
7
7
 
8
+ ## Unreleased
9
+
10
+ ## 0.471.0 — 27.10.2026
11
+
12
+ ### Added
13
+
14
+ - **`appMcpCommand(app, configuration, { name, untrusted, permissionExample, skill?, signedIn? })` — the shared
15
+ `mcp` for a CLI that is not a messenger** (gcal, drive and zoom each had their own): `mcp serve | revoke | server
16
+ start|stop|status | config | add | check`, the same commands, flags and answers as a messenger's `mcpCommand`,
17
+ with `--profiles <names|all>` (no profile is ever picked), `--http`, `--port`, `--public-url` and `--permission`
18
+ (a key no command checks is refused). Every command of the program is a tool behind `<cli>_tools_search`,
19
+ `<cli>_read` and `<cli>_write`, run in this process through the CLI's own `run`, under the profile's permissions
20
+ and its `configuration.permissions` keys; `mcp` itself is left out. The instructions come from the new
21
+ `appInstructions`, which requires the kinds of text other people wrote (`untrusted`, WC-MCP-11). Both kinds of
22
+ CLI now build the group from one `mcpGroup(host: McpHost)`.
23
+ - **`appDoctorCommand(app, configuration, { oauthClient? })` — the shared `doctor` for a CLI that is not a
24
+ messenger**: version, runtime, paths, `store: { path, exists }`, `oauth_client` (`wirecat` or `own`, or the
25
+ error), `read_only`, `profile_lock`, the config file with its issues — permission keys no command checks
26
+ included — the profiles and the config file's modes. It opens no keyring, store or network. `appDoctorReport` is
27
+ the same report for a CLI that renders its own.
28
+
29
+ ### Fixed
30
+
31
+ - **Each MCP call has its own `run_id`.** A command an agent ran through `<cli>_read` or `<cli>_write`
32
+ (`runCommandForAgent`) answered with the server process's run id, so two calls of one session had the same
33
+ `meta.run_id`. Each call now runs in a run of its own, `mcp <command>`, keeping the caller's trace.
34
+
35
+ ## 0.470.0 — 27.10.2026
36
+
37
+ ### Changed — may break callers
38
+
39
+ - **An MCP server serves only the profiles it is told**, the owner's choice written in the agent's config
40
+ ([WC-MCP-9](https://github.com/WireCatLabs/cli-base/blob/main/docs/standards/mcp.md#wc-mcp-9--a-server-serves-the-profiles-it-is-told-with-one-connection-for-each)).
41
+ `<cli> mcp serve` with no profile no longer serves every profile: it exits 2 with `validation_error` and lists the
42
+ profiles here. Name one (`<cli> work mcp serve`), a list (`<cli> mcp serve --profiles work,personal`, each name
43
+ must exist) or every profile (`--profiles all`); the startup line on stderr names the profiles served. A first word
44
+ with `--profiles` is refused; under `<PREFIX>_PROFILE_LOCK`, `--profiles` may name the locked profile only
45
+ (`permission_error`). `all` alone is the keyword and is refused inside a list; a profile named `all` is served as
46
+ `<cli> all mcp serve`. The same choice applies to `--http`, `mcp server start|stop|status`, `mcp revoke` and
47
+ `mcp config`; the entry `mcp config --profiles all` prints runs `mcp serve --profiles all`. A list's HTTP server
48
+ has its own login (`mcp-http/_<names>.json`), so its login never opens a wider server; `--profiles all` keeps
49
+ `_all.json`. For callers: `serverEntry` takes `profile` or `profiles` and refuses neither;
50
+ `mcpServerCommand` takes `profiles: true` to give `start|stop|status` the option, and `McpServerTarget.profiles`
51
+ names the set. A client entry from 0.463.0–0.469.0 that runs `<cli> mcp serve` with no profile stops working: print
52
+ it again with `mcp config --profiles all`. A background server of every profile started by those versions: `upgrade`
53
+ stops it and the new version refuses to start it, so start it again with `mcp server start --profiles all`; one
54
+ still running without `upgrade` is not recognised by `mcp server stop --profiles all` (its marker changed) and is
55
+ stopped by its PID.
56
+
8
57
  ## 0.469.0 — 27.10.2026
9
58
 
10
59
  ### Changed — may break callers
@@ -0,0 +1,107 @@
1
+ import { Command } from "commander";
2
+ import { type AppIdentity } from "../ports/app.js";
3
+ import type { Configuration } from "./settings.js";
4
+ export interface AppDoctorOptions {
5
+ /**
6
+ * Which OAuth client the CLI signs in with — WireCat's or one of the owner's own — read from the environment and
7
+ * files only. A throw is reported as its message.
8
+ */
9
+ oauthClient?: (env: NodeJS.ProcessEnv) => {
10
+ client: "wirecat" | "own";
11
+ };
12
+ }
13
+ /**
14
+ * Run when everything else is broken, so a problem in the config file or the OAuth setting is a field, never a throw.
15
+ * It reads no keyring, opens no store and calls no service: each can hang or prompt. `fixes` are the `chmod` commands
16
+ * that make the config private; they go to stderr, not into the answer.
17
+ */
18
+ export declare const appDoctorReport: (app: AppIdentity, configuration: Configuration, env: NodeJS.ProcessEnv, command: Command, options?: AppDoctorOptions) => {
19
+ report: {
20
+ version: string;
21
+ runtime: {
22
+ node: string;
23
+ platform: NodeJS.Platform;
24
+ };
25
+ paths: {
26
+ config: string;
27
+ state: string;
28
+ cache: string;
29
+ };
30
+ store: {
31
+ path: string;
32
+ exists: boolean;
33
+ };
34
+ oauth_client?: {
35
+ client: "wirecat" | "own" | null;
36
+ error?: string;
37
+ } | undefined;
38
+ read_only: string | null;
39
+ profile_lock: string | null;
40
+ config: {
41
+ path: string;
42
+ found: boolean;
43
+ issues: import("./config-issues.js").ConfigIssue[];
44
+ };
45
+ profiles: string[];
46
+ files: {
47
+ checked: false;
48
+ reason: string;
49
+ } | {
50
+ checked: true;
51
+ ok: boolean;
52
+ problems: {
53
+ path: string;
54
+ mode: string;
55
+ want: string;
56
+ }[];
57
+ };
58
+ };
59
+ fixes: string[] | never[];
60
+ } | {
61
+ report: {
62
+ version: string;
63
+ runtime: {
64
+ node: string;
65
+ platform: NodeJS.Platform;
66
+ };
67
+ paths: {
68
+ config: string;
69
+ state: string;
70
+ cache: string;
71
+ };
72
+ store: {
73
+ path: string;
74
+ exists: boolean;
75
+ };
76
+ oauth_client?: {
77
+ client: "wirecat" | "own" | null;
78
+ error?: string;
79
+ } | undefined;
80
+ read_only: string | null;
81
+ profile_lock: string | null;
82
+ config: {
83
+ path: string;
84
+ found: boolean;
85
+ issues: never[];
86
+ };
87
+ settings: {
88
+ error: string;
89
+ };
90
+ profiles: null;
91
+ files: {
92
+ checked: false;
93
+ reason: string;
94
+ } | {
95
+ checked: true;
96
+ ok: boolean;
97
+ problems: {
98
+ path: string;
99
+ mode: string;
100
+ want: string;
101
+ }[];
102
+ };
103
+ };
104
+ fixes: string[] | never[];
105
+ };
106
+ /** `doctor` for a CLI that is not a messenger: gcal, drive, zoom. A messenger's is `doctorCommand(messenger)`. */
107
+ export declare const appDoctorCommand: (app: AppIdentity, configuration: Configuration, options?: AppDoctorOptions) => Command;
@@ -0,0 +1,101 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { CliError, configFilePath, resolvePaths } from "@wirecat/cli-base";
4
+ import { annotate } from "@wirecat/cli-base/commands";
5
+ import { Command } from "commander";
6
+ import { envName, readOnlyLockOf } from "../ports/app.js";
7
+ import { storePath } from "../store/path.js";
8
+ import { environmentOf, outputFor } from "./context.js";
9
+ import { LOCAL_READ } from "./effects.js";
10
+ import { itemOf, renderAnswer } from "./envelope.js";
11
+ import { privateFiles } from "./messenger/private-files.js";
12
+ import { unknownPermissionIssues } from "./permission-keys.js";
13
+ import { profileFree } from "./profile.js";
14
+ const failure = (error) => {
15
+ if (error instanceof CliError)
16
+ return error.message;
17
+ throw error;
18
+ };
19
+ const configFiles = (path) => {
20
+ const files = privateFiles({ files: [path], dirs: [dirname(path)] });
21
+ if (!files.checked)
22
+ return { files, fixes: [] };
23
+ return {
24
+ files: { ...files, problems: files.problems.map(({ path, mode, want }) => ({ path, mode, want })) },
25
+ fixes: files.problems.map((problem) => problem.fix),
26
+ };
27
+ };
28
+ /**
29
+ * Run when everything else is broken, so a problem in the config file or the OAuth setting is a field, never a throw.
30
+ * It reads no keyring, opens no store and calls no service: each can hang or prompt. `fixes` are the `chmod` commands
31
+ * that make the config private; they go to stderr, not into the answer.
32
+ */
33
+ export const appDoctorReport = (app, configuration, env, command, options = {}) => {
34
+ const paths = resolvePaths({ appName: app.appName, prefix: app.envPrefix, env });
35
+ const store = resolve(storePath(env));
36
+ let oauth;
37
+ if (options.oauthClient)
38
+ try {
39
+ oauth = options.oauthClient(env);
40
+ }
41
+ catch (error) {
42
+ oauth = { client: null, error: failure(error) };
43
+ }
44
+ const common = {
45
+ version: app.version,
46
+ runtime: { node: process.versions.node, platform: process.platform },
47
+ paths: { config: paths.config, state: paths.state, cache: paths.cache },
48
+ store: { path: store, exists: existsSync(store) },
49
+ ...(oauth ? { oauth_client: oauth } : {}),
50
+ read_only: readOnlyLockOf(app, env) ?? null,
51
+ profile_lock: env[envName(app, "PROFILE_LOCK")]?.trim() || null,
52
+ };
53
+ try {
54
+ const settings = configuration.resolveSettings({}, { env, readOnly: true, needsProfile: false, withIssues: true });
55
+ const { files, fixes } = configFiles(settings.configPath);
56
+ return {
57
+ report: {
58
+ ...common,
59
+ config: {
60
+ path: settings.configPath,
61
+ found: settings.configFound,
62
+ issues: [
63
+ ...(settings.configIssues ?? []),
64
+ ...unknownPermissionIssues(command, settings, app.command, configuration.permissions),
65
+ ],
66
+ },
67
+ profiles: settings.configuredProfiles,
68
+ files,
69
+ },
70
+ fixes,
71
+ };
72
+ }
73
+ catch (error) {
74
+ const file = configFilePath(paths.config);
75
+ const { files, fixes } = configFiles(file);
76
+ return {
77
+ report: {
78
+ ...common,
79
+ config: { path: file, found: existsSync(file), issues: [] },
80
+ settings: { error: failure(error) },
81
+ profiles: null,
82
+ files,
83
+ },
84
+ fixes,
85
+ };
86
+ }
87
+ };
88
+ /** `doctor` for a CLI that is not a messenger: gcal, drive, zoom. A messenger's is `doctorCommand(messenger)`. */
89
+ export const appDoctorCommand = (app, configuration, options = {}) => profileFree(annotate(new Command("doctor"), {
90
+ effects: LOCAL_READ,
91
+ examples: [`${app.command} doctor`, `${app.command} doctor --json`],
92
+ }))
93
+ .description("check this installation: version, paths, config, profiles, store; no keyring or network")
94
+ .action(function () {
95
+ const env = environmentOf(this).env ?? process.env;
96
+ const { report, fixes } = appDoctorReport(app, configuration, env, this, options);
97
+ const output = outputFor(this);
98
+ renderAnswer(this, output, itemOf(report));
99
+ for (const fix of fixes)
100
+ output.renderer.note(`to make it private: ${fix}`);
101
+ });
@@ -0,0 +1,23 @@
1
+ import type { Command } from "commander";
2
+ import type { AppServerOptions } from "../mcp/app-server.js";
3
+ import type { AppIdentity } from "../ports/app.js";
4
+ import type { Configuration } from "./settings.js";
5
+ export interface AppMcpOptions {
6
+ /** `Google Calendar`: what the instructions call the service. */
7
+ name: string;
8
+ /** The kinds of text other people wrote that the answers carry: `event titles, descriptions, attendee names`. */
9
+ untrusted: AppServerOptions["untrusted"];
10
+ /** One of the CLI's permission keys for `--permission`'s help: `calendar.events.delete=deny`. */
11
+ permissionExample: string;
12
+ skill?: URL;
13
+ /** What the CLI alone knows stops a command for a profile, such as an OAuth grant without the scope. */
14
+ availability?: AppServerOptions["availability"];
15
+ /** Whether the profile can reach the service, read from disk; left out, `mcp check` does not say. */
16
+ signedIn?: (profile: string, env: NodeJS.ProcessEnv) => boolean;
17
+ }
18
+ /**
19
+ * `mcp serve | revoke | server | config | add | check` for a CLI that is not a messenger — the same commands, flags
20
+ * and answers as a messenger's `mcpCommand`, with `--profiles` and `--permission`, served by the CLI's own program
21
+ * (`createAppServer`). gcal, drive and zoom mount this instead of an `mcp` of their own.
22
+ */
23
+ export declare const appMcpCommand: (app: AppIdentity, configuration: Configuration, options: AppMcpOptions) => Command;
@@ -0,0 +1,49 @@
1
+ import { baseContext } from "./context.js";
2
+ import { mcpGroup } from "./messenger/mcp-command.js";
3
+ import { refuseUnknownPermissionKeys } from "./permission-keys.js";
4
+ /**
5
+ * `mcp serve | revoke | server | config | add | check` for a CLI that is not a messenger — the same commands, flags
6
+ * and answers as a messenger's `mcpCommand`, with `--profiles` and `--permission`, served by the CLI's own program
7
+ * (`createAppServer`). gcal, drive and zoom mount this instead of an `mcp` of their own.
8
+ */
9
+ export const appMcpCommand = (app, configuration, options) => {
10
+ const server = () => ({
11
+ app,
12
+ name: options.name,
13
+ untrusted: options.untrusted,
14
+ resolveSettings: configuration.resolveSettings,
15
+ permissions: configuration.permissions,
16
+ ...(options.skill ? { skill: options.skill } : {}),
17
+ ...(options.availability ? { availability: options.availability } : {}),
18
+ });
19
+ // `mcp serve` has no key of its own, so the shared check before a run never looks at the keys --permission names.
20
+ const checked = (self, context) => refuseUnknownPermissionKeys(self, context.settings, app.command, configuration.permissions);
21
+ const host = {
22
+ app,
23
+ name: options.name,
24
+ resolveSettings: configuration.resolveSettings,
25
+ context: (self, given) => baseContext(self, (flags, resolve) => configuration.resolveSettings(flags, { ...resolve, needsProfile: given?.needsProfile ?? true })),
26
+ permissionExample: options.permissionExample,
27
+ serveStdio: async (self, context) => {
28
+ checked(self, context);
29
+ const { serveAppOverStdio } = await import("../mcp/app-server.js");
30
+ await serveAppOverStdio(self, context, server());
31
+ },
32
+ serveHttp: async (self, context, http) => {
33
+ checked(self, context);
34
+ const { serveAppOverHttp } = await import("../mcp/app-server.js");
35
+ await serveAppOverHttp(self, context, server(), http);
36
+ },
37
+ instructions: async (profiles) => {
38
+ const { appInstructions } = await import("../mcp/instructions.js");
39
+ return appInstructions({
40
+ command: app.command,
41
+ name: options.name,
42
+ profile: profiles,
43
+ untrusted: options.untrusted,
44
+ });
45
+ },
46
+ signedIn: (_self, profile, env) => options.signedIn?.(profile, env),
47
+ };
48
+ return mcpGroup(host);
49
+ };
@@ -40,6 +40,8 @@ export { TASK_PAIR_QUERY, TASK_QUERY } from "../services/tasks.js";
40
40
  export { answerStdinOption, type Need, type SignInSteps, type SignInStepsOptions, signInSteps, } from "../terminal/sign-in-steps.js";
41
41
  export { type AgentPolicy, type AgentTask, type EvaluationResult, evaluateAgent, type Observation, } from "./agent-evaluation.js";
42
42
  export { answerEntity } from "./answer-entity.js";
43
+ export { type AppDoctorOptions, appDoctorReport } from "./app-doctor-command.js";
44
+ export type { AppMcpOptions } from "./app-mcp-command.js";
43
45
  export type { ApiCommandInput, ApiCommands } from "./bot/api.js";
44
46
  export { apiFlagOf, apiJson, apiOptionKey, apiPlainJson, checkApiBody, checkApiParameter, parseApiJson, readApiBody, } from "./bot/api-input.js";
45
47
  export { prepareRpcApiBody, type RpcApiBody } from "./bot/api-rpc.js";
@@ -90,7 +92,7 @@ export { terminalAsker } from "./messenger/ask.js";
90
92
  export type { CompletionOptions } from "./messenger/complete-command.js";
91
93
  export { type ConnectOptions, connectFor, type Messenger, type MessengerContext, messengerContext, type ProfileState, type ReadConnection, type StoredMessages, } from "./messenger/context.js";
92
94
  export { COMMAND_NEEDS } from "./messenger/lacks.js";
93
- export { httpTokenFile, type McpEnvironment, serverEntry } from "./messenger/mcp-command.js";
95
+ export { httpTokenFile, type McpEnvironment, type McpHost, serverEntry } from "./messenger/mcp-command.js";
94
96
  export { type MessageItem, messageItem, type ReadMessage } from "./messenger/message-items.js";
95
97
  export { type ModeProblem, type PrivateFiles, privateFiles, withSqliteSidecars } from "./messenger/private-files.js";
96
98
  export { buildReport as buildDoctorReport, type Report as DoctorReport, type ReportContext as DoctorReportContext, reportFileName as doctorReportFileName, } from "./messenger/report.js";
@@ -111,7 +113,7 @@ export { fieldsOf, projectFields } from "./result-fields.js";
111
113
  export { type Config, type Configuration, count, first, flag, fromFile, type GlobalFlags, NONE_NAMED, namedProfile, type ProfileKind, plain, type ResolveOptions, type Settings, type SettingsExtension, type SettingsExtensionContext, type Source, settingsFor, } from "./settings.js";
112
114
  export { type PermissionAnswer, readSetupAnswers, type SetupAnswers, setupPermissionCeiling } from "./setup-answers.js";
113
115
  export { AGENTS, type Agent, agentFor, screenFor } from "./setup-screen.js";
114
- export { accountCommand, addMeetingBatchCommand, addMeetingPreparationCommand, addMeetingProjectCandidatesCommand, addMeetingSeriesInventoryCommand, attachmentsCommand, botAdminsCommand, botCommand, botMcpCommand, botMembersCommand, botMessagesCommand, callsCommand, chatsCommand, commandsCommand, completeCommand, configCommand, contactsCommand, conversationsCommand, deleteCommand, doctorCommand, editCommand, floodCommand, forwardCommand, generatedApiCommand, guideCommand, imagesCommand, inboxCommand, markReadCommand, mcpCommand, mentionsCommand, messagesCommand, metadataCommand, modelCatalogueCommand, modelsCommand, pinCommand, pollsCommand, reactionsCommand, recipientsCommand, repliesCommand, reviewCommand, runsCommand, scheduleCommand, searchCommand, searchesCommand, sendCommand, sendsCommand, serveCommand, serverCommand, skillCommand, starCommands, statsCommand, stickersCommand, storeCommand, tagsCommand, tasksCommand, tasksStatsCommand, topicsCommand, unpinCommand, upgradeCommand, watchCommand, } from "./shared-command-exports.js";
116
+ export { accountCommand, addMeetingBatchCommand, addMeetingPreparationCommand, addMeetingProjectCandidatesCommand, addMeetingSeriesInventoryCommand, appDoctorCommand, appMcpCommand, attachmentsCommand, botAdminsCommand, botCommand, botMcpCommand, botMembersCommand, botMessagesCommand, callsCommand, chatsCommand, commandsCommand, completeCommand, configCommand, contactsCommand, conversationsCommand, deleteCommand, doctorCommand, editCommand, floodCommand, forwardCommand, generatedApiCommand, guideCommand, imagesCommand, inboxCommand, markReadCommand, mcpCommand, mentionsCommand, messagesCommand, metadataCommand, modelCatalogueCommand, modelsCommand, pinCommand, pollsCommand, reactionsCommand, recipientsCommand, repliesCommand, reviewCommand, runsCommand, scheduleCommand, searchCommand, searchesCommand, sendCommand, sendsCommand, serveCommand, serverCommand, skillCommand, starCommands, statsCommand, stickersCommand, storeCommand, tagsCommand, tasksCommand, tasksStatsCommand, topicsCommand, unpinCommand, upgradeCommand, watchCommand, } from "./shared-command-exports.js";
115
117
  export { assertSharedContracts, customizeSharedCommand, hasSharedContract, overrideSharedContract, type SharedContractOverride, sharedContractOverrides, sharedContracts, } from "./shared-contracts.js";
116
118
  export { runnable, withSafety } from "./steps.js";
117
119
  export { type StoreInfo, sharedStoreCommand, storeInfo, storeInfoCommand } from "./store-info.js";
package/dist/cli/index.js CHANGED
@@ -35,6 +35,7 @@ export { TASK_PAIR_QUERY, TASK_QUERY } from "../services/tasks.js";
35
35
  export { answerStdinOption, signInSteps, } from "../terminal/sign-in-steps.js";
36
36
  export { evaluateAgent, } from "./agent-evaluation.js";
37
37
  export { answerEntity } from "./answer-entity.js";
38
+ export { appDoctorReport } from "./app-doctor-command.js";
38
39
  export { apiFlagOf, apiJson, apiOptionKey, apiPlainJson, checkApiBody, checkApiParameter, parseApiJson, readApiBody, } from "./bot/api-input.js";
39
40
  export { prepareRpcApiBody } from "./bot/api-rpc.js";
40
41
  export { botContext, botWords } from "./bot/context.js";
@@ -102,7 +103,7 @@ export { fieldsOf, projectFields } from "./result-fields.js";
102
103
  export { count, first, flag, fromFile, NONE_NAMED, namedProfile, plain, settingsFor, } from "./settings.js";
103
104
  export { readSetupAnswers, setupPermissionCeiling } from "./setup-answers.js";
104
105
  export { AGENTS, agentFor, screenFor } from "./setup-screen.js";
105
- export { accountCommand, addMeetingBatchCommand, addMeetingPreparationCommand, addMeetingProjectCandidatesCommand, addMeetingSeriesInventoryCommand, attachmentsCommand, botAdminsCommand, botCommand, botMcpCommand, botMembersCommand, botMessagesCommand, callsCommand, chatsCommand, commandsCommand, completeCommand, configCommand, contactsCommand, conversationsCommand, deleteCommand, doctorCommand, editCommand, floodCommand, forwardCommand, generatedApiCommand, guideCommand, imagesCommand, inboxCommand, markReadCommand, mcpCommand, mentionsCommand, messagesCommand, metadataCommand, modelCatalogueCommand, modelsCommand, pinCommand, pollsCommand, reactionsCommand, recipientsCommand, repliesCommand, reviewCommand, runsCommand, scheduleCommand, searchCommand, searchesCommand, sendCommand, sendsCommand, serveCommand, serverCommand, skillCommand, starCommands, statsCommand, stickersCommand, storeCommand, tagsCommand, tasksCommand, tasksStatsCommand, topicsCommand, unpinCommand, upgradeCommand, watchCommand, } from "./shared-command-exports.js";
106
+ export { accountCommand, addMeetingBatchCommand, addMeetingPreparationCommand, addMeetingProjectCandidatesCommand, addMeetingSeriesInventoryCommand, appDoctorCommand, appMcpCommand, attachmentsCommand, botAdminsCommand, botCommand, botMcpCommand, botMembersCommand, botMessagesCommand, callsCommand, chatsCommand, commandsCommand, completeCommand, configCommand, contactsCommand, conversationsCommand, deleteCommand, doctorCommand, editCommand, floodCommand, forwardCommand, generatedApiCommand, guideCommand, imagesCommand, inboxCommand, markReadCommand, mcpCommand, mentionsCommand, messagesCommand, metadataCommand, modelCatalogueCommand, modelsCommand, pinCommand, pollsCommand, reactionsCommand, recipientsCommand, repliesCommand, reviewCommand, runsCommand, scheduleCommand, searchCommand, searchesCommand, sendCommand, sendsCommand, serveCommand, serverCommand, skillCommand, starCommands, statsCommand, stickersCommand, storeCommand, tagsCommand, tasksCommand, tasksStatsCommand, topicsCommand, unpinCommand, upgradeCommand, watchCommand, } from "./shared-command-exports.js";
106
107
  export { assertSharedContracts, customizeSharedCommand, hasSharedContract, overrideSharedContract, sharedContractOverrides, sharedContracts, } from "./shared-contracts.js";
107
108
  export { runnable, withSafety } from "./steps.js";
108
109
  export { sharedStoreCommand, storeInfo, storeInfoCommand } from "./store-info.js";
@@ -5,8 +5,14 @@ export declare const MCP_HTTP_DEFAULT_PORT = 8765;
5
5
  export interface McpServerTarget {
6
6
  /** What a person types before `mcp`: `tg personal`, `memo`. */
7
7
  invocation: string;
8
- /** Absent for a CLI without profiles. */
8
+ /** Absent for a CLI without profiles, and for a server of several. */
9
9
  profile?: string;
10
+ /** A server of several profiles: what `--profiles` took, its token file's name and the profiles it names. */
11
+ profiles?: {
12
+ option: string;
13
+ key: string;
14
+ label: string;
15
+ };
10
16
  envPrefix: string;
11
17
  env: NodeJS.ProcessEnv;
12
18
  tokenFile: string;
@@ -26,6 +32,8 @@ export interface McpServerOptions {
26
32
  * since a server that refuses the option dies before it listens.
27
33
  */
28
34
  permission?: boolean;
35
+ /** `start`, `stop` and `status` take `--profiles`, for a CLI whose `mcp serve` serves several profiles. */
36
+ profiles?: boolean;
29
37
  target: (self: Command) => McpServerTarget;
30
38
  /** The CLI's own answer: the item in `--json`, else the lines. */
31
39
  answer: (self: Command, item: object, lines: string[]) => void;
@@ -37,4 +45,4 @@ export interface McpServerOptions {
37
45
  * survives the shell that started it, and `stop` ends only a process that carries this CLI's marker.
38
46
  * None of it is offered over MCP: status holds a live login code.
39
47
  */
40
- export declare const mcpServerCommand: ({ example, defaultPort, permission, target, answer, system, }: McpServerOptions) => Command;
48
+ export declare const mcpServerCommand: ({ example, defaultPort, permission, profiles, target, answer, system, }: McpServerOptions) => Command;
@@ -18,7 +18,16 @@ const connectorUrl = (lock) => mcpUrlOf(new URL(lock.publicUrl)).href;
18
18
  const liveCode = (lock, now) => lock.loginCode && lock.loginCodeExpiresAt && Date.parse(lock.loginCodeExpiresAt) > now
19
19
  ? { code: lock.loginCode, expires_at: lock.loginCodeExpiresAt }
20
20
  : null;
21
- const markerOf = (target) => `${mcpHttpMarker(target.envPrefix)}=${target.profile ?? "default"}`;
21
+ const markerValue = (target) => target.profile ?? target.profiles?.key ?? "default";
22
+ const markerOf = (target) => `${mcpHttpMarker(target.envPrefix)}=${markerValue(target)}`;
23
+ /** How a line names the server: `chat personal`, or `chat` and the profiles it serves. */
24
+ const shown = (target) => target.profiles ? `${target.invocation}, ${target.profiles.label},` : target.invocation;
25
+ /** The `mcp server <verb>` that finds this server again. */
26
+ const again = (target, verb) => `${target.invocation} mcp server ${verb}${target.profiles ? ` --profiles ${target.profiles.option}` : ""}`;
27
+ const named = (target) => ({
28
+ profile: target.profile ?? null,
29
+ ...(target.profiles ? { profiles: target.profiles.option } : {}),
30
+ });
22
31
  /** Who started it decides whether `stop` may end it: only a process that carries our marker. */
23
32
  const byOf = (target, pid) => (carries(pid, markerOf(target)) ? "server" : "hand");
24
33
  const serving = (target, lock, now) => ({
@@ -45,7 +54,7 @@ const clientLines = (clients) => clients.length === 0
45
54
  * survives the shell that started it, and `stop` ends only a process that carries this CLI's marker.
46
55
  * None of it is offered over MCP: status holds a live login code.
47
56
  */
48
- export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT, permission = false, target, answer, system, }) => {
57
+ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT, permission = false, profiles = false, target, answer, system, }) => {
49
58
  const command = annotate(new Command("server"), { effects: LOCAL_READ, mcp: false })
50
59
  .summary("run the HTTP MCP server in the background")
51
60
  .description("start, stop and check `mcp serve --http` in the background; status shows the login code for a browser app");
@@ -74,8 +83,8 @@ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT,
74
83
  const lockFile = httpLockFile(one.tokenFile);
75
84
  const before = readHttpLock(lockFile);
76
85
  if (before && alive(before.pid)) {
77
- answer(this, { profile: one.profile ?? null, started: false, running: true, ...serving(one, before, Date.now()) }, [
78
- `Already serving ${one.invocation} over HTTP since ${clock(before.startedAt)} (PID ${before.pid}, port ${before.port}).`,
86
+ answer(this, { ...named(one), started: false, running: true, ...serving(one, before, Date.now()) }, [
87
+ `Already serving ${shown(one)} over HTTP since ${clock(before.startedAt)} (PID ${before.pid}, port ${before.port}).`,
79
88
  ]);
80
89
  return;
81
90
  }
@@ -92,7 +101,7 @@ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT,
92
101
  url.pathname === "/" ? url.origin : url.href,
93
102
  ...(flags.permission ?? []).flatMap((entry) => ["--permission", entry]),
94
103
  ],
95
- env: { ...one.childEnv, [mcpHttpMarker(one.envPrefix)]: one.profile ?? "default" },
104
+ env: { ...one.childEnv, [mcpHttpMarker(one.envPrefix)]: markerValue(one) },
96
105
  };
97
106
  writeSecurely(httpLaunchFile(lockFile), `${JSON.stringify(launch)}\n`, 0o600);
98
107
  const pid = sys.spawn(launch.argv, { ...inherited, ...launch.env }, log);
@@ -100,8 +109,8 @@ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT,
100
109
  const now = readHttpLock(lockFile);
101
110
  if (now?.pid === pid && now.listeningAt) {
102
111
  const code = liveCode(now, Date.now());
103
- answer(this, { profile: one.profile ?? null, started: true, running: true, ...serving(one, now, Date.now()), log }, [
104
- `Started: serving ${one.invocation} over HTTP (PID ${pid}) on 127.0.0.1:${now.port}.`,
112
+ answer(this, { ...named(one), started: true, running: true, ...serving(one, now, Date.now()), log }, [
113
+ `Started: serving ${shown(one)} over HTTP (PID ${pid}) on 127.0.0.1:${now.port}.`,
105
114
  `Browser apps connect to ${connectorUrl(now)}`,
106
115
  ...(code ? [`Login code for a new browser app: ${code.code} (until ${clock(code.expires_at)}).`] : []),
107
116
  ]);
@@ -113,7 +122,7 @@ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT,
113
122
  }
114
123
  await sys.pause();
115
124
  }
116
- throw new CliError("timeout", `mcp serve --http is running but not listening yet — \`${one.invocation} mcp server status\`; its log: ${log}`);
125
+ throw new CliError("timeout", `mcp serve --http is running but not listening yet — \`${again(one, "status")}\`; its log: ${log}`);
117
126
  });
118
127
  destroysNothing(annotate(command.command("stop"), { effects: LOCAL_WRITE, mcp: false, examples: [`${example} mcp server stop`] }))
119
128
  .summary("stop the background HTTP MCP server")
@@ -122,13 +131,13 @@ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT,
122
131
  const one = target(this);
123
132
  const lock = readHttpLock(httpLockFile(one.tokenFile));
124
133
  if (!lock || !alive(lock.pid)) {
125
- answer(this, { profile: one.profile ?? null, stopped: false }, [
126
- `Nothing to stop: ${one.invocation} is not serving MCP over HTTP.`,
134
+ answer(this, { ...named(one), stopped: false }, [
135
+ `Nothing to stop: ${shown(one)} is not serving MCP over HTTP.`,
127
136
  ]);
128
137
  return;
129
138
  }
130
139
  if (byOf(one, lock.pid) !== "server") {
131
- 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`);
140
+ throw new CliError("validation_error", `mcp serve --http for ${shown(one)} (PID ${lock.pid}) was not started by \`${again(one, "start")}\` — stop it where it runs`);
132
141
  }
133
142
  process.kill(lock.pid, "SIGTERM");
134
143
  const sys = machine(this);
@@ -136,8 +145,8 @@ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT,
136
145
  await sys.pause();
137
146
  if (alive(lock.pid))
138
147
  throw new CliError("timeout", `mcp serve --http (PID ${lock.pid}) has not stopped yet`);
139
- answer(this, { profile: one.profile ?? null, stopped: true, pid: lock.pid }, [
140
- `Stopped serving ${one.invocation} over HTTP (PID ${lock.pid}).`,
148
+ answer(this, { ...named(one), stopped: true, pid: lock.pid }, [
149
+ `Stopped serving ${shown(one)} over HTTP (PID ${lock.pid}).`,
141
150
  ]);
142
151
  });
143
152
  annotate(command.command("status"), {
@@ -155,7 +164,7 @@ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT,
155
164
  const clients = signedInClients(one.tokenFile, now);
156
165
  const live = running ? serving(one, lock, now) : undefined;
157
166
  answer(this, {
158
- profile: one.profile ?? null,
167
+ ...named(one),
159
168
  running,
160
169
  ...(live ?? {}),
161
170
  ...(lock && !running ? { stale: { pid: lock.pid, started_at: lock.startedAt } } : {}),
@@ -170,8 +179,8 @@ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT,
170
179
  log: httpLogFile(one.tokenFile),
171
180
  }, [
172
181
  live && lock
173
- ? `Serving ${one.invocation} over HTTP since ${clock(lock.startedAt)} (PID ${lock.pid}) on port ${lock.port}${live.listening ? "" : ", not listening yet"}; browser apps connect to ${live.connector_url}. ${live.by === "server" ? `Started by \`${one.invocation} mcp server start\`.` : "Started by hand — `mcp server stop` leaves it alone."}`
174
- : `Not serving ${one.invocation} over HTTP.`,
182
+ ? `Serving ${shown(one)} over HTTP since ${clock(lock.startedAt)} (PID ${lock.pid}) on port ${lock.port}${live.listening ? "" : ", not listening yet"}; browser apps connect to ${live.connector_url}. ${live.by === "server" ? `Started by \`${again(one, "start")}\`.` : "Started by hand — `mcp server stop` leaves it alone."}`
183
+ : `Not serving ${shown(one)} over HTTP.`,
175
184
  ...(live?.login_code
176
185
  ? [
177
186
  `Login code for a new browser app: ${live.login_code.code} (until ${clock(live.login_code.expires_at)}).`,
@@ -185,5 +194,8 @@ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT,
185
194
  ...clientLines(clients),
186
195
  ]);
187
196
  });
197
+ if (profiles)
198
+ for (const one of command.commands)
199
+ one.option("--profiles <names|all>", "the server of several profiles that `mcp serve --profiles` serves: names, comma-separated, or all");
188
200
  return command;
189
201
  };
@@ -1,8 +1,10 @@
1
1
  import { type ChildProcessWithoutNullStreams } from "node:child_process";
2
2
  import { Command } from "commander";
3
3
  import type { ServerSystem } from "../../background/system.js";
4
+ import type { HttpOptions } from "../../mcp/http/serve.js";
4
5
  import { type AppIdentity } from "../../ports/app.js";
5
- import { type BaseEnvironment } from "../context.js";
6
+ import { type BaseContext, type BaseEnvironment } from "../context.js";
7
+ import { type GlobalFlags, type ResolveOptions, type Settings } from "../settings.js";
6
8
  import { type Messenger } from "./context.js";
7
9
  /** Where the running CLI really lives; a test hands in its own. */
8
10
  export interface McpEnvironment extends BaseEnvironment {
@@ -17,12 +19,64 @@ export interface McpFlags {
17
19
  http?: boolean;
18
20
  port?: string;
19
21
  publicUrl?: string;
22
+ profiles?: string;
20
23
  }
21
- /** The token file of `<tool> mcp serve --http` for every profile: no profile name can start with `_`. */
24
+ /** The token file of `<tool> mcp serve --http --profiles all`: no profile name can start with `_`. */
22
25
  export declare const ALL_PROFILES = "_all";
26
+ /** `--profiles all`; a profile named `all` is served alone, as `<tool> all mcp serve`. */
27
+ export declare const EVERY_PROFILE = "all";
28
+ /** The profiles `--profiles` names: `all`, read again on each session, or a fixed list. */
29
+ export interface ProfileSet {
30
+ /** What `--profiles` takes to name this set again: `all`, or the names sorted, comma-separated. */
31
+ option: string;
32
+ profiles: () => string[];
33
+ /** The token file's name: a list's login never opens the server of other profiles. */
34
+ key: string;
35
+ }
36
+ /**
37
+ * The profile or profiles an MCP server is for, always named (NEED-1881, WC-MCP-9): a leading profile, or
38
+ * `--profiles` with a list or `all`. Neither, or both, is refused with the profiles on this computer. `--profiles`
39
+ * takes the place of `<PREFIX>_PROFILE`; under `<PREFIX>_PROFILE_LOCK` it may name the locked profile only
40
+ * (WC-ACCT-5).
41
+ */
42
+ export declare const servedProfiles: (self: Command, messenger: Pick<Messenger, "app" | "resolveSettings">, { settings, env }: {
43
+ settings: Pick<Settings, "profile" | "sources" | "profilesHere">;
44
+ env: NodeJS.ProcessEnv;
45
+ }, given: string | undefined, words: string) => {
46
+ profile: string;
47
+ } | {
48
+ set: ProfileSet;
49
+ };
23
50
  /** Where `mcp serve --http` keeps the hashes of the tokens it issued — per profile, in the state folder. */
24
51
  export declare const httpTokenFile: (app: AppIdentity, profile: string, env: NodeJS.ProcessEnv) => string;
52
+ /**
53
+ * What the `mcp` group needs of the CLI it serves. A messenger serves through its one connection (`messengerHost`);
54
+ * a CLI that is not a messenger through its own program (`appMcpCommand`). The commands, flags and answers are the
55
+ * same in both, so every WireCat CLI has one `mcp`.
56
+ */
57
+ export interface McpHost {
58
+ app: AppIdentity;
59
+ /** `Telegram`, `Google Calendar`: what the instructions call the service. */
60
+ name: string;
61
+ resolveSettings: (flags: GlobalFlags, options?: ResolveOptions) => Settings;
62
+ context: (self: Command, options?: {
63
+ needsProfile?: boolean;
64
+ }) => BaseContext;
65
+ /** A permission key of this CLI for `--permission`'s help: `messages.send=deny`. */
66
+ permissionExample: string;
67
+ serveStdio: (self: Command, context: BaseContext) => Promise<void>;
68
+ serveHttp: (self: Command, context: BaseContext, http: Omit<HttpOptions, "onCode" | "onError" | "appName">) => Promise<void>;
69
+ /** The instructions of the server of several profiles, whose tools each take `profile`. */
70
+ instructions: (profiles: readonly string[]) => string | Promise<string>;
71
+ /** Whether the profile can reach the service; `undefined` where the CLI cannot tell without connecting. */
72
+ signedIn: (self: Command, profile: string, env: NodeJS.ProcessEnv) => boolean | undefined;
73
+ /** A profile that is a bot, or not signed in here; the server of several profiles leaves bots out. */
74
+ profileState?: Messenger["profileState"];
75
+ }
25
76
  export declare const mcpCommand: (messenger: Messenger) => Command;
77
+ export declare const mcpGroup: (host: McpHost) => Command;
78
+ /** How the startup and status lines name a set, so the owner sees which accounts an agent reaches. */
79
+ export declare const profileSetLabel: (set: ProfileSet) => string;
26
80
  /**
27
81
  * The server's own stderr, so a failed start says why rather than only that it exited. Read as it
28
82
  * comes, which also keeps a chatty server from blocking on a full pipe. Only the end is kept, with the
@@ -42,9 +96,10 @@ export declare const redactTail: (text: string, home: string | undefined) => str
42
96
  * store. So is `XDG_RUNTIME_DIR`: the keyring is reached through it, and an MCP client that starts
43
97
  * servers with a trimmed environment leaves it out (measured 2026-09-28). Credentials are never copied.
44
98
  */
45
- export declare const serverEntry: (app: AppIdentity, { profile, flags, execPath, scriptPath, env, }: {
46
- /** Left out, the entry is `<tool> mcp serve`, the server of every profile. */
99
+ export declare const serverEntry: (app: AppIdentity, { profile, profiles, flags, execPath, scriptPath, env, }: {
100
+ /** One profile, or `profiles`: the `--profiles` of a server of several. */
47
101
  profile?: string;
102
+ profiles?: string;
48
103
  flags?: McpFlags;
49
104
  execPath: string;
50
105
  scriptPath: string;