@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 +49 -0
- package/dist/cli/app-doctor-command.d.ts +107 -0
- package/dist/cli/app-doctor-command.js +101 -0
- package/dist/cli/app-mcp-command.d.ts +23 -0
- package/dist/cli/app-mcp-command.js +49 -0
- package/dist/cli/index.d.ts +4 -2
- package/dist/cli/index.js +2 -1
- package/dist/cli/mcp-server-command.d.ts +10 -2
- package/dist/cli/mcp-server-command.js +28 -16
- package/dist/cli/messenger/mcp-command.d.ts +59 -4
- package/dist/cli/messenger/mcp-command.js +173 -75
- package/dist/cli/shared-command-exports.d.ts +348 -0
- package/dist/cli/shared-command-exports.js +4 -0
- package/dist/cli/steps.js +1 -1
- package/dist/mcp/agent.js +14 -2
- package/dist/mcp/app-server.d.ts +42 -0
- package/dist/mcp/app-server.js +162 -0
- package/dist/mcp/instructions.d.ts +12 -0
- package/dist/mcp/instructions.js +23 -0
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.js +2 -2
- package/package.json +1 -1
- package/parity.json +19 -3
|
@@ -14,50 +14,123 @@ import { grantsAuthority, LOCAL_READ, LOCAL_WRITE, REMOTE_WRITE } from "../effec
|
|
|
14
14
|
import { itemOf, renderAnswer } from "../envelope.js";
|
|
15
15
|
import { MCP_HTTP_DEFAULT_PORT, mcpServerCommand } from "../mcp-server-command.js";
|
|
16
16
|
import { rootOf } from "../profile.js";
|
|
17
|
-
import { namedProfile } from "../settings.js";
|
|
17
|
+
import { NoProfileHere, namedProfile } from "../settings.js";
|
|
18
18
|
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 serve --http
|
|
22
|
+
/** The token file of `<tool> mcp serve --http --profiles all`: no profile name can start with `_`. */
|
|
23
23
|
export const ALL_PROFILES = "_all";
|
|
24
|
+
/** `--profiles all`; a profile named `all` is served alone, as `<tool> all mcp serve`. */
|
|
25
|
+
export const EVERY_PROFILE = "all";
|
|
26
|
+
/**
|
|
27
|
+
* The profile or profiles an MCP server is for, always named (NEED-1881, WC-MCP-9): a leading profile, or
|
|
28
|
+
* `--profiles` with a list or `all`. Neither, or both, is refused with the profiles on this computer. `--profiles`
|
|
29
|
+
* takes the place of `<PREFIX>_PROFILE`; under `<PREFIX>_PROFILE_LOCK` it may name the locked profile only
|
|
30
|
+
* (WC-ACCT-5).
|
|
31
|
+
*/
|
|
32
|
+
export const servedProfiles = (self, messenger, { settings, env }, given, words) => {
|
|
33
|
+
const { app } = messenger;
|
|
34
|
+
const here = settings.profilesHere;
|
|
35
|
+
const listed = `profiles here: ${here.length > 0 ? here.join(", ") : "none"}`;
|
|
36
|
+
const named = namedProfile(settings);
|
|
37
|
+
if (given === undefined && named !== undefined)
|
|
38
|
+
return { profile: named };
|
|
39
|
+
if (given !== undefined && settings.sources.profile === "first word")
|
|
40
|
+
throw new CliError("validation_error", `name one profile or --profiles, not both: \`${app.command} ${named} ${words}\` or \`${app.command} ${words} ` +
|
|
41
|
+
`--profiles ${given}\``, { profiles: here });
|
|
42
|
+
if (given === undefined) {
|
|
43
|
+
if (here.length === 0)
|
|
44
|
+
throw new NoProfileHere(app);
|
|
45
|
+
throw new CliError("validation_error", `name the profiles to serve: \`${app.command} <profile> ${words}\`, or \`${app.command} ${words} --profiles ` +
|
|
46
|
+
`<a,b>\` or \`--profiles all\` — ${listed}`, { profiles: here });
|
|
47
|
+
}
|
|
48
|
+
const names = [...new Set(given.split(",").map((one) => one.trim()))].filter(Boolean).sort();
|
|
49
|
+
const lock = env[envName(app, "PROFILE_LOCK")]?.trim();
|
|
50
|
+
if (lock && names.some((one) => one !== lock))
|
|
51
|
+
throw new CliError("permission_error", `this process is locked to profile ${lock} (${envName(app, "PROFILE_LOCK")}) — --profiles may name only ${lock}`);
|
|
52
|
+
if (lock)
|
|
53
|
+
return { set: { option: lock, profiles: () => [lock], key: `_${lock}` } };
|
|
54
|
+
if (names.length === 1 && names[0] === EVERY_PROFILE) {
|
|
55
|
+
const every = () => messenger.resolveSettings(self.optsWithGlobals(), { env, needsProfile: false, readOnly: true }).profilesHere;
|
|
56
|
+
return { set: { option: EVERY_PROFILE, profiles: every, key: ALL_PROFILES } };
|
|
57
|
+
}
|
|
58
|
+
if (names.length === 0 || names.includes(EVERY_PROFILE))
|
|
59
|
+
throw new CliError("validation_error", `--profiles takes profile names, comma-separated, or \`all\` alone for every profile — a profile named all ` +
|
|
60
|
+
`is served as \`${app.command} all ${words}\`; ${listed}`, { profiles: here });
|
|
61
|
+
const unknown = names.filter((one) => !here.includes(one));
|
|
62
|
+
if (unknown.length > 0)
|
|
63
|
+
throw new CliError("validation_error", `no profile named ${unknown.map((one) => `"${one}"`).join(", ")} — ${listed}`, { profiles: here });
|
|
64
|
+
return { set: { option: names.join(","), profiles: () => names, key: `_${names.join(",")}` } };
|
|
65
|
+
};
|
|
66
|
+
const withProfiles = (command) => command.option("--profiles <names|all>", "serve several profiles from one server instead of one: names, comma-separated, or all for every profile");
|
|
24
67
|
/** Where `mcp serve --http` keeps the hashes of the tokens it issued — per profile, in the state folder. */
|
|
25
68
|
export const httpTokenFile = (app, profile, env) => join(resolvePaths({ appName: app.appName, prefix: app.envPrefix, env }).state, "mcp-http", `${profile}.json`);
|
|
26
|
-
const withFlags = (command) => command.option("--permission <key=level>",
|
|
27
|
-
|
|
28
|
-
|
|
69
|
+
const withFlags = (command, example) => command.option("--permission <key=level>", `make a permission stricter for this server only, like ${example}; it never loosens the profile; repeat for more keys`, (value, previous = []) => [...previous, value]);
|
|
70
|
+
const messengerHost = (messenger) => ({
|
|
71
|
+
app: messenger.app,
|
|
72
|
+
name: messenger.name ?? messenger.app.command,
|
|
73
|
+
resolveSettings: messenger.resolveSettings,
|
|
74
|
+
...(messenger.profileState ? { profileState: messenger.profileState } : {}),
|
|
75
|
+
context: (self, options) => messengerContext(self, messenger, options),
|
|
76
|
+
permissionExample: "messages.send=deny",
|
|
77
|
+
serveStdio: async (self, context) => {
|
|
78
|
+
// Loaded here, not at the top: every other command would otherwise pay for the SDK.
|
|
79
|
+
const { serveOverStdio } = await import("../../mcp/server.js");
|
|
80
|
+
await serveOverStdio(self, context, messenger, {});
|
|
81
|
+
},
|
|
82
|
+
serveHttp: async (self, context, http) => {
|
|
83
|
+
const { serveOverHttpUntilStopped } = await import("../../mcp/server.js");
|
|
84
|
+
await serveOverHttpUntilStopped(self, context, messenger, {}, http);
|
|
85
|
+
},
|
|
86
|
+
instructions: async (profiles) => {
|
|
87
|
+
const { instructions } = await import("../../mcp/instructions.js");
|
|
88
|
+
return instructions({
|
|
89
|
+
command: messenger.app.command,
|
|
90
|
+
name: messenger.name ?? messenger.app.command,
|
|
91
|
+
profile: profiles,
|
|
92
|
+
writes: [],
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
signedIn: (self, profile, env) => (messenger.accountOf?.(self) ?? recalledAccount(messenger.app, messenger.provider, profile, env)) !== undefined,
|
|
96
|
+
});
|
|
97
|
+
export const mcpCommand = (messenger) => mcpGroup(messengerHost(messenger));
|
|
98
|
+
export const mcpGroup = (host) => {
|
|
99
|
+
const { app } = host;
|
|
100
|
+
const flagged = (command) => withFlags(command, host.permissionExample);
|
|
29
101
|
const command = annotate(new Command("mcp"), { effects: LOCAL_READ, mcp: false })
|
|
30
102
|
.summary("serve this profile's commands to an AI agent, and set that up")
|
|
31
103
|
.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(
|
|
104
|
+
command.addCommand(withProfiles(flagged(annotate(new Command("serve"), {
|
|
33
105
|
effects: REMOTE_WRITE,
|
|
34
106
|
lifetime: "persistent",
|
|
35
107
|
mcp: false,
|
|
36
108
|
examples: [
|
|
37
109
|
`${app.command} personal mcp serve`,
|
|
110
|
+
`${app.command} mcp serve --profiles work,personal`,
|
|
38
111
|
`${app.command} personal mcp serve --http --public-url https://laptop.example.ts.net`,
|
|
39
112
|
],
|
|
40
113
|
})
|
|
41
114
|
.summary("serve this profile's commands to an AI agent")
|
|
42
|
-
.description(`serve this profile to an agent over MCP, on stdin and stdout — \`claude mcp add ${app.command} -- ${app.command} <profile> mcp serve\`;
|
|
115
|
+
.description(`serve this profile to an agent over MCP, on stdin and stdout — \`claude mcp add ${app.command} -- ${app.command} <profile> mcp serve\`; \`${app.command} mcp serve --profiles a,b\` or \`--profiles all\` serves those profiles from one server and each tool asks which`)))
|
|
43
116
|
.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")
|
|
44
117
|
.option("--port <port>", `the local port for --http (default ${DEFAULT_PORT})`)
|
|
45
118
|
.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")
|
|
46
119
|
.action(async function () {
|
|
47
120
|
const flags = this.optsWithGlobals();
|
|
48
|
-
const context =
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
51
|
-
return
|
|
121
|
+
const context = host.context(this, { needsProfile: false });
|
|
122
|
+
const served = servedProfiles(this, host, context, flags.profiles, "mcp serve");
|
|
123
|
+
if ("set" in served)
|
|
124
|
+
return serveProfiles(this, context, host, flags, served.set);
|
|
125
|
+
const { profile } = served;
|
|
52
126
|
if (flags.http) {
|
|
53
127
|
const publicUrl = publicUrlOf(flags.publicUrl, {
|
|
54
128
|
missing: "--http",
|
|
55
129
|
reaches: `the browser apps reach ${app.command}`,
|
|
56
130
|
path: true,
|
|
57
131
|
});
|
|
58
|
-
const { serveOverHttpUntilStopped } = await import("../../mcp/server.js");
|
|
59
132
|
const tokenFile = httpTokenFile(app, profile, context.env);
|
|
60
|
-
await
|
|
133
|
+
await host.serveHttp(this, context, {
|
|
61
134
|
publicUrl,
|
|
62
135
|
port: portOf(flags.port, DEFAULT_PORT),
|
|
63
136
|
tokenFile,
|
|
@@ -66,74 +139,84 @@ export const mcpCommand = (messenger) => {
|
|
|
66
139
|
});
|
|
67
140
|
return;
|
|
68
141
|
}
|
|
69
|
-
|
|
70
|
-
const { serveOverStdio } = await import("../../mcp/server.js");
|
|
71
|
-
await serveOverStdio(this, context, messenger, {});
|
|
142
|
+
await host.serveStdio(this, context);
|
|
72
143
|
}));
|
|
73
|
-
command.addCommand(annotate(new Command("revoke"), {
|
|
144
|
+
command.addCommand(withProfiles(annotate(new Command("revoke"), {
|
|
74
145
|
effects: LOCAL_WRITE,
|
|
75
146
|
mcp: false,
|
|
76
|
-
examples: [`${app.command} personal mcp revoke`],
|
|
147
|
+
examples: [`${app.command} personal mcp revoke`, `${app.command} mcp revoke --profiles all`],
|
|
77
148
|
})
|
|
78
149
|
.summary("forget every login given to a browser app")
|
|
79
|
-
.description(
|
|
80
|
-
.
|
|
81
|
-
const
|
|
82
|
-
const profile = namedProfile(context.settings);
|
|
150
|
+
.description("forget every login `mcp serve --http` gave a browser app, of this profile or of the server `--profiles` names; each app must log in again")).action(async function () {
|
|
151
|
+
const context = host.context(this, { needsProfile: false });
|
|
152
|
+
const served = servedProfiles(this, host, context, this.opts().profiles, "mcp revoke");
|
|
83
153
|
const { revokeAll } = await import("../../mcp/http/oauth.js");
|
|
84
|
-
revokeAll(httpTokenFile(app,
|
|
85
|
-
renderAnswer(this, context, itemOf(
|
|
154
|
+
revokeAll(httpTokenFile(app, "set" in served ? served.set.key : served.profile, context.env));
|
|
155
|
+
renderAnswer(this, context, itemOf("set" in served
|
|
156
|
+
? { revoked: true, profile: null, profiles: served.set.option }
|
|
157
|
+
: { revoked: true, profile: served.profile }));
|
|
86
158
|
}));
|
|
87
159
|
command.addCommand(mcpServerCommand({
|
|
88
160
|
example: `${app.command} personal`,
|
|
89
161
|
permission: true,
|
|
162
|
+
profiles: true,
|
|
90
163
|
target: (self) => {
|
|
91
|
-
const
|
|
92
|
-
|
|
164
|
+
const context = host.context(self, { needsProfile: false });
|
|
165
|
+
const { env } = context;
|
|
166
|
+
const served = servedProfiles(self, host, context, self.opts().profiles, `mcp server ${self.name()}`);
|
|
167
|
+
if ("set" in served) {
|
|
168
|
+
const { set } = served;
|
|
93
169
|
return {
|
|
94
170
|
invocation: app.command,
|
|
171
|
+
profiles: { option: set.option, key: set.key, label: profileSetLabel(set) },
|
|
95
172
|
envPrefix: app.envPrefix,
|
|
96
173
|
env,
|
|
97
|
-
tokenFile: httpTokenFile(app,
|
|
98
|
-
argv: ["mcp", "serve", "--http"],
|
|
174
|
+
tokenFile: httpTokenFile(app, set.key, env),
|
|
175
|
+
argv: ["mcp", "serve", "--http", "--profiles", set.option],
|
|
99
176
|
version: app.version,
|
|
100
177
|
};
|
|
178
|
+
}
|
|
179
|
+
const { profile } = served;
|
|
101
180
|
return {
|
|
102
|
-
invocation: `${app.command} ${
|
|
103
|
-
profile
|
|
181
|
+
invocation: `${app.command} ${profile}`,
|
|
182
|
+
profile,
|
|
104
183
|
envPrefix: app.envPrefix,
|
|
105
184
|
env,
|
|
106
|
-
tokenFile: httpTokenFile(app,
|
|
185
|
+
tokenFile: httpTokenFile(app, profile, env),
|
|
107
186
|
argv: ["mcp", "serve", "--http"],
|
|
108
|
-
childEnv: { [`${app.envPrefix}_PROFILE`]:
|
|
187
|
+
childEnv: { [`${app.envPrefix}_PROFILE`]: profile },
|
|
109
188
|
version: app.version,
|
|
110
189
|
};
|
|
111
190
|
},
|
|
112
|
-
answer: (self, item, lines) => renderAnswer(self,
|
|
191
|
+
answer: (self, item, lines) => renderAnswer(self, host.context(self, { needsProfile: false }), itemOf(item), () => lines.join("\n")),
|
|
113
192
|
system: (self) => environmentOf(self).system,
|
|
114
193
|
}));
|
|
115
|
-
command.addCommand(
|
|
194
|
+
command.addCommand(withProfiles(flagged(annotate(new Command("config"), {
|
|
116
195
|
effects: LOCAL_READ,
|
|
117
|
-
examples: [`${app.command} personal mcp config`],
|
|
196
|
+
examples: [`${app.command} personal mcp config`, `${app.command} mcp config --profiles all`],
|
|
118
197
|
})
|
|
119
|
-
.summary("print the local MCP configuration
|
|
120
|
-
.description("print the mcpServers entry for Claude Desktop, Cursor and others, with full paths; writes nothing")).action(async function () {
|
|
198
|
+
.summary("print the local MCP configuration of this profile, or of those --profiles names")
|
|
199
|
+
.description("print the mcpServers entry for Claude Desktop, Cursor and others, with full paths; writes nothing"))).action(async function () {
|
|
121
200
|
const flags = this.optsWithGlobals();
|
|
122
|
-
const context =
|
|
123
|
-
const {
|
|
201
|
+
const context = host.context(this, { needsProfile: false });
|
|
202
|
+
const { env } = context;
|
|
124
203
|
const given = environmentOf(this).mcp ?? {};
|
|
125
|
-
const
|
|
204
|
+
const served = servedProfiles(this, host, context, flags.profiles, "mcp config");
|
|
126
205
|
const entry = serverEntry(app, {
|
|
127
|
-
...(
|
|
206
|
+
...("set" in served ? { profiles: served.set.option } : { profile: served.profile }),
|
|
128
207
|
flags,
|
|
129
208
|
execPath: given.execPath ?? process.execPath,
|
|
130
209
|
scriptPath: given.scriptPath ?? realpathSync(process.argv[1] ?? ""),
|
|
131
210
|
env,
|
|
132
211
|
});
|
|
133
212
|
// Pasted into a file, so a person gets the client's own JSON, indented; a script finds it under `config`.
|
|
134
|
-
renderAnswer(this, context, itemOf({ config: entry.config }, {
|
|
213
|
+
renderAnswer(this, context, itemOf({ config: entry.config }, {
|
|
214
|
+
next: entry.nodeBound
|
|
215
|
+
? [nextStep("mcp.config", { args: { profiles: "set" in served ? served.set.option : undefined } })]
|
|
216
|
+
: [],
|
|
217
|
+
}), () => JSON.stringify(entry.config, null, 2));
|
|
135
218
|
}));
|
|
136
|
-
command.addCommand(
|
|
219
|
+
command.addCommand(flagged(grantsAuthority(annotate(new Command("add"), {
|
|
137
220
|
effects: LOCAL_WRITE,
|
|
138
221
|
examples: [`${app.command} personal mcp add codex --allow-writes`],
|
|
139
222
|
}))
|
|
@@ -142,7 +225,7 @@ export const mcpCommand = (messenger) => {
|
|
|
142
225
|
.option("--allow-writes", "acknowledge that this profile offers writing tools")).action(async function (client) {
|
|
143
226
|
if (client !== "codex" && client !== "claude-code")
|
|
144
227
|
throw new CliError("validation_error", "choose codex or claude-code");
|
|
145
|
-
const context =
|
|
228
|
+
const context = host.context(this);
|
|
146
229
|
const { settings, env } = context;
|
|
147
230
|
const given = environmentOf(this).mcp ?? {};
|
|
148
231
|
const configuration = serverEntry(app, {
|
|
@@ -167,11 +250,11 @@ export const mcpCommand = (messenger) => {
|
|
|
167
250
|
throw new CliError("validation_error", error instanceof Error ? error.message : "MCP setup failed");
|
|
168
251
|
}
|
|
169
252
|
}));
|
|
170
|
-
command.addCommand(
|
|
253
|
+
command.addCommand(flagged(annotate(new Command("check"), {
|
|
171
254
|
effects: LOCAL_READ,
|
|
172
255
|
examples: [`${app.command} personal mcp check`],
|
|
173
256
|
}).description("check this profile's local MCP handshake and tool list")).action(async function () {
|
|
174
|
-
const context =
|
|
257
|
+
const context = host.context(this);
|
|
175
258
|
const { settings, env } = context;
|
|
176
259
|
const given = environmentOf(this).mcp ?? {};
|
|
177
260
|
const configuration = serverEntry(app, {
|
|
@@ -190,8 +273,8 @@ export const mcpCommand = (messenger) => {
|
|
|
190
273
|
const { tools, potentialWrites } = await probeStdio(entry, {
|
|
191
274
|
start: stderr.start,
|
|
192
275
|
});
|
|
193
|
-
const
|
|
194
|
-
if (
|
|
276
|
+
const signedIn = host.signedIn(this, settings.profile, env);
|
|
277
|
+
if (signedIn === false) {
|
|
195
278
|
const setup = rootOf(this).commands.some((one) => one.name() === "setup")
|
|
196
279
|
? ` — log in with \`${app.command} ${settings.profile} setup\``
|
|
197
280
|
: "";
|
|
@@ -199,9 +282,9 @@ export const mcpCommand = (messenger) => {
|
|
|
199
282
|
`tools will fail${setup}`);
|
|
200
283
|
}
|
|
201
284
|
renderAnswer(this, context, itemOf({
|
|
202
|
-
healthy:
|
|
285
|
+
healthy: signedIn !== false,
|
|
203
286
|
profile: settings.profile,
|
|
204
|
-
signed_in:
|
|
287
|
+
signed_in: signedIn ?? null,
|
|
205
288
|
tools: tools.length,
|
|
206
289
|
potential_writes: potentialWrites.length,
|
|
207
290
|
}));
|
|
@@ -213,42 +296,44 @@ export const mcpCommand = (messenger) => {
|
|
|
213
296
|
}));
|
|
214
297
|
return command;
|
|
215
298
|
};
|
|
299
|
+
/** How the startup and status lines name a set, so the owner sees which accounts an agent reaches. */
|
|
300
|
+
export const profileSetLabel = (set) => {
|
|
301
|
+
const names = set.profiles().join(", ");
|
|
302
|
+
if (set.option === EVERY_PROFILE)
|
|
303
|
+
return `every profile (${names || "none yet"})`;
|
|
304
|
+
return set.profiles().length === 1 ? `profile ${names}` : `profiles ${names}`;
|
|
305
|
+
};
|
|
216
306
|
/**
|
|
217
|
-
* `<tool> mcp serve
|
|
218
|
-
* `<tool> <profile> mcp serve`
|
|
219
|
-
*
|
|
307
|
+
* `<tool> mcp serve --profiles <names|all>`: those profiles from one server, each call in that profile's own
|
|
308
|
+
* `<tool> <profile> mcp serve` (`src/mcp/all-profiles.ts`). Nothing picks a profile: each tool takes one, and the
|
|
309
|
+
* agent is told to ask.
|
|
220
310
|
*/
|
|
221
|
-
const
|
|
222
|
-
const { app } =
|
|
311
|
+
const serveProfiles = async (self, context, host, flags, set) => {
|
|
312
|
+
const { app } = host;
|
|
223
313
|
const { env } = context;
|
|
224
|
-
const tokenFile = httpTokenFile(app,
|
|
225
|
-
const state = (profile) =>
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
314
|
+
const tokenFile = httpTokenFile(app, set.key, env);
|
|
315
|
+
const state = (profile) => host.profileState?.(profile, env) ?? "signed-in";
|
|
316
|
+
const bots = set.option === EVERY_PROFILE ? [] : set.profiles().filter((profile) => state(profile) === "bot");
|
|
317
|
+
if (bots.length > 0)
|
|
318
|
+
throw new CliError("validation_error", `${bots.join(", ")} ${bots.length === 1 ? "is a bot profile" : "are bot profiles"}, served by \`${app.command} <profile> bot mcp serve\``);
|
|
319
|
+
// `all` reads the profiles again for each session, so one set up while the server runs appears.
|
|
320
|
+
const profiles = () => set.profiles().filter((profile) => state(profile) !== "bot");
|
|
230
321
|
if (profiles().length === 0)
|
|
231
|
-
throw new CliError("configuration_error", `no ${app.command} profile on this computer yet — set one up, then run \`${app.command} mcp serve\` again`);
|
|
322
|
+
throw new CliError("configuration_error", `no ${app.command} profile on this computer yet — set one up, then run \`${app.command} mcp serve --profiles all\` again`);
|
|
232
323
|
const given = environmentOf(self).mcp ?? {};
|
|
233
324
|
const execPath = given.execPath ?? process.execPath;
|
|
234
325
|
const scriptPath = given.scriptPath ?? realpathSync(process.argv[1] ?? "");
|
|
235
326
|
const tightened = (flags.permission ?? []).flatMap((entry) => ["--permission", entry]);
|
|
236
|
-
const { instructions } = await import("../../mcp/instructions.js");
|
|
237
327
|
const options = {
|
|
238
328
|
command: app.command,
|
|
239
329
|
version: app.version,
|
|
240
|
-
name:
|
|
330
|
+
name: host.name,
|
|
241
331
|
profiles,
|
|
242
332
|
signedIn: (profile) => state(profile) === "signed-in",
|
|
243
333
|
signIn: (profile) => `profile ${profile} is not signed in — run \`${app.command} ${profile} setup\`, or sign in from WireCat Desktop, ` +
|
|
244
334
|
"then try again. If the owner did not choose this profile, ask which one.",
|
|
245
335
|
child: (profile) => ({ command: execPath, args: [scriptPath, profile, "mcp", "serve", ...tightened], env }),
|
|
246
|
-
instructions: instructions(
|
|
247
|
-
command: app.command,
|
|
248
|
-
name: messenger.name ?? app.command,
|
|
249
|
-
profile: profiles(),
|
|
250
|
-
writes: [],
|
|
251
|
-
}),
|
|
336
|
+
instructions: await host.instructions(profiles()),
|
|
252
337
|
note: (line) => context.renderer.note(line),
|
|
253
338
|
};
|
|
254
339
|
if (flags.http) {
|
|
@@ -268,13 +353,13 @@ const serveEveryProfile = async (self, context, messenger, flags) => {
|
|
|
268
353
|
version: app.version,
|
|
269
354
|
onCode: (code, expires) => context.renderer.note(env[mcpHttpMarker(app.envPrefix)] === undefined
|
|
270
355
|
? `login code for a new browser app: ${code} (until ${expires.toTimeString().slice(0, 5)}; a new one after each login)`
|
|
271
|
-
: `a new login code for a browser app: \`${app.command} mcp server status\``),
|
|
356
|
+
: `a new login code for a browser app: \`${app.command} mcp server status --profiles ${set.option}\``),
|
|
272
357
|
}).catch((error) => {
|
|
273
358
|
if (error.code === "EADDRINUSE")
|
|
274
359
|
throw new CliError("configuration_error", `port ${port} is in use — pass another with --port`);
|
|
275
360
|
throw error;
|
|
276
361
|
});
|
|
277
|
-
context.renderer.note(`serving
|
|
362
|
+
context.renderer.note(`serving ${profileSetLabel(set)} on ${listening.url.href} — point your tunnel at it; connectors use ${mcpUrlOf(publicUrl).href}`);
|
|
278
363
|
await new Promise((resolve) => {
|
|
279
364
|
process.once("SIGINT", resolve);
|
|
280
365
|
process.once("SIGTERM", resolve);
|
|
@@ -283,6 +368,7 @@ const serveEveryProfile = async (self, context, messenger, flags) => {
|
|
|
283
368
|
return;
|
|
284
369
|
}
|
|
285
370
|
const { serveAllProfilesOverStdio } = await import("../../mcp/all-profiles.js");
|
|
371
|
+
context.renderer.note(`serving ${profileSetLabel(set)} over MCP on stdin and stdout`);
|
|
286
372
|
const { stdin, stdout, signal } = environmentOf(self);
|
|
287
373
|
await serveAllProfilesOverStdio(options, {
|
|
288
374
|
...(stdin ? { stdin: stdin } : {}),
|
|
@@ -331,6 +417,14 @@ export const redactTail = (text, home) => {
|
|
|
331
417
|
const lines = out.split("\n").filter((line) => line.trim() !== "");
|
|
332
418
|
return lines.slice(-TAIL_LINES).join("\n").slice(-TAIL_BYTES);
|
|
333
419
|
};
|
|
420
|
+
/** `tg-work`; `tg` for every profile; `tg-personal-work` for a list, a name every client accepts. */
|
|
421
|
+
const serverName = (app, profile, profiles) => {
|
|
422
|
+
if (profile !== undefined)
|
|
423
|
+
return `${app.command}-${profile}`;
|
|
424
|
+
if (profiles === EVERY_PROFILE)
|
|
425
|
+
return app.command;
|
|
426
|
+
return `${app.command}-${(profiles ?? "").split(",").join("-")}`;
|
|
427
|
+
};
|
|
334
428
|
const VERSION_MANAGER = /[\\/](\.nvm|nvm|\.fnm|fnm|fnm_multishells|\.volta|volta|\.asdf|mise)[\\/]/i;
|
|
335
429
|
/**
|
|
336
430
|
* `node` and the script by full path, on every platform: a client started from the desktop does
|
|
@@ -340,10 +434,13 @@ const VERSION_MANAGER = /[\\/](\.nvm|nvm|\.fnm|fnm|fnm_multishells|\.volta|volta
|
|
|
340
434
|
* store. So is `XDG_RUNTIME_DIR`: the keyring is reached through it, and an MCP client that starts
|
|
341
435
|
* servers with a trimmed environment leaves it out (measured 2026-09-28). Credentials are never copied.
|
|
342
436
|
*/
|
|
343
|
-
export const serverEntry = (app, { profile, flags = {}, execPath, scriptPath, env, }) => {
|
|
437
|
+
export const serverEntry = (app, { profile, profiles, flags = {}, execPath, scriptPath, env, }) => {
|
|
438
|
+
if ((profile === undefined) === (profiles === undefined))
|
|
439
|
+
throw new CliError("validation_error", "an MCP entry serves one profile or the profiles --profiles names");
|
|
440
|
+
const words = profile === undefined ? `mcp config --profiles ${profiles}` : `${profile} mcp config`;
|
|
344
441
|
if (installerOf(scriptPath) === "npx") {
|
|
345
442
|
throw new CliError("validation_error", `this ${app.command} runs from npx's cache, which gets cleared, and the path would stop working — ` +
|
|
346
|
-
`install it globally, then run \`${app.command} ${
|
|
443
|
+
`install it globally, then run \`${app.command} ${words}\` again`);
|
|
347
444
|
}
|
|
348
445
|
const names = [
|
|
349
446
|
...["CONFIG_DIR", "STATE_DIR", "CACHE_DIR"].map((name) => envName(app, name)),
|
|
@@ -360,12 +457,13 @@ export const serverEntry = (app, { profile, flags = {}, execPath, scriptPath, en
|
|
|
360
457
|
...(profile === undefined ? [] : [profile]),
|
|
361
458
|
"mcp",
|
|
362
459
|
"serve",
|
|
460
|
+
...(profiles === undefined ? [] : ["--profiles", profiles]),
|
|
363
461
|
...(flags.permission ?? []).flatMap((entry) => ["--permission", entry]),
|
|
364
462
|
],
|
|
365
463
|
...(Object.keys(directories).length > 0 ? { env: directories } : {}),
|
|
366
464
|
};
|
|
367
465
|
return {
|
|
368
|
-
config: { mcpServers: { [
|
|
466
|
+
config: { mcpServers: { [serverName(app, profile, profiles)]: server } },
|
|
369
467
|
/** The path belongs to one Node version of a version manager; switching Node breaks the entry. */
|
|
370
468
|
nodeBound: VERSION_MANAGER.test(execPath),
|
|
371
469
|
};
|