@wirecat/cli-template 0.6.1 → 0.8.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/README.md +2 -2
- package/bin/release +36 -12
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +53 -7
- package/dist/commands/mcp.js.map +1 -1
- package/dist/mcp.d.ts +10 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +33 -2
- package/dist/mcp.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/commands.md +56 -3
- package/docs/dev/skills/release/fresh-audit.md +3 -3
- package/docs/dev/test-matrix.md +11 -1
- package/docs/usage.md +7 -1
- package/kit/dist/interview.js +2 -2
- package/kit/dist/new.js +15 -9
- package/package.json +3 -3
- package/scripts/test-matrix-untested.ts +28 -0
- package/src/commands/mcp.ts +78 -7
- package/src/mcp-http.test.ts +165 -0
- package/src/mcp.ts +54 -2
- package/src/roots.test.ts +7 -2
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,55 @@ Notable changes to `@wirecat/cli-template`. One section per version, newest firs
|
|
|
5
5
|
|
|
6
6
|
<!-- cspell:words katex smol whatsapp -->
|
|
7
7
|
|
|
8
|
+
## 0.8.0 — 27.10.2026
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- The fresh-audit prompt lets the auditor read the cli-base workflow files the CLI's workflows pin (WC-CI-8), as
|
|
13
|
+
the handbook's New CLI page now says.
|
|
14
|
+
|
|
15
|
+
### Removed
|
|
16
|
+
|
|
17
|
+
- **`mcp --permission` is gone.** cli-core 0.449.0 checks a `--permission` key against the messenger's names
|
|
18
|
+
(`messages`, `drafts`, `chats` and the like), so in a CLI made from this template it refused every value, its own
|
|
19
|
+
command paths included. It returns when cli-core can check a CLI's own permission names. Until then the profile's
|
|
20
|
+
`permissions` are the way to make the MCP server stricter. `mcp server start` still shows a `--permission` option,
|
|
21
|
+
which cli-core adds; the server it starts refuses it.
|
|
22
|
+
|
|
23
|
+
## 0.7.0 — 27.10.2026
|
|
24
|
+
|
|
25
|
+
### Changed — may break callers
|
|
26
|
+
|
|
27
|
+
- **The skeleton runs on cli-core 0.449.0 and cli-base 0.62.0**, the only cli-base line cli-core 0.449 accepts; a CLI
|
|
28
|
+
that pins cli-base moves both pins together. The commands and their help do not change, apart from the `mcp`
|
|
29
|
+
additions below.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **`clidev <profile> mcp --http` serves the profile to ChatGPT and Claude in the browser**, on `127.0.0.1` behind the
|
|
34
|
+
owner's tunnel, with a one-time login code; `--revoke` ends every login, and is refused under `CLIDEV_READ_ONLY`.
|
|
35
|
+
**`mcp server start|stop|status`** runs it in the background (from cli-core 0.445.0): `status` shows the port, the
|
|
36
|
+
connector address, the current login code and the apps logged in, and the background server writes no login code to
|
|
37
|
+
its log. A second `mcp --http` for the same profile is refused. `mcp --permission key=level` makes a permission
|
|
38
|
+
stricter for one server, over stdio too. A CLI made from an older template takes this with `upgrade`, or mounts
|
|
39
|
+
`mcpServerCommand` on its own `mcp` and passes `lockFile: httpLockFile(tokenFile)` to `serveOverHttp`.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **`new` writes the speech model advice into `docs/dev/DESIGN.md` only for `--category messenger`.** A calendar or
|
|
44
|
+
notes CLI got "If this CLI defines a messenger, set its speechModels…" and a warning that no speech model hears a
|
|
45
|
+
language, though only a messenger transcribes voice messages.
|
|
46
|
+
- **The cron step `new` prints points to the list of taken monthly minutes** in cli-base's CI page (WC-CI-9), so
|
|
47
|
+
nobody has to read `monthly.yml` in every repository to find a free one. The examples now use `gcal-cli` and
|
|
48
|
+
`gcal`: `cal` is the system calendar program on macOS and many Linux systems.
|
|
49
|
+
- **Every command spends less time getting ready (cli-base 0.62.0, cli-core 0.449.0).** The check of shared commands now
|
|
50
|
+
describes each command once, not again for every group above it. It was about 60% of each command's setup; in tg a
|
|
51
|
+
warm `run(["--version"])` went from 49 to 29 ms. It catches the same changes as before.
|
|
52
|
+
- **`bin/release` no longer reports a release that passed as failed.** `gh run watch` also stops with an error when
|
|
53
|
+
one of its own requests to GitHub fails, while the run goes on; 0.6.1 was published, tagged and green, and the
|
|
54
|
+
script said it failed. It now waits for the run to finish, reads its result, and checks npm before it says anything.
|
|
55
|
+
It names `bin/release --local` only when npm refused the publish because it does not trust the workflow yet.
|
|
56
|
+
|
|
8
57
|
## 0.6.1 — 26.10.2026
|
|
9
58
|
|
|
10
59
|
### Fixed
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ with the tool's own introduction once it does.
|
|
|
11
11
|
To start a new CLI from the template:
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
npx @wirecat/cli-template new
|
|
14
|
+
npx @wirecat/cli-template new gcal-cli --command gcal --package @wirecat/gcal-cli --owner WireCatLabs
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## Quick start
|
|
@@ -33,7 +33,7 @@ such as `personal` in `clidev personal config show`; none is ever assumed. `CLID
|
|
|
33
33
|
| `clidev commands --json` | Prints every command as JSON, for agents and scripts |
|
|
34
34
|
| `clidev personal config show` | Shows each setting and where it came from |
|
|
35
35
|
| `clidev skill install` | Installs the agent skill for Claude Code, Codex, Gemini CLI and OpenClaw; `--for hermes` for Hermes Agent |
|
|
36
|
-
| `clidev personal mcp` | Serves the profile's commands to an AI agent over MCP |
|
|
36
|
+
| `clidev personal mcp` | Serves the profile's commands to an AI agent over MCP; `--http` for agents in the browser |
|
|
37
37
|
| `clidev runs list` | Lists recorded runs, newest first: every command that failed is kept |
|
|
38
38
|
| `clidev upgrade --check` | Says whether a newer version is on npm |
|
|
39
39
|
|
package/bin/release
CHANGED
|
@@ -55,6 +55,15 @@ version=$(node -p 'require("./package.json").version')
|
|
|
55
55
|
published() {
|
|
56
56
|
npm view "$package@$version" version --prefer-online >/dev/null 2>&1
|
|
57
57
|
}
|
|
58
|
+
# The version list `npm view` reads can lag minutes behind a publish.
|
|
59
|
+
wait_for_npm() {
|
|
60
|
+
echo "== waiting for npm to show $version (up to fifteen minutes) =="
|
|
61
|
+
for _ in $(seq 90); do
|
|
62
|
+
published && return 0
|
|
63
|
+
sleep 10
|
|
64
|
+
done
|
|
65
|
+
return 1
|
|
66
|
+
}
|
|
58
67
|
|
|
59
68
|
# main takes changes only through a pull request (the repository's ruleset).
|
|
60
69
|
land() {
|
|
@@ -147,11 +156,32 @@ if [ "$mode" = ci ]; then
|
|
|
147
156
|
sleep 2
|
|
148
157
|
done
|
|
149
158
|
[ -n "$run" ] || { echo "the release workflow did not start within a minute — see gh run list --workflow release.yml" >&2; exit 1; }
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
159
|
+
# gh run watch also exits non-zero when one of its own API calls fails, while the run goes on: only the run's
|
|
160
|
+
# conclusion and npm say whether the release failed.
|
|
161
|
+
state=""
|
|
162
|
+
for _ in $(seq 30); do
|
|
163
|
+
gh run watch "$run" || sleep 10
|
|
164
|
+
state=$(gh run view "$run" --json status,conclusion --jq '.status + ":" + .conclusion' 2>/dev/null || true)
|
|
165
|
+
case "$state" in completed:*) break ;; esac
|
|
166
|
+
done
|
|
167
|
+
case "$state" in
|
|
168
|
+
completed:success)
|
|
169
|
+
wait_for_npm ||
|
|
170
|
+
{ echo "release run $run passed but npm does not show $version — see gh run view $run" >&2; exit 1; } ;;
|
|
171
|
+
completed:*)
|
|
172
|
+
if published; then
|
|
173
|
+
echo "$package@$version is on npm, but release run $run did not pass; check its tag: gh run view $run" >&2
|
|
174
|
+
exit 1
|
|
175
|
+
fi
|
|
176
|
+
echo "release failed; inspect: gh run view $run --log-failed" >&2
|
|
177
|
+
# npm answers 404 to a publish from a workflow it does not trust.
|
|
178
|
+
if gh run view "$run" --log-failed 2>/dev/null | grep -q "404 Not Found - PUT"; then
|
|
179
|
+
echo "npm refused the publish; a package npm does not trust yet publishes its first version with" >&2
|
|
180
|
+
echo "bin/release --local" >&2
|
|
181
|
+
fi
|
|
182
|
+
exit 1 ;;
|
|
183
|
+
*) echo "lost track of release run $run — see gh run view $run" >&2; exit 1 ;;
|
|
184
|
+
esac
|
|
155
185
|
git fetch -q --tags origin
|
|
156
186
|
echo "published $package@$version"
|
|
157
187
|
exit 0
|
|
@@ -194,13 +224,7 @@ echo "== publishing $package@$version =="
|
|
|
194
224
|
pnpm build
|
|
195
225
|
NPM_TOKEN="$publish_token" npm publish --access public --ignore-scripts "$@"
|
|
196
226
|
|
|
197
|
-
|
|
198
|
-
seen=""
|
|
199
|
-
for _ in $(seq 90); do
|
|
200
|
-
if published; then seen=1; break; fi
|
|
201
|
-
sleep 10
|
|
202
|
-
done
|
|
203
|
-
[ -n "$seen" ] || {
|
|
227
|
+
wait_for_npm || {
|
|
204
228
|
echo "npm does not show $version after fifteen minutes — not tagging. Once it does, on $(git rev-parse HEAD):" >&2
|
|
205
229
|
echo " git tag -a v$version -m $version && git push origin v$version" >&2
|
|
206
230
|
exit 1
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/commands/mcp.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/commands/mcp.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAkBnC,eAAO,MAAM,UAAU,QAAO,OAyExB,CAAA"}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { annotate, describeProgram } from "@wirecat/cli-base/commands";
|
|
3
|
-
import { changesAuthorityAt, environmentOf, REMOTE_WRITE, rootOf } from "@wirecat/cli-core/cli";
|
|
3
|
+
import { changesAuthorityAt, environmentOf, httpTokenFile, itemOf, MCP_HTTP_DEFAULT_PORT, mcpServerCommand, outputFor, REMOTE_WRITE, refuseUnderReadOnly, renderAnswer, rootOf, } from "@wirecat/cli-core/cli";
|
|
4
|
+
import { portOf, publicUrlOf } from "@wirecat/cli-core/mcp";
|
|
4
5
|
import { Command } from "commander";
|
|
5
6
|
import { configuration } from "../config.js";
|
|
6
7
|
import { IDENTITY } from "../identity.js";
|
|
8
|
+
const settingsOf = (command) => {
|
|
9
|
+
const flags = command.optsWithGlobals();
|
|
10
|
+
const env = environmentOf(command).env ?? process.env;
|
|
11
|
+
const { profile } = configuration.resolveSettings(flags, { env });
|
|
12
|
+
return { flags, env, profile, tokenFile: httpTokenFile(IDENTITY, profile, env) };
|
|
13
|
+
};
|
|
7
14
|
export const mcpCommand = () => annotate(new Command("mcp"), {
|
|
8
15
|
effects: REMOTE_WRITE,
|
|
9
16
|
lifetime: "persistent",
|
|
@@ -12,25 +19,64 @@ export const mcpCommand = () => annotate(new Command("mcp"), {
|
|
|
12
19
|
})
|
|
13
20
|
.summary("serve this profile's commands to an AI agent")
|
|
14
21
|
.description(`serve this profile's commands to an agent over MCP, on stdin and stdout, writes included; the profile's permissions decide — \`claude mcp add ${IDENTITY.command} -- ${IDENTITY.command} personal mcp\``)
|
|
22
|
+
.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")
|
|
23
|
+
.option("--port <port>", `the local port for --http (default ${MCP_HTTP_DEFAULT_PORT})`)
|
|
24
|
+
.option("--public-url <url>", "the tunnel's https address the browser apps use, e.g. https://<name>.ts.net")
|
|
25
|
+
.option("--revoke", "forget every login given to a browser app; each must log in again")
|
|
26
|
+
.addCommand(mcpServerCommand({
|
|
27
|
+
example: `${IDENTITY.command} personal`,
|
|
28
|
+
target: (self) => {
|
|
29
|
+
const { env, profile, tokenFile } = settingsOf(self);
|
|
30
|
+
return {
|
|
31
|
+
invocation: `${IDENTITY.command} ${profile}`,
|
|
32
|
+
profile,
|
|
33
|
+
envPrefix: IDENTITY.envPrefix,
|
|
34
|
+
env,
|
|
35
|
+
tokenFile,
|
|
36
|
+
argv: [profile, "mcp", "--http"],
|
|
37
|
+
version: IDENTITY.version,
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
answer: (self, item, lines) => renderAnswer(self, outputFor(self), itemOf(item), () => lines.join("\n")),
|
|
41
|
+
}))
|
|
15
42
|
.action(async function () {
|
|
16
|
-
const flags = this
|
|
17
|
-
if (flags.
|
|
43
|
+
const { flags, env, profile, tokenFile } = settingsOf(this);
|
|
44
|
+
if (flags.revoke) {
|
|
45
|
+
refuseUnderReadOnly(this, "mcp --revoke");
|
|
46
|
+
const { revokeAll } = await import("@wirecat/cli-core/mcp");
|
|
47
|
+
revokeAll(tokenFile);
|
|
48
|
+
renderAnswer(this, outputFor(this), itemOf({ revoked: true, profile }));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!flags.http && (flags.json || flags.jsonl))
|
|
18
52
|
throw new CliError("validation_error", "MCP over stdio controls its own output; leave out --json and --jsonl");
|
|
19
53
|
const environment = environmentOf(this);
|
|
20
|
-
const env = environment.env ?? process.env;
|
|
21
|
-
const { profile } = configuration.resolveSettings(flags, { env });
|
|
22
54
|
const run = environment.runCommand;
|
|
23
55
|
if (!run)
|
|
24
56
|
throw new Error("mcp runs inside the program's run(), which provides runCommand");
|
|
25
57
|
const program = rootOf(this);
|
|
26
|
-
const
|
|
27
|
-
await serveMcp({
|
|
58
|
+
const options = {
|
|
28
59
|
profile,
|
|
29
60
|
commands: describeProgram(program),
|
|
30
61
|
levels: () => configuration.resolveSettings(flags, { env }).permissions,
|
|
31
62
|
authority: changesAuthorityAt(program),
|
|
32
63
|
run,
|
|
33
64
|
...(environment.signal ? { signal: environment.signal } : {}),
|
|
65
|
+
};
|
|
66
|
+
// Loaded here, not at the top: every other command would otherwise pay for the MCP SDK.
|
|
67
|
+
const { serveMcp, serveMcpOverHttp } = await import("../mcp.js");
|
|
68
|
+
if (!flags.http)
|
|
69
|
+
return serveMcp(options);
|
|
70
|
+
const { renderer } = outputFor(this);
|
|
71
|
+
await serveMcpOverHttp(options, {
|
|
72
|
+
publicUrl: publicUrlOf(flags.publicUrl, {
|
|
73
|
+
missing: "--http",
|
|
74
|
+
reaches: `the browser apps reach ${IDENTITY.command}`,
|
|
75
|
+
}),
|
|
76
|
+
port: portOf(flags.port, MCP_HTTP_DEFAULT_PORT),
|
|
77
|
+
tokenFile,
|
|
78
|
+
background: env[`${IDENTITY.envPrefix}_MCP_HTTP`] !== undefined,
|
|
79
|
+
note: (line) => renderer.note(line),
|
|
34
80
|
});
|
|
35
81
|
});
|
|
36
82
|
//# sourceMappingURL=mcp.js.map
|
package/dist/commands/mcp.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../../src/commands/mcp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAC5C,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AACtE,OAAO,
|
|
1
|
+
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../../src/commands/mcp.ts"],"names":[],"mappings":"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"}
|
package/dist/mcp.d.ts
CHANGED
|
@@ -16,4 +16,14 @@ export interface McpOptions {
|
|
|
16
16
|
export declare const createMcpServer: (options: McpOptions) => McpServer;
|
|
17
17
|
/** Serves until the client closes the connection or the command is stopped. */
|
|
18
18
|
export declare const serveMcp: (options: McpOptions) => Promise<void>;
|
|
19
|
+
export interface HttpServing {
|
|
20
|
+
publicUrl: URL;
|
|
21
|
+
port: number;
|
|
22
|
+
tokenFile: string;
|
|
23
|
+
/** Started by `mcp server start`: its stderr is a log file. */
|
|
24
|
+
background: boolean;
|
|
25
|
+
note: (line: string) => void;
|
|
26
|
+
}
|
|
27
|
+
/** Serves on 127.0.0.1 behind the owner's tunnel until the command is stopped (WC-MCP-12). */
|
|
28
|
+
export declare const serveMcpOverHttp: (options: McpOptions, http: HttpServing) => Promise<void>;
|
|
19
29
|
//# sourceMappingURL=mcp.d.ts.map
|
package/dist/mcp.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAGxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAGxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAUvD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAA;AAGpD,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAA;IAChC,gGAAgG;IAChG,MAAM,EAAE,MAAM,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAA;IAC7C,SAAS,EAAE,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,OAAO,CAAA;IAC/C,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,UAAU,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IACjE,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,SAAS,CAAC,EAAE,UAAU,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;CAChD;AAOD,6GAA6G;AAC7G,eAAO,MAAM,eAAe,YAAa,UAAU,KAAG,SAwCrD,CAAA;AAED,+EAA+E;AAC/E,eAAO,MAAM,QAAQ,YAAmB,UAAU,KAAG,OAAO,CAAC,IAAI,CAkBhE,CAAA;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,GAAG,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,+DAA+D;IAC/D,UAAU,EAAE,OAAO,CAAA;IACnB,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAC7B;AAED,8FAA8F;AAC9F,eAAO,MAAM,gBAAgB,YAAmB,UAAU,QAAQ,WAAW,KAAG,OAAO,CAAC,IAAI,CAgC3F,CAAA"}
|
package/dist/mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
3
|
-
import { mcpErrorResult } from "@wirecat/cli-base";
|
|
4
|
-
import { registerCommands, runCommandForAgent, toolsFromCommands } from "@wirecat/cli-core/mcp";
|
|
3
|
+
import { CliError, mcpErrorResult } from "@wirecat/cli-base";
|
|
4
|
+
import { httpLockFile, MCP_PATH, registerCommands, runCommandForAgent, serveOverHttp, toolsFromCommands, } from "@wirecat/cli-core/mcp";
|
|
5
5
|
import { IDENTITY } from "./identity.js";
|
|
6
6
|
const EXCLUDED = [["mcp"]];
|
|
7
7
|
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -64,4 +64,35 @@ export const serveMcp = async (options) => {
|
|
|
64
64
|
await server.close();
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
|
+
/** Serves on 127.0.0.1 behind the owner's tunnel until the command is stopped (WC-MCP-12). */
|
|
68
|
+
export const serveMcpOverHttp = async (options, http) => {
|
|
69
|
+
const listening = await serveOverHttp(() => createMcpServer(options), {
|
|
70
|
+
publicUrl: http.publicUrl,
|
|
71
|
+
port: http.port,
|
|
72
|
+
tokenFile: http.tokenFile,
|
|
73
|
+
lockFile: httpLockFile(http.tokenFile),
|
|
74
|
+
version: IDENTITY.version,
|
|
75
|
+
appName: IDENTITY.command,
|
|
76
|
+
// A code in a log file outlives its use; in the background it goes only to the lock that status reads.
|
|
77
|
+
onCode: (code, expires) => http.note(http.background
|
|
78
|
+
? `a new login code for a browser app: \`${IDENTITY.command} ${options.profile} mcp server status\``
|
|
79
|
+
: `login code for a new browser app: ${code} (until ${expires.toTimeString().slice(0, 5)}; a new one after each login)`),
|
|
80
|
+
onError: (error) => http.note(`mcp: ${error.message}`),
|
|
81
|
+
}).catch((error) => {
|
|
82
|
+
if (error instanceof Error && "code" in error && error.code === "EADDRINUSE")
|
|
83
|
+
throw new CliError("configuration_error", `port ${http.port} is in use — pass another with --port`);
|
|
84
|
+
throw error;
|
|
85
|
+
});
|
|
86
|
+
http.note(`serving on ${listening.url.href} — point your tunnel at it; connectors use ${new URL(MCP_PATH, http.publicUrl).href}`);
|
|
87
|
+
try {
|
|
88
|
+
await new Promise((resolve) => {
|
|
89
|
+
options.signal?.addEventListener("abort", () => resolve(), { once: true });
|
|
90
|
+
if (options.signal?.aborted)
|
|
91
|
+
resolve();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
await listening.close();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
67
98
|
//# sourceMappingURL=mcp.js.map
|
package/dist/mcp.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AACxD,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAA;AACzE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"mcp.js","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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"}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.8.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.8.0";
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
package/docs/commands.md
CHANGED
|
@@ -125,12 +125,65 @@ serve this profile's commands to an agent over MCP, on stdin and stdout, writes
|
|
|
125
125
|
**Changes something in the service.**
|
|
126
126
|
|
|
127
127
|
```sh
|
|
128
|
-
clidev mcp
|
|
128
|
+
clidev mcp [options]
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
| Option | What it does |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `--http` | serve over HTTP on 127.0.0.1 for ChatGPT and Claude in the browser, behind your tunnel; the profile's permissions decide. |
|
|
134
|
+
| `--port <port>` | the local port for --http (default 8765). |
|
|
135
|
+
| `--public-url <url>` | the tunnel's https address the browser apps use, e.g. https://<name>.ts.net. |
|
|
136
|
+
| `--revoke` | forget every login given to a browser app; each must log in again. |
|
|
137
|
+
|
|
138
|
+
### `clidev mcp server`
|
|
139
|
+
|
|
140
|
+
start, stop and check `mcp --http` in the background; status shows the login code for a browser app
|
|
141
|
+
|
|
142
|
+
#### `clidev mcp server start`
|
|
143
|
+
|
|
144
|
+
start `mcp --http` apart from this shell and answer once it listens, with its login code
|
|
145
|
+
|
|
146
|
+
**Changes something on this computer only.**
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
clidev mcp server start [options]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
| Option | What it does |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `--port <port>` | the local port (default 8765). |
|
|
155
|
+
| `--public-url <url>` | the tunnel's https address the browser apps use, e.g. https://<name>.ts.net. |
|
|
156
|
+
| `--permission <key=level>` | make a permission stricter for this server only; repeat for more keys. |
|
|
157
|
+
|
|
158
|
+
**Returns:** start the HTTP MCP server in the background
|
|
159
|
+
|
|
160
|
+
**Example:** `clidev personal mcp server start --public-url https://laptop.example.ts.net`
|
|
161
|
+
|
|
162
|
+
#### `clidev mcp server stop`
|
|
163
|
+
|
|
164
|
+
stop `mcp --http` — only one that `mcp server start` started
|
|
165
|
+
|
|
166
|
+
**Changes something on this computer only.**
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
clidev mcp server stop
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Returns:** stop the background HTTP MCP server
|
|
173
|
+
|
|
174
|
+
**Example:** `clidev personal mcp server stop`
|
|
175
|
+
|
|
176
|
+
#### `clidev mcp server status`
|
|
177
|
+
|
|
178
|
+
whether `mcp --http` runs, on which port and address, the login code for a new browser app, and the apps logged in
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
clidev mcp server status
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Returns:** whether the HTTP MCP server runs, its login code and the apps logged in
|
|
132
185
|
|
|
133
|
-
**Example:** `clidev personal mcp`
|
|
186
|
+
**Example:** `clidev personal mcp server status --json`
|
|
134
187
|
|
|
135
188
|
## `clidev upgrade`
|
|
136
189
|
|
|
@@ -5,9 +5,9 @@ agent's context. Give it as written.
|
|
|
5
5
|
|
|
6
6
|
## Prompt
|
|
7
7
|
|
|
8
|
-
You audit a WireCat CLI repository, in the current folder. Use only
|
|
9
|
-
`node_modules/@wirecat/cli-base/docs/standards/README.md`,
|
|
10
|
-
or earlier reviews. Run `pnpm install` first if `node_modules` is missing.
|
|
8
|
+
You audit a WireCat CLI repository, in the current folder. Use only three sources: the handbook, starting at
|
|
9
|
+
`node_modules/@wirecat/cli-base/docs/standards/README.md`, the repository itself, and the cli-base workflow files its
|
|
10
|
+
own workflows pin (WC-CI-8). Do not read plans, journals or earlier reviews. Run `pnpm install` first if `node_modules` is missing.
|
|
11
11
|
|
|
12
12
|
Check the repository against every handbook page that applies to it: the code, the docs, the configs and the
|
|
13
13
|
workflows. You may run the package scripts. Change nothing.
|
package/docs/dev/test-matrix.md
CHANGED
|
@@ -7,7 +7,7 @@ ran it · ⛔ not tested offline, with the reason and where it is checked instea
|
|
|
7
7
|
Measured from the test run (`coverage/argv.jsonl`), not searched for — see
|
|
8
8
|
[TESTING.md](TESTING.md) for how, and for the states and failures that cut across commands.
|
|
9
9
|
|
|
10
|
-
**
|
|
10
|
+
**50 ✅ · 7 ⛔ · 0 ❌** — 18 commands, 39 options.
|
|
11
11
|
|
|
12
12
|
| Command | Option | | Note |
|
|
13
13
|
|---|---|---|---|
|
|
@@ -20,6 +20,16 @@ Measured from the test run (`coverage/argv.jsonl`), not searched for — see
|
|
|
20
20
|
| `skill install` | `--for` | ✅ | |
|
|
21
21
|
| `skill status` | | ✅ | |
|
|
22
22
|
| `mcp` | | ✅ | |
|
|
23
|
+
| `mcp` | `--http` | ✅ | |
|
|
24
|
+
| `mcp` | `--port` | ✅ | |
|
|
25
|
+
| `mcp` | `--public-url` | ✅ | |
|
|
26
|
+
| `mcp` | `--revoke` | ✅ | |
|
|
27
|
+
| `mcp server start` | | ⛔ | starts a detached `mcp --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/ |
|
|
28
|
+
| `mcp server start` | `--port` | ⛔ | starts a detached `mcp --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/ |
|
|
29
|
+
| `mcp server start` | `--public-url` | ⛔ | starts a detached `mcp --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/ |
|
|
30
|
+
| `mcp server start` | `--permission` | ⛔ | cli-core adds it and passes it to `mcp --http`, which no longer takes it, so the started server refuses it; checked by hand against dist/ |
|
|
31
|
+
| `mcp server stop` | | ⛔ | starts a detached `mcp --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/ |
|
|
32
|
+
| `mcp server status` | | ✅ | |
|
|
23
33
|
| `upgrade` | | ✅ | |
|
|
24
34
|
| `upgrade` | `--check` | ✅ | |
|
|
25
35
|
| `config show` | | ✅ | |
|
package/docs/usage.md
CHANGED
|
@@ -15,7 +15,7 @@ command without one stops with exit code 2 and lists the profiles on this comput
|
|
|
15
15
|
|
|
16
16
|
## Output for scripts
|
|
17
17
|
|
|
18
|
-
Every command except `mcp` takes `--json`, and prints one JSON value on stdout. Errors are JSON on stderr, with a code to branch
|
|
18
|
+
Every command except `mcp` on stdin and stdout takes `--json`, and prints one JSON value on stdout. Errors are JSON on stderr, with a code to branch
|
|
19
19
|
on. The [command reference](commands.md) lists the codes.
|
|
20
20
|
|
|
21
21
|
## Agents
|
|
@@ -30,3 +30,9 @@ claude mcp add clidev -- clidev personal mcp
|
|
|
30
30
|
|
|
31
31
|
The server offers every command, the ones that write included. The profile's permissions decide what runs; see
|
|
32
32
|
[settings](configuration.md#permissions).
|
|
33
|
+
|
|
34
|
+
For ChatGPT and Claude in the browser, `clidev personal mcp --http --public-url https://laptop.example.ts.net` serves on
|
|
35
|
+
`127.0.0.1` (port 8765, or `--port`) behind your own tunnel. A browser app logs in with a one-time code that the
|
|
36
|
+
command prints; `clidev personal mcp --revoke` ends every login. `clidev personal mcp server start` runs the same
|
|
37
|
+
server in the background, `mcp server status` shows its port, the login code and the apps logged in, and
|
|
38
|
+
`mcp server stop` ends it.
|
package/kit/dist/interview.js
CHANGED
|
@@ -6,13 +6,13 @@ export const NAME_QUESTIONS = [
|
|
|
6
6
|
{
|
|
7
7
|
key: "command",
|
|
8
8
|
flag: "command",
|
|
9
|
-
text: "The command word people type (lowercase, e.g.
|
|
9
|
+
text: "The command word people type (lowercase, e.g. gcal)",
|
|
10
10
|
check: (value) => commandWord.test(value) ? undefined : "lowercase letters, digits and dashes, starting with a letter",
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
key: "package",
|
|
14
14
|
flag: "package",
|
|
15
|
-
text: "The npm package name (e.g. @wirecat/
|
|
15
|
+
text: "The npm package name (e.g. @wirecat/gcal-cli)",
|
|
16
16
|
check: (value) => (npmName.test(value) ? undefined : "a valid npm package name, scoped or not"),
|
|
17
17
|
},
|
|
18
18
|
{
|
package/kit/dist/new.js
CHANGED
|
@@ -25,17 +25,18 @@ export const speechModels = (languages) => {
|
|
|
25
25
|
return ["parakeet-v3"];
|
|
26
26
|
return undefined;
|
|
27
27
|
};
|
|
28
|
-
|
|
28
|
+
// Speech models transcribe voice messages, so only a messenger needs them.
|
|
29
|
+
export const languageWarnings = (languages, messenger) => languages.flatMap((lang) => [
|
|
29
30
|
...(STEMMED.includes(baseLang(lang))
|
|
30
31
|
? []
|
|
31
32
|
: [`${lang}: cli-core has no search stemmer for it; search matches whole words only`]),
|
|
32
|
-
...(PARAKEET.includes(baseLang(lang)) ? [
|
|
33
|
+
...(messenger && !PARAKEET.includes(baseLang(lang)) ? [`${lang}: no speech model in cli-core hears it`] : []),
|
|
33
34
|
]);
|
|
34
35
|
const NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
35
36
|
const NAME_QUESTION = {
|
|
36
37
|
key: "name",
|
|
37
38
|
flag: "name",
|
|
38
|
-
text: "The folder (and repository) name, e.g.
|
|
39
|
+
text: "The folder (and repository) name, e.g. gcal-cli",
|
|
39
40
|
check: (value) => (NAME_PATTERN.test(value) ? undefined : "lowercase letters, digits, dots and dashes"),
|
|
40
41
|
};
|
|
41
42
|
const collect = async (args, io) => {
|
|
@@ -97,8 +98,9 @@ export const withReference = (agents, category) => {
|
|
|
97
98
|
export const designDoc = (names, answers) => {
|
|
98
99
|
const rows = INTERVIEW.map((question) => `- **${question.title ?? question.text}:** ${(answers[question.key] ?? []).map((value) => labelOf(question, value)).join(", ")}`);
|
|
99
100
|
const languages = answers.contentLang ?? [];
|
|
101
|
+
const messenger = answers.category?.[0] === "messenger";
|
|
100
102
|
const speech = speechModels(languages);
|
|
101
|
-
const warnings = languageWarnings(languages);
|
|
103
|
+
const warnings = languageWarnings(languages, messenger);
|
|
102
104
|
return [
|
|
103
105
|
`# ${names.command} — design`,
|
|
104
106
|
"",
|
|
@@ -116,9 +118,13 @@ export const designDoc = (names, answers) => {
|
|
|
116
118
|
"## Content languages",
|
|
117
119
|
"",
|
|
118
120
|
`Languages: ${languages.join(", ")}.`,
|
|
119
|
-
|
|
120
|
-
?
|
|
121
|
-
|
|
121
|
+
...(messenger
|
|
122
|
+
? [
|
|
123
|
+
speech
|
|
124
|
+
? `Set the messenger's \`speechModels\` to ${speech.map((model) => `\`${model}\``).join(", ")}.`
|
|
125
|
+
: "No speech model in cli-core covers all of these languages: leave `speechModels` out.",
|
|
126
|
+
]
|
|
127
|
+
: []),
|
|
122
128
|
...warnings.map((warning) => `- ${warning}`),
|
|
123
129
|
"",
|
|
124
130
|
"## Further answers",
|
|
@@ -220,7 +226,7 @@ export const markExecutable = async (cwd, paths) => {
|
|
|
220
226
|
export const manualSteps = (names) => [
|
|
221
227
|
`1. Create the repository ${names.owner}/${names.name} and its main ruleset (cli-base docs/standards/new-cli.md).`,
|
|
222
228
|
`2. Set npm trusted publishing for ${names.package}, naming release.yml (WC-REL-11), and add any secrets.`,
|
|
223
|
-
"3. Give .github/workflows/monthly.yml a cron minute
|
|
229
|
+
"3. Give .github/workflows/monthly.yml a free cron minute from the list in cli-base docs/standards/ci.md (WC-CI-9).",
|
|
224
230
|
"4. WireCat CLIs only: a pull request to cli-base's PARITY_GROUPS, a line in cli-core's WIRECAT_TOOLS and in cli-docs' tools.json.",
|
|
225
231
|
];
|
|
226
232
|
export const runNew = async (cwd, source, args, io) => {
|
|
@@ -261,7 +267,7 @@ export const runNew = async (cwd, source, args, io) => {
|
|
|
261
267
|
await writeOut(target, MANIFEST, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
262
268
|
await run("git", ["init", "-q", "-b", "main"], { cwd: target });
|
|
263
269
|
await markExecutable(target, [...rendered].filter(([, file]) => file.mode & 0o111).map(([path]) => path));
|
|
264
|
-
const warnings = languageWarnings(answers.contentLang ?? []);
|
|
270
|
+
const warnings = languageWarnings(answers.contentLang ?? [], category === "messenger");
|
|
265
271
|
return [
|
|
266
272
|
`Created ${target} from ${source.name}@${source.version}.`,
|
|
267
273
|
...(warnings.length ? ["", "Content languages:", ...warnings.map((warning) => ` ${warning}`)] : []),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-template",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Start a new WireCat CLI: a working skeleton on cli-base and cli-core, copied by new and kept current by upgrade",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|
|
@@ -84,8 +84,8 @@
|
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
86
|
"@modelcontextprotocol/server": "2.4.0",
|
|
87
|
-
"@wirecat/cli-base": "0.
|
|
88
|
-
"@wirecat/cli-core": "0.
|
|
87
|
+
"@wirecat/cli-base": "0.62.0",
|
|
88
|
+
"@wirecat/cli-core": "0.449.0",
|
|
89
89
|
"commander": "15.0.0"
|
|
90
90
|
},
|
|
91
91
|
"devDependencies": {
|
|
@@ -9,4 +9,32 @@ export const UNTESTED: { command: string; option?: string; reason: string }[] =
|
|
|
9
9
|
option: "--version",
|
|
10
10
|
reason: "Commander answers before any command runs, so the trace never sees it; `pnpm version:check` checks it",
|
|
11
11
|
},
|
|
12
|
+
{
|
|
13
|
+
command: "mcp server start",
|
|
14
|
+
reason:
|
|
15
|
+
"starts a detached `mcp --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
command: "mcp server start",
|
|
19
|
+
option: "--port",
|
|
20
|
+
reason:
|
|
21
|
+
"starts a detached `mcp --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
command: "mcp server start",
|
|
25
|
+
option: "--public-url",
|
|
26
|
+
reason:
|
|
27
|
+
"starts a detached `mcp --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
command: "mcp server start",
|
|
31
|
+
option: "--permission",
|
|
32
|
+
reason:
|
|
33
|
+
"cli-core adds it and passes it to `mcp --http`, which no longer takes it, so the started server refuses it; checked by hand against dist/",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
command: "mcp server stop",
|
|
37
|
+
reason:
|
|
38
|
+
"starts a detached `mcp --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/",
|
|
39
|
+
},
|
|
12
40
|
]
|
package/src/commands/mcp.ts
CHANGED
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base"
|
|
2
2
|
import { annotate, describeProgram } from "@wirecat/cli-base/commands"
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
changesAuthorityAt,
|
|
5
|
+
environmentOf,
|
|
6
|
+
type GlobalFlags,
|
|
7
|
+
httpTokenFile,
|
|
8
|
+
itemOf,
|
|
9
|
+
MCP_HTTP_DEFAULT_PORT,
|
|
10
|
+
mcpServerCommand,
|
|
11
|
+
outputFor,
|
|
12
|
+
REMOTE_WRITE,
|
|
13
|
+
refuseUnderReadOnly,
|
|
14
|
+
renderAnswer,
|
|
15
|
+
rootOf,
|
|
16
|
+
} from "@wirecat/cli-core/cli"
|
|
17
|
+
import { portOf, publicUrlOf } from "@wirecat/cli-core/mcp"
|
|
4
18
|
import { Command } from "commander"
|
|
5
19
|
import { configuration } from "../config.js"
|
|
6
20
|
import { IDENTITY } from "../identity.js"
|
|
7
21
|
|
|
22
|
+
interface McpFlags extends GlobalFlags {
|
|
23
|
+
http?: boolean
|
|
24
|
+
port?: string
|
|
25
|
+
publicUrl?: string
|
|
26
|
+
revoke?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const settingsOf = (command: Command) => {
|
|
30
|
+
const flags = command.optsWithGlobals<McpFlags>()
|
|
31
|
+
const env = environmentOf(command).env ?? process.env
|
|
32
|
+
const { profile } = configuration.resolveSettings(flags, { env })
|
|
33
|
+
return { flags, env, profile, tokenFile: httpTokenFile(IDENTITY, profile, env) }
|
|
34
|
+
}
|
|
35
|
+
|
|
8
36
|
export const mcpCommand = (): Command =>
|
|
9
37
|
annotate(new Command("mcp"), {
|
|
10
38
|
effects: REMOTE_WRITE,
|
|
@@ -16,23 +44,66 @@ export const mcpCommand = (): Command =>
|
|
|
16
44
|
.description(
|
|
17
45
|
`serve this profile's commands to an agent over MCP, on stdin and stdout, writes included; the profile's permissions decide — \`claude mcp add ${IDENTITY.command} -- ${IDENTITY.command} personal mcp\``,
|
|
18
46
|
)
|
|
47
|
+
.option(
|
|
48
|
+
"--http",
|
|
49
|
+
"serve over HTTP on 127.0.0.1 for ChatGPT and Claude in the browser, behind your tunnel; the profile's permissions decide",
|
|
50
|
+
)
|
|
51
|
+
.option("--port <port>", `the local port for --http (default ${MCP_HTTP_DEFAULT_PORT})`)
|
|
52
|
+
.option("--public-url <url>", "the tunnel's https address the browser apps use, e.g. https://<name>.ts.net")
|
|
53
|
+
.option("--revoke", "forget every login given to a browser app; each must log in again")
|
|
54
|
+
.addCommand(
|
|
55
|
+
mcpServerCommand({
|
|
56
|
+
example: `${IDENTITY.command} personal`,
|
|
57
|
+
target: (self) => {
|
|
58
|
+
const { env, profile, tokenFile } = settingsOf(self)
|
|
59
|
+
return {
|
|
60
|
+
invocation: `${IDENTITY.command} ${profile}`,
|
|
61
|
+
profile,
|
|
62
|
+
envPrefix: IDENTITY.envPrefix,
|
|
63
|
+
env,
|
|
64
|
+
tokenFile,
|
|
65
|
+
argv: [profile, "mcp", "--http"],
|
|
66
|
+
version: IDENTITY.version,
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
answer: (self, item, lines) => renderAnswer(self, outputFor(self), itemOf(item), () => lines.join("\n")),
|
|
70
|
+
}),
|
|
71
|
+
)
|
|
19
72
|
.action(async function (this: Command) {
|
|
20
|
-
const flags = this
|
|
21
|
-
if (flags.
|
|
73
|
+
const { flags, env, profile, tokenFile } = settingsOf(this)
|
|
74
|
+
if (flags.revoke) {
|
|
75
|
+
refuseUnderReadOnly(this, "mcp --revoke")
|
|
76
|
+
const { revokeAll } = await import("@wirecat/cli-core/mcp")
|
|
77
|
+
revokeAll(tokenFile)
|
|
78
|
+
renderAnswer(this, outputFor(this), itemOf({ revoked: true, profile }))
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
if (!flags.http && (flags.json || flags.jsonl))
|
|
22
82
|
throw new CliError("validation_error", "MCP over stdio controls its own output; leave out --json and --jsonl")
|
|
23
83
|
const environment = environmentOf(this)
|
|
24
|
-
const env = environment.env ?? process.env
|
|
25
|
-
const { profile } = configuration.resolveSettings(flags, { env })
|
|
26
84
|
const run = environment.runCommand
|
|
27
85
|
if (!run) throw new Error("mcp runs inside the program's run(), which provides runCommand")
|
|
28
86
|
const program = rootOf(this)
|
|
29
|
-
const
|
|
30
|
-
await serveMcp({
|
|
87
|
+
const options = {
|
|
31
88
|
profile,
|
|
32
89
|
commands: describeProgram(program),
|
|
33
90
|
levels: () => configuration.resolveSettings(flags, { env }).permissions,
|
|
34
91
|
authority: changesAuthorityAt(program),
|
|
35
92
|
run,
|
|
36
93
|
...(environment.signal ? { signal: environment.signal } : {}),
|
|
94
|
+
}
|
|
95
|
+
// Loaded here, not at the top: every other command would otherwise pay for the MCP SDK.
|
|
96
|
+
const { serveMcp, serveMcpOverHttp } = await import("../mcp.js")
|
|
97
|
+
if (!flags.http) return serveMcp(options)
|
|
98
|
+
const { renderer } = outputFor(this)
|
|
99
|
+
await serveMcpOverHttp(options, {
|
|
100
|
+
publicUrl: publicUrlOf(flags.publicUrl, {
|
|
101
|
+
missing: "--http",
|
|
102
|
+
reaches: `the browser apps reach ${IDENTITY.command}`,
|
|
103
|
+
}),
|
|
104
|
+
port: portOf(flags.port, MCP_HTTP_DEFAULT_PORT),
|
|
105
|
+
tokenFile,
|
|
106
|
+
background: env[`${IDENTITY.envPrefix}_MCP_HTTP`] !== undefined,
|
|
107
|
+
note: (line) => renderer.note(line),
|
|
37
108
|
})
|
|
38
109
|
})
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { createServer } from "node:net"
|
|
2
|
+
import {
|
|
3
|
+
Client,
|
|
4
|
+
type OAuthClientInformationMixed,
|
|
5
|
+
type OAuthClientProvider,
|
|
6
|
+
type OAuthTokens,
|
|
7
|
+
StreamableHTTPClientTransport,
|
|
8
|
+
UnauthorizedError,
|
|
9
|
+
} from "@modelcontextprotocol/client"
|
|
10
|
+
import { expect, it, vi } from "vitest"
|
|
11
|
+
import { run } from "./program.js"
|
|
12
|
+
|
|
13
|
+
const started = (argv: string[], signal?: AbortSignal, env?: NodeJS.ProcessEnv) => {
|
|
14
|
+
const out: string[] = []
|
|
15
|
+
const err: string[] = []
|
|
16
|
+
const code = run(argv, {
|
|
17
|
+
streams: { data: (text) => out.push(text), diagnostic: (text) => err.push(text) },
|
|
18
|
+
tty: false,
|
|
19
|
+
...(signal ? { signal } : {}),
|
|
20
|
+
...(env ? { env } : {}),
|
|
21
|
+
})
|
|
22
|
+
return { code, out, err }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const answer = async (argv: string[]) => {
|
|
26
|
+
const one = started(argv)
|
|
27
|
+
expect(await one.code).toBe(0)
|
|
28
|
+
return JSON.parse(one.out.join("")).item
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const REDIRECT = "http://localhost/callback"
|
|
32
|
+
|
|
33
|
+
/** Logs in the way a browser app does: registers, types the terminal's code on the consent page, swaps it for tokens. */
|
|
34
|
+
const loggedInClient = async (base: URL, loginCode: () => string) => {
|
|
35
|
+
const endpoint = new URL("/mcp", base)
|
|
36
|
+
const saved: { info?: OAuthClientInformationMixed; tokens?: OAuthTokens; verifier?: string; consentAt?: URL } = {}
|
|
37
|
+
const authProvider: OAuthClientProvider = {
|
|
38
|
+
get redirectUrl() {
|
|
39
|
+
return REDIRECT
|
|
40
|
+
},
|
|
41
|
+
get clientMetadata() {
|
|
42
|
+
return {
|
|
43
|
+
client_name: "Test app",
|
|
44
|
+
redirect_uris: [REDIRECT],
|
|
45
|
+
token_endpoint_auth_method: "none",
|
|
46
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
47
|
+
response_types: ["code"],
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
clientInformation: () => saved.info,
|
|
51
|
+
saveClientInformation: (info) => {
|
|
52
|
+
saved.info = info
|
|
53
|
+
},
|
|
54
|
+
tokens: () => saved.tokens,
|
|
55
|
+
saveTokens: (tokens) => {
|
|
56
|
+
saved.tokens = tokens
|
|
57
|
+
},
|
|
58
|
+
redirectToAuthorization: (url) => {
|
|
59
|
+
saved.consentAt = url
|
|
60
|
+
},
|
|
61
|
+
saveCodeVerifier: (verifier) => {
|
|
62
|
+
saved.verifier = verifier
|
|
63
|
+
},
|
|
64
|
+
codeVerifier: () => saved.verifier ?? "",
|
|
65
|
+
}
|
|
66
|
+
const first = new StreamableHTTPClientTransport(endpoint, { authProvider })
|
|
67
|
+
try {
|
|
68
|
+
await new Client({ name: "test", version: "1" }).connect(first)
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (!(error instanceof UnauthorizedError)) throw error
|
|
71
|
+
}
|
|
72
|
+
if (!saved.consentAt) throw new Error("the server did not ask the client to log in")
|
|
73
|
+
const typed = new URLSearchParams(saved.consentAt.searchParams)
|
|
74
|
+
typed.set("login_code", loginCode())
|
|
75
|
+
const answer = await fetch(new URL("/authorize", base), { method: "POST", body: typed, redirect: "manual" })
|
|
76
|
+
const location = answer.headers.get("location")
|
|
77
|
+
if (answer.status !== 302 || !location) throw new Error(`consent refused: ${answer.status}`)
|
|
78
|
+
await first.finishAuth(new URL(location).searchParams.get("code") ?? "")
|
|
79
|
+
const client = new Client({ name: "test", version: "1" })
|
|
80
|
+
await client.connect(new StreamableHTTPClientTransport(endpoint, { authProvider }))
|
|
81
|
+
return client
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const freePort = () =>
|
|
85
|
+
new Promise<number>((resolve, reject) => {
|
|
86
|
+
const probe = createServer()
|
|
87
|
+
probe.once("error", reject)
|
|
88
|
+
probe.listen(0, "127.0.0.1", () => {
|
|
89
|
+
const address = probe.address()
|
|
90
|
+
probe.close(() => (typeof address === "object" && address ? resolve(address.port) : reject(new Error("no port"))))
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it("serves the three tools behind the browser login, and shows the server in mcp server status until it stops, and frees the lock when it stops", async () => {
|
|
95
|
+
const port = await freePort()
|
|
96
|
+
const base = new URL(`http://127.0.0.1:${port}`)
|
|
97
|
+
const stop = new AbortController()
|
|
98
|
+
const serving = started(["personal", "mcp", "--http", "--public-url", base.href, "--port", String(port)], stop.signal)
|
|
99
|
+
await vi.waitFor(() => expect(serving.err.join("\n")).toContain("serving on"))
|
|
100
|
+
const codes = () => [...serving.err.join("\n").matchAll(/login code for a new browser app: (\S+)/g)].map((m) => m[1])
|
|
101
|
+
const client = await loggedInClient(base, () => codes()[0] ?? "")
|
|
102
|
+
try {
|
|
103
|
+
expect((await client.listTools()).tools.map((tool) => tool.name).sort()).toEqual([
|
|
104
|
+
"clidev_read",
|
|
105
|
+
"clidev_tools_search",
|
|
106
|
+
"clidev_write",
|
|
107
|
+
])
|
|
108
|
+
expect(await answer(["personal", "mcp", "server", "status", "--json"])).toMatchObject({
|
|
109
|
+
profile: "personal",
|
|
110
|
+
running: true,
|
|
111
|
+
port,
|
|
112
|
+
connector_url: new URL("/mcp", base).href,
|
|
113
|
+
login_code: { code: codes().at(-1) },
|
|
114
|
+
clients: [expect.objectContaining({ claimed_name: "Test app" })],
|
|
115
|
+
})
|
|
116
|
+
} finally {
|
|
117
|
+
await client.close()
|
|
118
|
+
stop.abort()
|
|
119
|
+
expect(await serving.code).toBe(0)
|
|
120
|
+
}
|
|
121
|
+
expect(await answer(["personal", "mcp", "server", "status", "--json"])).toMatchObject({ running: false })
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it("refuses mcp --http without the tunnel's address", async () => {
|
|
125
|
+
const one = started(["personal", "mcp", "--http", "--json"])
|
|
126
|
+
expect(await one.code).not.toBe(0)
|
|
127
|
+
expect(one.err.join("\n")).toContain("--public-url")
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it("forgets every browser login on --revoke", async () => {
|
|
131
|
+
expect(await answer(["personal", "mcp", "--revoke", "--json"])).toEqual({ revoked: true, profile: "personal" })
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it("keeps the login code out of a background server's log, and refuses a port in use", async () => {
|
|
135
|
+
const port = await freePort()
|
|
136
|
+
const url = `http://127.0.0.1:${port}`
|
|
137
|
+
const stop = new AbortController()
|
|
138
|
+
const background = started(["personal", "mcp", "--http", "--public-url", url, "--port", String(port)], stop.signal, {
|
|
139
|
+
...process.env,
|
|
140
|
+
CLIDEV_MCP_HTTP: "personal",
|
|
141
|
+
})
|
|
142
|
+
try {
|
|
143
|
+
await vi.waitFor(() => expect(background.err.join("\n")).toContain("serving on"))
|
|
144
|
+
expect(background.err.join("\n")).toContain(
|
|
145
|
+
"a new login code for a browser app: `clidev personal mcp server status`",
|
|
146
|
+
)
|
|
147
|
+
const second = started(["work", "mcp", "--http", "--public-url", url, "--port", String(port), "--json"])
|
|
148
|
+
expect(await second.code).not.toBe(0)
|
|
149
|
+
expect(second.err.join("\n")).toContain(`port ${port} is in use`)
|
|
150
|
+
} finally {
|
|
151
|
+
stop.abort()
|
|
152
|
+
await background.code
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it("refuses --json for MCP over stdio", async () => {
|
|
157
|
+
const one = started(["personal", "mcp", "--json"])
|
|
158
|
+
expect(await one.code).toBe(2)
|
|
159
|
+
expect(one.err.join("\n")).toContain("MCP over stdio controls its own output")
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it("refuses --revoke under the read-only lock", async () => {
|
|
163
|
+
const one = started(["personal", "mcp", "--revoke", "--json"], undefined, { ...process.env, CLIDEV_READ_ONLY: "1" })
|
|
164
|
+
expect(await one.code).toBe(5)
|
|
165
|
+
})
|
package/src/mcp.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/server"
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"
|
|
3
|
-
import { mcpErrorResult } from "@wirecat/cli-base"
|
|
3
|
+
import { CliError, mcpErrorResult } from "@wirecat/cli-base"
|
|
4
4
|
import type { CommandInfo } from "@wirecat/cli-base/commands"
|
|
5
5
|
import type { RunOptions } from "@wirecat/cli-core/cli"
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
httpLockFile,
|
|
8
|
+
MCP_PATH,
|
|
9
|
+
type McpCommand,
|
|
10
|
+
registerCommands,
|
|
11
|
+
runCommandForAgent,
|
|
12
|
+
serveOverHttp,
|
|
13
|
+
toolsFromCommands,
|
|
14
|
+
} from "@wirecat/cli-core/mcp"
|
|
7
15
|
import type { Level } from "@wirecat/cli-core/sends"
|
|
8
16
|
import { IDENTITY } from "./identity.js"
|
|
9
17
|
|
|
@@ -86,3 +94,47 @@ export const serveMcp = async (options: McpOptions): Promise<void> => {
|
|
|
86
94
|
await server.close()
|
|
87
95
|
}
|
|
88
96
|
}
|
|
97
|
+
|
|
98
|
+
export interface HttpServing {
|
|
99
|
+
publicUrl: URL
|
|
100
|
+
port: number
|
|
101
|
+
tokenFile: string
|
|
102
|
+
/** Started by `mcp server start`: its stderr is a log file. */
|
|
103
|
+
background: boolean
|
|
104
|
+
note: (line: string) => void
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Serves on 127.0.0.1 behind the owner's tunnel until the command is stopped (WC-MCP-12). */
|
|
108
|
+
export const serveMcpOverHttp = async (options: McpOptions, http: HttpServing): Promise<void> => {
|
|
109
|
+
const listening = await serveOverHttp(() => createMcpServer(options), {
|
|
110
|
+
publicUrl: http.publicUrl,
|
|
111
|
+
port: http.port,
|
|
112
|
+
tokenFile: http.tokenFile,
|
|
113
|
+
lockFile: httpLockFile(http.tokenFile),
|
|
114
|
+
version: IDENTITY.version,
|
|
115
|
+
appName: IDENTITY.command,
|
|
116
|
+
// A code in a log file outlives its use; in the background it goes only to the lock that status reads.
|
|
117
|
+
onCode: (code, expires) =>
|
|
118
|
+
http.note(
|
|
119
|
+
http.background
|
|
120
|
+
? `a new login code for a browser app: \`${IDENTITY.command} ${options.profile} mcp server status\``
|
|
121
|
+
: `login code for a new browser app: ${code} (until ${expires.toTimeString().slice(0, 5)}; a new one after each login)`,
|
|
122
|
+
),
|
|
123
|
+
onError: (error) => http.note(`mcp: ${error.message}`),
|
|
124
|
+
}).catch((error: unknown) => {
|
|
125
|
+
if (error instanceof Error && "code" in error && error.code === "EADDRINUSE")
|
|
126
|
+
throw new CliError("configuration_error", `port ${http.port} is in use — pass another with --port`)
|
|
127
|
+
throw error
|
|
128
|
+
})
|
|
129
|
+
http.note(
|
|
130
|
+
`serving on ${listening.url.href} — point your tunnel at it; connectors use ${new URL(MCP_PATH, http.publicUrl).href}`,
|
|
131
|
+
)
|
|
132
|
+
try {
|
|
133
|
+
await new Promise<void>((resolve) => {
|
|
134
|
+
options.signal?.addEventListener("abort", () => resolve(), { once: true })
|
|
135
|
+
if (options.signal?.aborted) resolve()
|
|
136
|
+
})
|
|
137
|
+
} finally {
|
|
138
|
+
await listening.close()
|
|
139
|
+
}
|
|
140
|
+
}
|
package/src/roots.test.ts
CHANGED
|
@@ -77,13 +77,18 @@ it("skill status and skill show --for read what skill install wrote", async () =
|
|
|
77
77
|
expect((await ran(["skill", "install", "--for", "claude", "--json"])).code).toBe(0)
|
|
78
78
|
const status = await ran(["skill", "status", "--json"])
|
|
79
79
|
expect(status.code).toBe(0)
|
|
80
|
-
|
|
80
|
+
// cli-core #2059 wraps skill answers in `{ item, meta }`; accept both until this CLI pins the release that ships it.
|
|
81
|
+
const answer = (out: string) => {
|
|
82
|
+
const body = JSON.parse(out)
|
|
83
|
+
return body.item ?? body
|
|
84
|
+
}
|
|
85
|
+
expect(answer(status.out).targets).toEqual([
|
|
81
86
|
expect.objectContaining({ target: "claude", state: "current" }),
|
|
82
87
|
expect.objectContaining({ target: "agents", installed: null, state: "missing" }),
|
|
83
88
|
])
|
|
84
89
|
const shown = await ran(["skill", "show", "--for", "claude", "--json"])
|
|
85
90
|
expect(shown.code).toBe(0)
|
|
86
|
-
expect(
|
|
91
|
+
expect(answer(shown.out)).toMatchObject({
|
|
87
92
|
target: "claude",
|
|
88
93
|
content: expect.stringContaining("name: clidev-cli"),
|
|
89
94
|
})
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.8.0"
|