@wirecat/cli-core 0.444.0 → 0.446.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 +24 -1
- package/dist/cli/guide.js +3 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/mcp-server-command.d.ts +35 -0
- package/dist/cli/mcp-server-command.js +174 -0
- package/dist/cli/messenger/mcp-command.d.ts +2 -0
- package/dist/cli/messenger/mcp-command.js +25 -2
- package/dist/mcp/http/oauth.d.ts +2 -1
- package/dist/mcp/http/oauth.js +12 -7
- package/dist/mcp/http/serve.d.ts +6 -0
- package/dist/mcp/http/serve.js +40 -6
- package/dist/mcp/http/state.d.ts +52 -0
- package/dist/mcp/http/state.js +49 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/server.js +6 -1
- package/package.json +1 -1
- package/parity.json +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,30 @@ 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
|
-
##
|
|
8
|
+
## 0.446.0 — 26.10.2026
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`checkGuide` accepts a step that runs a group with an action of its own**, such as `mcp` now that it has
|
|
13
|
+
`mcp server`; before, it asked for one of the group's subcommands.
|
|
14
|
+
- **The `mcp server` group states its effects** (reads locally), as a CLI that requires effects on every command
|
|
15
|
+
expects.
|
|
16
|
+
|
|
17
|
+
## 0.445.0 — 26.10.2026
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **`mcp server start|stop|status` runs the HTTP MCP server in the background** (WireCat Desktop step 3). `start`
|
|
22
|
+
takes `--public-url`, `--port` and `--permission` as `mcp --http` does and answers once it listens; `stop` ends
|
|
23
|
+
only a server `start` started; `status --json` gives the port, the connector address, the login code for a new
|
|
24
|
+
browser app with when it runs out, and the apps logged in, by the name each gave itself. `mcp --http` now keeps a
|
|
25
|
+
lock beside its token file, so a second one for the same profile is refused. A CLI with its own `mcp` mounts
|
|
26
|
+
`mcpServerCommand` and passes `lockFile: httpLockFile(tokenFile)` to `serveOverHttp`.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **The login code is renewed when it runs out**, not only when someone types a wrong or late one, and the login
|
|
31
|
+
page says it is shown in WireCat Desktop or the terminal.
|
|
9
32
|
|
|
10
33
|
## 0.444.0 — 26.10.2026
|
|
11
34
|
|
package/dist/cli/guide.js
CHANGED
|
@@ -291,7 +291,9 @@ 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
|
-
|
|
294
|
+
// A group with an action of its own runs as typed: `mcp` serves, `mcp server start` is its child.
|
|
295
|
+
const runsItself = at._actionHandler != null;
|
|
296
|
+
if (at.commands.length > 0 && !runsItself)
|
|
295
297
|
return [`needs one of: ${at.commands.map((one) => one.name()).join(", ")}`];
|
|
296
298
|
for (const command of chain)
|
|
297
299
|
silent(command);
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -76,6 +76,7 @@ export { type HelpSection, inSection, SECTIONS } from "./help/sections.js";
|
|
|
76
76
|
export { bufferedInput, type InputPolicy, inputPolicy, MAX_BUFFERED_INPUT, MAX_SECRET_INPUT, provideInputPolicy, } from "./input-policy.js";
|
|
77
77
|
export { answerFields, checkJson, type EntityFields, entityFields, type JsonFieldsResolver, type JsonRequest, liftJson, } from "./json-fields.js";
|
|
78
78
|
export { commandPath, LIST_QUERY_GUIDE, listQuery, nativeField, nativeTerms, pagedList, pageOf, type QueryWords, queryArgument, queryHelp, queryTerms, searchText, termField, timeBounds, typedQuery, unfilteredQuery, withQuery, } from "./list-query.js";
|
|
79
|
+
export { MCP_HTTP_DEFAULT_PORT, type McpServerOptions, type McpServerTarget, mcpServerCommand, } from "./mcp-server-command.js";
|
|
79
80
|
export { formatMeetingBatch, prepareMeetingBatch } from "./meeting-batch.js";
|
|
80
81
|
export { meetingEvidenceItem, meetingPersonContextItem } from "./meeting-evidence-answer.js";
|
|
81
82
|
export { parseEvidenceFindingsJson, parseMeetingFindingsJson } from "./meeting-findings-input.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -69,6 +69,7 @@ export { inSection, SECTIONS } from "./help/sections.js";
|
|
|
69
69
|
export { bufferedInput, inputPolicy, MAX_BUFFERED_INPUT, MAX_SECRET_INPUT, provideInputPolicy, } from "./input-policy.js";
|
|
70
70
|
export { answerFields, checkJson, entityFields, liftJson, } from "./json-fields.js";
|
|
71
71
|
export { commandPath, LIST_QUERY_GUIDE, listQuery, nativeField, nativeTerms, pagedList, pageOf, queryArgument, queryHelp, queryTerms, searchText, termField, timeBounds, typedQuery, unfilteredQuery, withQuery, } from "./list-query.js";
|
|
72
|
+
export { MCP_HTTP_DEFAULT_PORT, mcpServerCommand, } from "./mcp-server-command.js";
|
|
72
73
|
export { formatMeetingBatch, prepareMeetingBatch } from "./meeting-batch.js";
|
|
73
74
|
export { meetingEvidenceItem, meetingPersonContextItem } from "./meeting-evidence-answer.js";
|
|
74
75
|
export { parseEvidenceFindingsJson, parseMeetingFindingsJson } from "./meeting-findings-input.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { type ServerSystem } from "../background/system.js";
|
|
3
|
+
export declare const MCP_HTTP_DEFAULT_PORT = 8765;
|
|
4
|
+
/** What one CLI tells `mcp server` about the server it would run. */
|
|
5
|
+
export interface McpServerTarget {
|
|
6
|
+
/** What a person types before `mcp`: `tg personal`, `memo`. */
|
|
7
|
+
invocation: string;
|
|
8
|
+
/** Absent for a CLI without profiles. */
|
|
9
|
+
profile?: string;
|
|
10
|
+
envPrefix: string;
|
|
11
|
+
env: NodeJS.ProcessEnv;
|
|
12
|
+
tokenFile: string;
|
|
13
|
+
/** The words after the CLI's own entry that run `mcp --http` in the foreground, before `--port`. */
|
|
14
|
+
argv: string[];
|
|
15
|
+
/** Added to the server's environment: the profile, for a CLI that reads it from there. */
|
|
16
|
+
childEnv?: Record<string, string>;
|
|
17
|
+
version: string;
|
|
18
|
+
}
|
|
19
|
+
export interface McpServerOptions {
|
|
20
|
+
/** How help shows the CLI before `mcp`: `tg personal`. */
|
|
21
|
+
example: string;
|
|
22
|
+
/** The CLI's own `mcp --http` default, when it is not 8765. */
|
|
23
|
+
defaultPort?: number;
|
|
24
|
+
target: (self: Command) => McpServerTarget;
|
|
25
|
+
/** The CLI's own answer: the item in `--json`, else the lines. */
|
|
26
|
+
answer: (self: Command, item: object, lines: string[]) => void;
|
|
27
|
+
system?: (self: Command) => ServerSystem | undefined;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* `mcp --http` in the background: `start`, `stop`, `status`, as `server` does for `serve`. The server
|
|
31
|
+
* keeps a lock beside its token file (`state.ts`) with its port, address and login code, so status
|
|
32
|
+
* survives the shell that started it, and `stop` ends only a process that carries this CLI's marker.
|
|
33
|
+
* None of it is offered over MCP: status holds a live login code.
|
|
34
|
+
*/
|
|
35
|
+
export declare const mcpServerCommand: ({ example, defaultPort, target, answer, system, }: McpServerOptions) => Command;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { annotate } from "@wirecat/cli-base/commands";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { alive, carries } from "../background/processes.js";
|
|
5
|
+
import { thisMachine } from "../background/system.js";
|
|
6
|
+
import { readableLog, tail } from "../background/units.js";
|
|
7
|
+
import { portOf, publicUrlOf } from "../http/public-url.js";
|
|
8
|
+
import { httpLockFile, httpLogFile, mcpHttpMarker, readHttpLock, signedInClients, } from "../mcp/http/state.js";
|
|
9
|
+
import { destroysNothing, LOCAL_READ, LOCAL_WRITE } from "./effects.js";
|
|
10
|
+
export const MCP_HTTP_DEFAULT_PORT = 8765;
|
|
11
|
+
const LOOKS = 120;
|
|
12
|
+
const clock = (iso) => {
|
|
13
|
+
const when = new Date(iso);
|
|
14
|
+
const time = when.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
|
15
|
+
return when.toDateString() === new Date().toDateString() ? time : `${when.toLocaleDateString("sv-SE")} ${time}`;
|
|
16
|
+
};
|
|
17
|
+
const connectorUrl = (lock) => new URL("/mcp", lock.publicUrl).href;
|
|
18
|
+
const liveCode = (lock, now) => lock.loginCode && lock.loginCodeExpiresAt && Date.parse(lock.loginCodeExpiresAt) > now
|
|
19
|
+
? { code: lock.loginCode, expires_at: lock.loginCodeExpiresAt }
|
|
20
|
+
: null;
|
|
21
|
+
const markerOf = (target) => `${mcpHttpMarker(target.envPrefix)}=${target.profile ?? "default"}`;
|
|
22
|
+
/** Who started it decides whether `stop` may end it: only a process that carries our marker. */
|
|
23
|
+
const byOf = (target, pid) => (carries(pid, markerOf(target)) ? "server" : "hand");
|
|
24
|
+
const serving = (target, lock, now) => ({
|
|
25
|
+
pid: lock.pid,
|
|
26
|
+
started_at: lock.startedAt,
|
|
27
|
+
listening: lock.listeningAt !== undefined,
|
|
28
|
+
by: byOf(target, lock.pid),
|
|
29
|
+
port: lock.port,
|
|
30
|
+
public_url: lock.publicUrl,
|
|
31
|
+
connector_url: connectorUrl(lock),
|
|
32
|
+
login_code: liveCode(lock, now),
|
|
33
|
+
...(lock.version ? { version: lock.version, outdated: lock.version !== target.version } : {}),
|
|
34
|
+
});
|
|
35
|
+
const clientLines = (clients) => clients.length === 0
|
|
36
|
+
? ["No browser app is logged in."]
|
|
37
|
+
: [
|
|
38
|
+
`Logged-in apps (names are what each app calls itself): ${clients
|
|
39
|
+
.map((one) => `${one.claimedName === null ? "no name" : JSON.stringify(one.claimedName)} (${clock(one.lastLoginAt)})`)
|
|
40
|
+
.join(", ")}.`,
|
|
41
|
+
];
|
|
42
|
+
/**
|
|
43
|
+
* `mcp --http` in the background: `start`, `stop`, `status`, as `server` does for `serve`. The server
|
|
44
|
+
* keeps a lock beside its token file (`state.ts`) with its port, address and login code, so status
|
|
45
|
+
* survives the shell that started it, and `stop` ends only a process that carries this CLI's marker.
|
|
46
|
+
* None of it is offered over MCP: status holds a live login code.
|
|
47
|
+
*/
|
|
48
|
+
export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT, target, answer, system, }) => {
|
|
49
|
+
const command = annotate(new Command("server"), { effects: LOCAL_READ, mcp: false })
|
|
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");
|
|
52
|
+
const machine = (self) => system?.(self) ?? thisMachine();
|
|
53
|
+
destroysNothing(annotate(command.command("start"), {
|
|
54
|
+
effects: LOCAL_WRITE,
|
|
55
|
+
mcp: false,
|
|
56
|
+
examples: [`${example} mcp server start --public-url https://laptop.example.ts.net`],
|
|
57
|
+
}))
|
|
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")
|
|
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")
|
|
62
|
+
.option("--permission <key=level>", "make a permission stricter for this server only; repeat for more keys", (value, previous = []) => [...previous, value])
|
|
63
|
+
.action(async function () {
|
|
64
|
+
// A CLI whose `mcp` takes the same flags reads them first, so they are looked up on both.
|
|
65
|
+
const flags = this.optsWithGlobals();
|
|
66
|
+
const one = target(this);
|
|
67
|
+
const url = publicUrlOf(flags.publicUrl, { missing: "mcp server start", reaches: "the browser apps reach it" });
|
|
68
|
+
const port = portOf(flags.port, defaultPort);
|
|
69
|
+
const lockFile = httpLockFile(one.tokenFile);
|
|
70
|
+
const before = readHttpLock(lockFile);
|
|
71
|
+
if (before && alive(before.pid)) {
|
|
72
|
+
answer(this, { profile: one.profile ?? null, started: false, running: true, ...serving(one, before, Date.now()) }, [
|
|
73
|
+
`Already serving ${one.invocation} over HTTP since ${clock(before.startedAt)} (PID ${before.pid}, port ${before.port}).`,
|
|
74
|
+
]);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const sys = machine(this);
|
|
78
|
+
const log = httpLogFile(one.tokenFile);
|
|
79
|
+
const { [`${one.envPrefix}_TIMEOUT`]: _timeout, ...inherited } = one.env;
|
|
80
|
+
const pid = sys.spawn([
|
|
81
|
+
...one.argv,
|
|
82
|
+
"--port",
|
|
83
|
+
String(port),
|
|
84
|
+
"--public-url",
|
|
85
|
+
url.origin,
|
|
86
|
+
...(flags.permission ?? []).flatMap((entry) => ["--permission", entry]),
|
|
87
|
+
], { ...inherited, ...one.childEnv, [mcpHttpMarker(one.envPrefix)]: one.profile ?? "default" }, log);
|
|
88
|
+
for (let look = 0; look < LOOKS; look++) {
|
|
89
|
+
const now = readHttpLock(lockFile);
|
|
90
|
+
if (now?.pid === pid && now.listeningAt) {
|
|
91
|
+
const code = liveCode(now, Date.now());
|
|
92
|
+
answer(this, { profile: one.profile ?? null, started: true, running: true, ...serving(one, now, Date.now()), log }, [
|
|
93
|
+
`Started: serving ${one.invocation} over HTTP (PID ${pid}) on 127.0.0.1:${now.port}.`,
|
|
94
|
+
`Browser apps connect to ${connectorUrl(now)}`,
|
|
95
|
+
...(code ? [`Login code for a new browser app: ${code.code} (until ${clock(code.expires_at)}).`] : []),
|
|
96
|
+
]);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (!alive(pid)) {
|
|
100
|
+
const last = readableLog(tail(log, 5)).trim();
|
|
101
|
+
throw new CliError("provider_unavailable", `mcp --http stopped before it listened${last ? `:\n${last}` : ""}`);
|
|
102
|
+
}
|
|
103
|
+
await sys.pause();
|
|
104
|
+
}
|
|
105
|
+
throw new CliError("timeout", `mcp --http is running but not listening yet — \`${one.invocation} mcp server status\`; its log: ${log}`);
|
|
106
|
+
});
|
|
107
|
+
destroysNothing(annotate(command.command("stop"), { effects: LOCAL_WRITE, mcp: false, examples: [`${example} mcp server stop`] }))
|
|
108
|
+
.summary("stop the background HTTP MCP server")
|
|
109
|
+
.description("stop `mcp --http` — only one that `mcp server start` started")
|
|
110
|
+
.action(async function () {
|
|
111
|
+
const one = target(this);
|
|
112
|
+
const lock = readHttpLock(httpLockFile(one.tokenFile));
|
|
113
|
+
if (!lock || !alive(lock.pid)) {
|
|
114
|
+
answer(this, { profile: one.profile ?? null, stopped: false }, [
|
|
115
|
+
`Nothing to stop: ${one.invocation} is not serving MCP over HTTP.`,
|
|
116
|
+
]);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (byOf(one, lock.pid) !== "server") {
|
|
120
|
+
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`);
|
|
121
|
+
}
|
|
122
|
+
process.kill(lock.pid, "SIGTERM");
|
|
123
|
+
const sys = machine(this);
|
|
124
|
+
for (let look = 0; look < LOOKS && alive(lock.pid); look++)
|
|
125
|
+
await sys.pause();
|
|
126
|
+
if (alive(lock.pid))
|
|
127
|
+
throw new CliError("timeout", `mcp --http (PID ${lock.pid}) has not stopped yet`);
|
|
128
|
+
answer(this, { profile: one.profile ?? null, stopped: true, pid: lock.pid }, [
|
|
129
|
+
`Stopped serving ${one.invocation} over HTTP (PID ${lock.pid}).`,
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
annotate(command.command("status"), {
|
|
133
|
+
effects: LOCAL_READ,
|
|
134
|
+
mcp: false,
|
|
135
|
+
examples: [`${example} mcp server status --json`],
|
|
136
|
+
})
|
|
137
|
+
.summary("whether the HTTP MCP server runs, its login code and the apps logged in")
|
|
138
|
+
.description("whether `mcp --http` runs, on which port and address, the login code for a new browser app, and the apps logged in")
|
|
139
|
+
.action(function () {
|
|
140
|
+
const one = target(this);
|
|
141
|
+
const lock = readHttpLock(httpLockFile(one.tokenFile));
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
const running = lock !== undefined && alive(lock.pid);
|
|
144
|
+
const clients = signedInClients(one.tokenFile, now);
|
|
145
|
+
const live = running ? serving(one, lock, now) : undefined;
|
|
146
|
+
answer(this, {
|
|
147
|
+
profile: one.profile ?? null,
|
|
148
|
+
running,
|
|
149
|
+
...(live ?? {}),
|
|
150
|
+
...(lock && !running ? { stale: { pid: lock.pid, started_at: lock.startedAt } } : {}),
|
|
151
|
+
clients: clients.map((client) => ({
|
|
152
|
+
claimed_name: client.claimedName,
|
|
153
|
+
last_login_at: client.lastLoginAt,
|
|
154
|
+
})),
|
|
155
|
+
log: httpLogFile(one.tokenFile),
|
|
156
|
+
}, [
|
|
157
|
+
live && lock
|
|
158
|
+
? `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."}`
|
|
159
|
+
: `Not serving ${one.invocation} over HTTP.`,
|
|
160
|
+
...(live?.login_code
|
|
161
|
+
? [
|
|
162
|
+
`Login code for a new browser app: ${live.login_code.code} (until ${clock(live.login_code.expires_at)}).`,
|
|
163
|
+
]
|
|
164
|
+
: []),
|
|
165
|
+
...(lock && !running
|
|
166
|
+
? [
|
|
167
|
+
`A server that started ${clock(lock.startedAt)} (PID ${lock.pid}) is gone; the next start takes its place.`,
|
|
168
|
+
]
|
|
169
|
+
: []),
|
|
170
|
+
...clientLines(clients),
|
|
171
|
+
]);
|
|
172
|
+
});
|
|
173
|
+
return command;
|
|
174
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
+
import type { ServerSystem } from "../../background/system.js";
|
|
3
4
|
import { type AppIdentity } from "../../ports/app.js";
|
|
4
5
|
import { type BaseEnvironment } from "../context.js";
|
|
5
6
|
import { type Messenger } from "./context.js";
|
|
@@ -9,6 +10,7 @@ export interface McpEnvironment extends BaseEnvironment {
|
|
|
9
10
|
scriptPath?: string;
|
|
10
11
|
execPath?: string;
|
|
11
12
|
};
|
|
13
|
+
system?: ServerSystem;
|
|
12
14
|
}
|
|
13
15
|
export interface McpFlags {
|
|
14
16
|
permission?: string[];
|
|
@@ -7,15 +7,17 @@ import { installerOf } from "@wirecat/cli-base/update";
|
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import { ATTACHMENT_LIMIT_ENV } from "../../attachments/limits.js";
|
|
9
9
|
import { portOf, publicUrlOf } from "../../http/public-url.js";
|
|
10
|
+
import { httpLockFile } from "../../mcp/http/state.js";
|
|
10
11
|
import { envName } from "../../ports/app.js";
|
|
11
12
|
import { environmentOf } from "../context.js";
|
|
12
13
|
import { grantsAuthority, LOCAL_READ, LOCAL_WRITE, REMOTE_WRITE } from "../effects.js";
|
|
13
14
|
import { itemOf, renderAnswer } from "../envelope.js";
|
|
15
|
+
import { MCP_HTTP_DEFAULT_PORT, mcpServerCommand } from "../mcp-server-command.js";
|
|
14
16
|
import { rootOf } from "../profile.js";
|
|
15
17
|
import { nextStep } from "../steps.js";
|
|
16
18
|
import { recalledAccount } from "./accounts.js";
|
|
17
19
|
import { messengerContext } from "./context.js";
|
|
18
|
-
const DEFAULT_PORT =
|
|
20
|
+
const DEFAULT_PORT = MCP_HTTP_DEFAULT_PORT;
|
|
19
21
|
/** Where `mcp --http` keeps the hashes of the tokens it issued — per profile, in the state folder. */
|
|
20
22
|
export const httpTokenFile = (app, profile, env) => join(resolvePaths({ appName: app.appName, prefix: app.envPrefix, env }).state, "mcp-http", `${profile}.json`);
|
|
21
23
|
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]);
|
|
@@ -43,10 +45,13 @@ export const mcpCommand = (messenger) => {
|
|
|
43
45
|
reaches: `the browser apps reach ${app.command}`,
|
|
44
46
|
});
|
|
45
47
|
const { serveOverHttpUntilStopped } = await import("../../mcp/server.js");
|
|
48
|
+
const tokenFile = httpTokenFile(app, context.settings.profile, context.env);
|
|
46
49
|
await serveOverHttpUntilStopped(this, context, messenger, {}, {
|
|
47
50
|
publicUrl,
|
|
48
51
|
port: portOf(flags.port, DEFAULT_PORT),
|
|
49
|
-
tokenFile
|
|
52
|
+
tokenFile,
|
|
53
|
+
lockFile: httpLockFile(tokenFile),
|
|
54
|
+
version: app.version,
|
|
50
55
|
});
|
|
51
56
|
return;
|
|
52
57
|
}
|
|
@@ -54,6 +59,24 @@ export const mcpCommand = (messenger) => {
|
|
|
54
59
|
const { serveOverStdio } = await import("../../mcp/server.js");
|
|
55
60
|
await serveOverStdio(this, context, messenger, {});
|
|
56
61
|
});
|
|
62
|
+
command.addCommand(mcpServerCommand({
|
|
63
|
+
example: `${app.command} personal`,
|
|
64
|
+
target: (self) => {
|
|
65
|
+
const { settings, env } = messengerContext(self, messenger);
|
|
66
|
+
return {
|
|
67
|
+
invocation: `${app.command} ${settings.profile}`,
|
|
68
|
+
profile: settings.profile,
|
|
69
|
+
envPrefix: app.envPrefix,
|
|
70
|
+
env,
|
|
71
|
+
tokenFile: httpTokenFile(app, settings.profile, env),
|
|
72
|
+
argv: ["mcp", "--http"],
|
|
73
|
+
childEnv: { [`${app.envPrefix}_PROFILE`]: settings.profile },
|
|
74
|
+
version: app.version,
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
answer: (self, item, lines) => renderAnswer(self, messengerContext(self, messenger), itemOf(item), () => lines.join("\n")),
|
|
78
|
+
system: (self) => environmentOf(self).system,
|
|
79
|
+
}));
|
|
57
80
|
command.addCommand(withFlags(annotate(new Command("config"), {
|
|
58
81
|
effects: LOCAL_READ,
|
|
59
82
|
examples: [`${app.command} personal mcp config`],
|
package/dist/mcp/http/oauth.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface OwnerLogin {
|
|
|
7
7
|
verifier: OAuthTokenVerifier;
|
|
8
8
|
/** Answers the login routes; `undefined` for any other path. */
|
|
9
9
|
handle(request: Request): Promise<Response | undefined>;
|
|
10
|
+
close(): void;
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
13
|
* An OAuth 2.1 authorization server for exactly one owner (CLI-58). Anyone may register a client —
|
|
@@ -18,7 +19,7 @@ export declare const ownerLogin: ({ issuer, resource, file, appName, onCode, now
|
|
|
18
19
|
resource: URL;
|
|
19
20
|
file: string;
|
|
20
21
|
appName: string;
|
|
21
|
-
/**
|
|
22
|
+
/** The code for the next login, for the owner's eyes only: their terminal, or the server's status file. */
|
|
22
23
|
onCode: (code: string, expires: Date) => void;
|
|
23
24
|
now?: () => number;
|
|
24
25
|
}) => OwnerLogin;
|
package/dist/mcp/http/oauth.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomInt, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { chmodSync,
|
|
2
|
+
import { chmodSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { OAuthError, OAuthErrorCode, } from "@modelcontextprotocol/server";
|
|
5
|
-
|
|
6
|
-
const REFRESH_SECONDS = 30 * 24 * 60 * 60;
|
|
5
|
+
import { ACCESS_SECONDS, REFRESH_SECONDS, readStored } from "./state.js";
|
|
7
6
|
const AUTH_CODE_MS = 60 * 1000;
|
|
8
7
|
const LOGIN_CODE_MS = 10 * 60 * 1000;
|
|
9
8
|
const WRONG_TRIES = 5;
|
|
@@ -20,7 +19,7 @@ const same = (given, expected) => {
|
|
|
20
19
|
};
|
|
21
20
|
/** Only hashes reach the disk: the file is worth nothing to someone who copies it. */
|
|
22
21
|
const tokenFile = (path) => ({
|
|
23
|
-
read: () =>
|
|
22
|
+
read: () => readStored(path),
|
|
24
23
|
write: (stored) => {
|
|
25
24
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
26
25
|
const temporary = `${path}.${randomBytes(16).toString("hex")}.tmp`;
|
|
@@ -58,9 +57,14 @@ export const ownerLogin = ({ issuer, resource, file, appName, onCode, now = () =
|
|
|
58
57
|
let login = { value: "", expires: 0 };
|
|
59
58
|
let wrong = 0;
|
|
60
59
|
let retryAt = 0;
|
|
60
|
+
let renewal;
|
|
61
|
+
// Renewed when it runs out, not only when someone tries it: a code read from the status file must still work.
|
|
61
62
|
const freshCode = () => {
|
|
62
63
|
const part = () => Array.from({ length: 4 }, () => CODE_ALPHABET[randomInt(CODE_ALPHABET.length)]).join("");
|
|
63
64
|
login = { value: `${part()}-${part()}`, expires: now() + LOGIN_CODE_MS };
|
|
65
|
+
clearTimeout(renewal);
|
|
66
|
+
renewal = setTimeout(freshCode, LOGIN_CODE_MS);
|
|
67
|
+
renewal.unref();
|
|
64
68
|
onCode(login.value, new Date(login.expires));
|
|
65
69
|
};
|
|
66
70
|
freshCode();
|
|
@@ -147,7 +151,7 @@ export const ownerLogin = ({ issuer, resource, file, appName, onCode, now = () =
|
|
|
147
151
|
.join("");
|
|
148
152
|
const who = escapeHtml(client.name ?? "an application");
|
|
149
153
|
const target = new URL(params.get("redirect_uri") ?? "");
|
|
150
|
-
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(appName)} login</title><style>body{font:16px system-ui,sans-serif;max-width:28rem;margin:3rem auto;padding:0 1rem;color:#1d2430}input[type=text]{font:20px ui-monospace,monospace;letter-spacing:.1em;width:100%;padding:.5rem;margin:.5rem 0 1rem;box-sizing:border-box}button{font:inherit;padding:.5rem 1rem}.error{color:#b42318}</style></head><body><h1>${escapeHtml(appName)}</h1><p>An app that calls itself <strong>${who}</strong> asks to reach your messages through <code>${escapeHtml(appName)} mcp --http</code>.</p><p>After you allow it, the login goes to <strong>${escapeHtml(target.host)}</strong>. Allow only if that is the app you are connecting, such as chatgpt.com or claude.ai.</p><p>Type the code shown in the terminal where
|
|
154
|
+
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(appName)} login</title><style>body{font:16px system-ui,sans-serif;max-width:28rem;margin:3rem auto;padding:0 1rem;color:#1d2430}input[type=text]{font:20px ui-monospace,monospace;letter-spacing:.1em;width:100%;padding:.5rem;margin:.5rem 0 1rem;box-sizing:border-box}button{font:inherit;padding:.5rem 1rem}.error{color:#b42318}</style></head><body><h1>${escapeHtml(appName)}</h1><p>An app that calls itself <strong>${who}</strong> asks to reach your messages through <code>${escapeHtml(appName)} mcp --http</code>.</p><p>After you allow it, the login goes to <strong>${escapeHtml(target.host)}</strong>. Allow only if that is the app you are connecting, such as chatgpt.com or claude.ai.</p><p>Type the code shown in WireCat Desktop, or in the terminal where the server runs. If you did not start this login, close this page.</p>${message ? `<p class="error">${escapeHtml(message)}</p>` : ""}<form method="post" action="/authorize">${hidden}<label>Login code<input type="text" name="login_code" autocomplete="off" autofocus required></label><button type="submit">Allow</button></form></body></html>`;
|
|
151
155
|
return new Response(html, {
|
|
152
156
|
status: message ? 400 : 200,
|
|
153
157
|
headers: {
|
|
@@ -174,7 +178,7 @@ export const ownerLogin = ({ issuer, resource, file, appName, onCode, now = () =
|
|
|
174
178
|
return new Response("This server needs response_type=code and PKCE with S256.", { status: 400 });
|
|
175
179
|
if (request.method !== "POST")
|
|
176
180
|
return page(params, client);
|
|
177
|
-
const throttled = () => new Response("Too many wrong codes. Wait
|
|
181
|
+
const throttled = () => new Response("Too many wrong codes. Wait, then try the new code.", {
|
|
178
182
|
status: 429,
|
|
179
183
|
headers: { "retry-after": String(Math.ceil((retryAt - now()) / 1000)) },
|
|
180
184
|
});
|
|
@@ -187,7 +191,7 @@ export const ownerLogin = ({ issuer, resource, file, appName, onCode, now = () =
|
|
|
187
191
|
const typed = (params.get("login_code") ?? "").trim().toUpperCase();
|
|
188
192
|
if (login.expires < now()) {
|
|
189
193
|
freshCode();
|
|
190
|
-
return page(params, client, "The code expired. A new one is shown in the terminal.");
|
|
194
|
+
return page(params, client, "The code expired. A new one is shown in WireCat Desktop and in the terminal.");
|
|
191
195
|
}
|
|
192
196
|
if (!same(typed, login.value)) {
|
|
193
197
|
wrong += 1;
|
|
@@ -266,6 +270,7 @@ export const ownerLogin = ({ issuer, resource, file, appName, onCode, now = () =
|
|
|
266
270
|
};
|
|
267
271
|
return {
|
|
268
272
|
code: () => login.value,
|
|
273
|
+
close: () => clearTimeout(renewal),
|
|
269
274
|
metadata,
|
|
270
275
|
verifier,
|
|
271
276
|
handle: async (request) => {
|
package/dist/mcp/http/serve.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ export interface HttpOptions {
|
|
|
11
11
|
onCode: (code: string, expires: Date) => void;
|
|
12
12
|
onError?: (error: Error) => void;
|
|
13
13
|
now?: () => number;
|
|
14
|
+
/**
|
|
15
|
+
* `httpLockFile(tokenFile)`: one server per token file, and what `mcp server status` reads — the
|
|
16
|
+
* port, the address and the current login code. Without it nothing is written.
|
|
17
|
+
*/
|
|
18
|
+
lockFile?: string;
|
|
19
|
+
version?: string;
|
|
14
20
|
}
|
|
15
21
|
/**
|
|
16
22
|
* MCP over Streamable HTTP on 127.0.0.1, behind the owner's tunnel (CLI-58). Every MCP request needs
|
package/dist/mcp/http/serve.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { createMcpHandler, getOAuthProtectedResourceMetadataUrl, hostHeaderValidationResponse, isLegacyRequest, oauthMetadataResponse, requireBearerAuth, WebStandardStreamableHTTPServerTransport, } from "@modelcontextprotocol/server";
|
|
3
|
+
import { CliError } from "@wirecat/cli-base";
|
|
3
4
|
import { listen } from "../../http/listen.js";
|
|
4
5
|
import { ownerLogin } from "./oauth.js";
|
|
6
|
+
import { httpLock } from "./state.js";
|
|
5
7
|
export const MCP_PATH = "/mcp";
|
|
6
8
|
const LOCAL = ["localhost", "127.0.0.1"];
|
|
7
9
|
const MAX_SESSIONS = 20;
|
|
@@ -65,10 +67,31 @@ const legacySessions = (build, now) => {
|
|
|
65
67
|
* the owner's browser cannot reach the port through a rebound DNS name.
|
|
66
68
|
*/
|
|
67
69
|
export const serveOverHttp = async (build, options) => {
|
|
68
|
-
const { publicUrl, port, tokenFile, appName, onCode, onError, now } = options;
|
|
70
|
+
const { publicUrl, port, tokenFile, appName, onCode, onError, now, lockFile, version } = options;
|
|
71
|
+
const lock = lockFile
|
|
72
|
+
? httpLock(lockFile, {
|
|
73
|
+
pid: process.pid,
|
|
74
|
+
startedAt: new Date().toISOString(),
|
|
75
|
+
port,
|
|
76
|
+
publicUrl: publicUrl.href,
|
|
77
|
+
...(version ? { version } : {}),
|
|
78
|
+
})
|
|
79
|
+
: undefined;
|
|
80
|
+
if (lock?.held)
|
|
81
|
+
throw new CliError("validation_error", `${appName} already serves MCP over HTTP for this profile on port ${lock.held.port} (PID ${lock.held.pid}, since ${lock.held.startedAt})`);
|
|
69
82
|
const issuer = new URL(publicUrl.origin);
|
|
70
83
|
const resource = new URL(MCP_PATH, issuer);
|
|
71
|
-
const login = ownerLogin({
|
|
84
|
+
const login = ownerLogin({
|
|
85
|
+
issuer,
|
|
86
|
+
resource,
|
|
87
|
+
file: tokenFile,
|
|
88
|
+
appName,
|
|
89
|
+
onCode: (code, expires) => {
|
|
90
|
+
lock?.update({ loginCode: code, loginCodeExpiresAt: expires.toISOString() });
|
|
91
|
+
onCode(code, expires);
|
|
92
|
+
},
|
|
93
|
+
...(now ? { now } : {}),
|
|
94
|
+
});
|
|
72
95
|
const insecure = issuer.protocol === "http:" && LOCAL.includes(issuer.hostname);
|
|
73
96
|
const mcp = createMcpHandler(build, { legacy: "reject", ...(onError ? { onerror: onError } : {}) });
|
|
74
97
|
const legacy = legacySessions(build, now ?? Date.now);
|
|
@@ -106,13 +129,24 @@ export const serveOverHttp = async (build, options) => {
|
|
|
106
129
|
if (auth instanceof Response)
|
|
107
130
|
return auth;
|
|
108
131
|
return (await isLegacyRequest(request)) ? legacy.handle(request, auth) : mcp.fetch(request, { authInfo: auth });
|
|
109
|
-
}, { host: "127.0.0.1", port })
|
|
132
|
+
}, { host: "127.0.0.1", port }).catch((error) => {
|
|
133
|
+
login.close();
|
|
134
|
+
lock?.release();
|
|
135
|
+
throw error;
|
|
136
|
+
});
|
|
137
|
+
lock?.update({ listeningAt: new Date().toISOString() });
|
|
110
138
|
return {
|
|
111
139
|
url: listening.url,
|
|
112
140
|
close: async () => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
141
|
+
login.close();
|
|
142
|
+
try {
|
|
143
|
+
await listening.close();
|
|
144
|
+
await legacy.close();
|
|
145
|
+
await mcp.close();
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
lock?.release();
|
|
149
|
+
}
|
|
116
150
|
},
|
|
117
151
|
};
|
|
118
152
|
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type Lock } from "../../background/lock.js";
|
|
2
|
+
export declare const ACCESS_SECONDS: number;
|
|
3
|
+
export declare const REFRESH_SECONDS: number;
|
|
4
|
+
export interface StoredClient {
|
|
5
|
+
id: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
secretHash?: string;
|
|
8
|
+
redirectUris: string[];
|
|
9
|
+
authMethod: "none" | "client_secret_post" | "client_secret_basic";
|
|
10
|
+
issuedAt: number;
|
|
11
|
+
}
|
|
12
|
+
export interface StoredToken {
|
|
13
|
+
hash: string;
|
|
14
|
+
kind: "access" | "refresh";
|
|
15
|
+
clientId: string;
|
|
16
|
+
expiresAt: number;
|
|
17
|
+
}
|
|
18
|
+
/** The token file: registered clients and the hashes of the tokens they hold. */
|
|
19
|
+
export interface Stored {
|
|
20
|
+
clients: StoredClient[];
|
|
21
|
+
tokens: StoredToken[];
|
|
22
|
+
}
|
|
23
|
+
export declare const readStored: (path: string) => Stored;
|
|
24
|
+
/** What a running `mcp --http` writes beside its token file, mode 600 — the login code is a live credential. */
|
|
25
|
+
export interface HttpLock extends Lock {
|
|
26
|
+
port: number;
|
|
27
|
+
publicUrl: string;
|
|
28
|
+
loginCode?: string;
|
|
29
|
+
loginCodeExpiresAt?: string;
|
|
30
|
+
}
|
|
31
|
+
/** The variable a server started by `mcp server start` carries; a serve's `<PREFIX>_SERVER` never ends with it. */
|
|
32
|
+
export declare const mcpHttpMarker: (envPrefix: string) => string;
|
|
33
|
+
export declare const httpLogFile: (tokenFile: string) => string;
|
|
34
|
+
/** Beside the token file, so each CLI's own layout decides both: `mcp-http/<profile>.json` → `.lock`. */
|
|
35
|
+
export declare const httpLockFile: (tokenFile: string) => string;
|
|
36
|
+
export declare const readHttpLock: (path: string) => HttpLock | undefined;
|
|
37
|
+
/** The lock a serving process keeps current: taken before it listens, given up when it closes. */
|
|
38
|
+
export declare const httpLock: (path: string, first: HttpLock) => {
|
|
39
|
+
held: HttpLock | undefined;
|
|
40
|
+
update: (change: Partial<HttpLock>) => void;
|
|
41
|
+
release: () => void;
|
|
42
|
+
};
|
|
43
|
+
export interface SignedInClient {
|
|
44
|
+
/** What the app said its name was when it registered — anyone may register, so it proves nothing. */
|
|
45
|
+
claimedName: string | null;
|
|
46
|
+
lastLoginAt: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The apps that hold a token now, and when each last got one. Ids and hashes stay in the file: what
|
|
50
|
+
* leaves is enough to recognise an app, never to act as it.
|
|
51
|
+
*/
|
|
52
|
+
export declare const signedInClients: (tokenFile: string, now?: number) => SignedInClient[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { holdLock, readLock, releaseLock, takeLock } from "../../background/lock.js";
|
|
3
|
+
export const ACCESS_SECONDS = 60 * 60;
|
|
4
|
+
export const REFRESH_SECONDS = 30 * 24 * 60 * 60;
|
|
5
|
+
export const readStored = (path) => existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : { clients: [], tokens: [] };
|
|
6
|
+
/** The variable a server started by `mcp server start` carries; a serve's `<PREFIX>_SERVER` never ends with it. */
|
|
7
|
+
export const mcpHttpMarker = (envPrefix) => `${envPrefix}_MCP_HTTP`;
|
|
8
|
+
export const httpLogFile = (tokenFile) => `${tokenFile.replace(/\.json$/, "")}.log`;
|
|
9
|
+
/** Beside the token file, so each CLI's own layout decides both: `mcp-http/<profile>.json` → `.lock`. */
|
|
10
|
+
export const httpLockFile = (tokenFile) => `${tokenFile.replace(/\.json$/, "")}.lock`;
|
|
11
|
+
export const readHttpLock = (path) => {
|
|
12
|
+
const lock = readLock(path);
|
|
13
|
+
return lock && typeof lock.port === "number" && typeof lock.publicUrl === "string" ? lock : undefined;
|
|
14
|
+
};
|
|
15
|
+
/** The lock a serving process keeps current: taken before it listens, given up when it closes. */
|
|
16
|
+
export const httpLock = (path, first) => {
|
|
17
|
+
let current = first;
|
|
18
|
+
const held = takeLock(path, current);
|
|
19
|
+
return {
|
|
20
|
+
held: held,
|
|
21
|
+
update: (change) => {
|
|
22
|
+
current = { ...current, ...change };
|
|
23
|
+
holdLock(path, current);
|
|
24
|
+
},
|
|
25
|
+
release: () => releaseLock(path),
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The apps that hold a token now, and when each last got one. Ids and hashes stay in the file: what
|
|
30
|
+
* leaves is enough to recognise an app, never to act as it.
|
|
31
|
+
*/
|
|
32
|
+
export const signedInClients = (tokenFile, now = Date.now()) => {
|
|
33
|
+
const seconds = Math.floor(now / 1000);
|
|
34
|
+
const { clients, tokens } = readStored(tokenFile);
|
|
35
|
+
const last = new Map();
|
|
36
|
+
for (const token of tokens) {
|
|
37
|
+
if (token.expiresAt <= seconds)
|
|
38
|
+
continue;
|
|
39
|
+
const issued = token.expiresAt - (token.kind === "access" ? ACCESS_SECONDS : REFRESH_SECONDS);
|
|
40
|
+
last.set(token.clientId, Math.max(last.get(token.clientId) ?? 0, issued));
|
|
41
|
+
}
|
|
42
|
+
return clients
|
|
43
|
+
.filter((client) => last.has(client.id))
|
|
44
|
+
.map((client) => ({
|
|
45
|
+
claimedName: client.name ?? null,
|
|
46
|
+
lastLoginAt: new Date((last.get(client.id) ?? 0) * 1000).toISOString(),
|
|
47
|
+
}))
|
|
48
|
+
.sort((a, b) => b.lastLoginAt.localeCompare(a.lastLoginAt));
|
|
49
|
+
};
|
package/dist/mcp/index.d.ts
CHANGED
|
@@ -3,4 +3,5 @@ export { type AgentAnswer, type AgentContext, type AgentProgram, type AgentTool,
|
|
|
3
3
|
export { type BotServerOptions, createBotServer, type RunBotCommand } from "./bot/server.js";
|
|
4
4
|
export { revokeAll } from "./http/oauth.js";
|
|
5
5
|
export { type HttpOptions, MCP_PATH, serveOverHttp } from "./http/serve.js";
|
|
6
|
+
export { httpLockFile } from "./http/state.js";
|
|
6
7
|
export { type McpCommand, registerCommands } from "./surface.js";
|
package/dist/mcp/index.js
CHANGED
|
@@ -4,4 +4,5 @@ export { runCommandForAgent, toolsFromCommands, } from "./agent.js";
|
|
|
4
4
|
export { createBotServer } from "./bot/server.js";
|
|
5
5
|
export { revokeAll } from "./http/oauth.js";
|
|
6
6
|
export { MCP_PATH, serveOverHttp } from "./http/serve.js";
|
|
7
|
+
export { httpLockFile } from "./http/state.js";
|
|
7
8
|
export { registerCommands } from "./surface.js";
|
package/dist/mcp/server.js
CHANGED
|
@@ -25,6 +25,7 @@ import { levelFor, readKeysForCommand } from "../sends/permissions.js";
|
|
|
25
25
|
import { openStore } from "../store/store.js";
|
|
26
26
|
import { runCommandForAgent, toolsFromCommands } from "./agent.js";
|
|
27
27
|
import { agentLog } from "./agent-log.js";
|
|
28
|
+
import { mcpHttpMarker } from "./http/state.js";
|
|
28
29
|
import { instructions } from "./instructions.js";
|
|
29
30
|
import { registerPrompts } from "./prompts.js";
|
|
30
31
|
import { registerResources } from "./resources.js";
|
|
@@ -469,10 +470,14 @@ export const serveOverHttpUntilStopped = async (command, context, messenger, opt
|
|
|
469
470
|
const { session, embedders, build, log } = createServer(command, context, messenger, options);
|
|
470
471
|
const { serveOverHttp } = await import("./http/serve.js");
|
|
471
472
|
const appName = messenger.app.command;
|
|
473
|
+
const appEnvPrefix = messenger.app.envPrefix;
|
|
472
474
|
const listening = await serveOverHttp(build, {
|
|
473
475
|
...http,
|
|
474
476
|
appName,
|
|
475
|
-
|
|
477
|
+
// In the background its log is a file; the code goes only to the lock that `mcp server status` reads.
|
|
478
|
+
onCode: (code, expires) => context.renderer.note(context.env[mcpHttpMarker(appEnvPrefix)] === undefined
|
|
479
|
+
? `login code for a new browser app: ${code} (until ${expires.toTimeString().slice(0, 5)}; a new one after each login)`
|
|
480
|
+
: `a new login code for a browser app: \`${appName} ${context.settings.profile} mcp server status\``),
|
|
476
481
|
onError: (error) => context.renderer.note(`mcp: ${error.message}`),
|
|
477
482
|
}).catch(async (error) => {
|
|
478
483
|
await session.close();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.446.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
|
@@ -3179,6 +3179,23 @@
|
|
|
3179
3179
|
}
|
|
3180
3180
|
}
|
|
3181
3181
|
},
|
|
3182
|
+
"mcp server": {
|
|
3183
|
+
"in": "all"
|
|
3184
|
+
},
|
|
3185
|
+
"mcp server start": {
|
|
3186
|
+
"in": "all",
|
|
3187
|
+
"options": {
|
|
3188
|
+
"--permission": "all",
|
|
3189
|
+
"--port": "all",
|
|
3190
|
+
"--public-url": "all"
|
|
3191
|
+
}
|
|
3192
|
+
},
|
|
3193
|
+
"mcp server status": {
|
|
3194
|
+
"in": "all"
|
|
3195
|
+
},
|
|
3196
|
+
"mcp server stop": {
|
|
3197
|
+
"in": "all"
|
|
3198
|
+
},
|
|
3182
3199
|
"mentions": {
|
|
3183
3200
|
"in": "all"
|
|
3184
3201
|
},
|