After v0.14.3 npm-publish failed again with 401 Unauthorized despite
path-scoped _authToken. Direct curl probe to keigit confirmed BOTH Bearer
and Basic auth schemes work — so the issue is npm 10 not sending the
auth header in CI. Likely cause: deprecated `always-auth=true` interfered
with token resolution.
== Publish auth fix ==
- Drop `always-auth=true` (deprecated in npm 10+; warns in logs)
- Keep path-scoped `_authToken` (npm 10 canonical)
- Add legacy Basic-auth fallback rows (username/_password/email) — Forgejo
accepts both schemes per direct probe; if one resolution path fails,
npm tries the other
- chmod 600 on $HOME/.npmrc and project .npmrc (defense-in-depth)
- Bump 0.14.3 → 0.14.4
== Slice A — TS server hardening (Sonnet code-implementer-typescript) ==
File: _ts_packages/packages/mcp-server/src/server.ts (+3/-1)
File: _ts_packages/packages/mcp-server/src/index.ts (+14/-4)
- safeEqual constant-time path on length mismatch (timing oracle close)
- HTTP server defaults to 127.0.0.1 bind; --bind <addr> opt-in for 0.0.0.0
- Body cap 1 MiB with 413 response (DoS prevention)
- VERIFIED: tsc -b --noEmit exit 0
== Slice B — Outcome-only profile hardening (Sonnet code-implementer) ==
Files: install.sh, install/lib-args.sh, install/lib-profile-outcome-only.sh
- Confirm-screen gate before destructive install (skips on --dry-run / --yes)
- _outcome_install_ledger return value tracked → summary reflects reality
(was: false-success "ledger: ..." when init failed)
- --dry-run silent-ignored on non-outcome profiles → now warns
- VERIFIED: end-to-end smoke against fake $HOME with `<<< "y"` — all 5
files installed, schema v9 + 2 triggers, summary correct
== Slice D — jq-merge dedup tuple (Sonnet code-implementer) ==
File: install/lib-hooks.sh
- Replaced `unique_by(.command)` with reduce-into-object keyed on
norm-ed command (tilde-vs-absolute path collision fix)
- Snippet-wins precedence on collision
- 3 manual scenario traces pass: tilde+tilde, absolute+tilde, idempotency
== Slice E — Doc honesty pass (Sonnet code-implementer, selective-merged) ==
Files: README.md, docs/{INSTALL,ARCHITECTURE,PROFILE-OUTCOME-ONLY}.md
Note: Slice E worktree was based on an older main commit; merged
selectively to preserve current-main values (565 DNAs, not worktree's 518)
- README:62 plugin marketplace URL: KeiSei84/KeiSeiKit → KeiSei84/KeiSeiKit-1.0
(consistent with line 66 git clone URL + Cargo.toml repository field)
- README:9-15: per-claim [REAL: <command>] markers on all 8 numerics
- README:124-132 + PROFILE-OUTCOME-ONLY.md:43-55 + ARCHITECTURE.md:288-302:
rephrase 100-row router claim — now describes Wilson lower-bound
(δ=0.10, q*=0.70) continuous metric with file:line pointer to select.rs
- INSTALL.md: ESTIMATE-HTC marker covering all install-time / disk-size
numerics in profile table (RULE 0.18 compliance)
- PROFILE-OUTCOME-ONLY.md privacy section: discloses agent-toolstats.jsonl
sidecar (was undocumented per W3 finding)
- PROFILE-OUTCOME-ONLY.md uninstall: added 6th rm -f for .bak-* cleanup
(closes orphan-accumulation per W3+W4 audits)
[FROM-JOURNAL: tasks.jsonl this session — 12 audit agents waves 5+6 +
4 parallel fix-implementer worktrees ran ~25 min wall-time]
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
140 lines
4.8 KiB
JavaScript
140 lines
4.8 KiB
JavaScript
#!/usr/bin/env node
|
|
// Entry point: parse argv, select transport (stdio or HTTP), start McpServer.
|
|
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { McpServer } from "./server.js";
|
|
|
|
interface CliArgs {
|
|
stdio: boolean;
|
|
port?: number;
|
|
bind: string;
|
|
authTokenFile?: string;
|
|
rustBinDir: string;
|
|
}
|
|
|
|
function parseArgv(argv: readonly string[]): CliArgs {
|
|
const out: CliArgs = {
|
|
stdio: false,
|
|
bind: "127.0.0.1",
|
|
rustBinDir: process.env["KEI_RUST_BIN_DIR"] ?? defaultBinDir(),
|
|
};
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const a = argv[i];
|
|
if (a === "--stdio") out.stdio = true;
|
|
else if (a === "--port") out.port = Number(argv[++i] ?? "");
|
|
else if (a === "--bind") {
|
|
const v = argv[++i];
|
|
if (v !== undefined) out.bind = v;
|
|
} else if (a === "--auth-token-file") {
|
|
const v = argv[++i];
|
|
if (v !== undefined) out.authTokenFile = v;
|
|
} else if (a === "--rust-bin-dir") {
|
|
const v = argv[++i];
|
|
if (v !== undefined) out.rustBinDir = v;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function defaultBinDir(): string {
|
|
const home = process.env["HOME"] ?? "";
|
|
return path.join(home, ".claude", "agents", "_primitives", "_rust", "target", "release");
|
|
}
|
|
|
|
async function readTokenFile(p: string | undefined): Promise<string | undefined> {
|
|
if (!p) return process.env["KEI_MCP_AUTH_TOKEN"];
|
|
const raw = await fs.readFile(p, "utf8");
|
|
return raw.trim();
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const args = parseArgv(process.argv.slice(2));
|
|
const token = args.stdio ? undefined : await readTokenFile(args.authTokenFile);
|
|
const server = new McpServer({
|
|
rustBinDir: args.rustBinDir,
|
|
...(token !== undefined ? { authToken: token } : {}),
|
|
});
|
|
await server.loadAdapters((m) => process.stderr.write(`[adapters] ${m}\n`));
|
|
if (args.stdio) await runStdio(server);
|
|
else await runHttp(server, args.port ?? 3000, args.bind);
|
|
}
|
|
|
|
async function runStdio(server: McpServer): Promise<void> {
|
|
process.stderr.write(`[keisei-mcp] stdio mode; ${server.listTools().length} tools\n`);
|
|
process.stdin.setEncoding("utf8");
|
|
for await (const chunk of process.stdin) {
|
|
for (const line of String(chunk).split("\n")) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) continue;
|
|
const resp = await dispatchStdioLine(server, trimmed);
|
|
process.stdout.write(resp + "\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
async function dispatchStdioLine(server: McpServer, line: string): Promise<string> {
|
|
try {
|
|
const payload = JSON.parse(line) as { tool: string; args?: Record<string, unknown> };
|
|
const call = payload.args !== undefined
|
|
? { tool: payload.tool, args: payload.args }
|
|
: { tool: payload.tool };
|
|
const resp = await server.handle(call);
|
|
return JSON.stringify(resp);
|
|
} catch (err) {
|
|
return JSON.stringify({ ok: false, error: { code: -32700, message: String(err) } });
|
|
}
|
|
}
|
|
|
|
async function runHttp(server: McpServer, port: number, bindAddr: string): Promise<void> {
|
|
const http = await import("node:http");
|
|
const srv = http.createServer((req, res) => void handleHttp(server, req, res));
|
|
// Bind to 127.0.0.1 by default; pass --bind 0.0.0.0 to expose on all interfaces.
|
|
srv.listen(port, bindAddr, () =>
|
|
process.stderr.write(`[keisei-mcp] http ${bindAddr}:${port}; ${server.listTools().length} tools\n`),
|
|
);
|
|
}
|
|
|
|
async function handleHttp(server: McpServer, req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse): Promise<void> {
|
|
if (req.method !== "POST") {
|
|
res.writeHead(405);
|
|
res.end();
|
|
return;
|
|
}
|
|
const MAX_BODY = 1 * 1024 * 1024; // 1 MiB
|
|
let total = 0;
|
|
const chunks: Buffer[] = [];
|
|
for await (const c of req) {
|
|
total += (c as Buffer).length;
|
|
if (total > MAX_BODY) {
|
|
res.writeHead(413, { "content-type": "application/json" });
|
|
res.end(JSON.stringify({ ok: false, error: { code: -32600, message: "request body exceeds 1 MiB" } }));
|
|
req.destroy();
|
|
return;
|
|
}
|
|
chunks.push(c as Buffer);
|
|
}
|
|
try {
|
|
const body = JSON.parse(Buffer.concat(chunks).toString("utf8")) as {
|
|
tool: string;
|
|
args?: Record<string, unknown>;
|
|
};
|
|
const authHeader = req.headers["authorization"];
|
|
const header = typeof authHeader === "string" ? authHeader.replace(/^Bearer\s+/i, "") : undefined;
|
|
const resp = await server.handle({
|
|
tool: body.tool,
|
|
...(body.args !== undefined ? { args: body.args } : {}),
|
|
...(header !== undefined ? { authHeader: header } : {}),
|
|
});
|
|
res.writeHead(resp.ok ? 200 : 400, { "content-type": "application/json" });
|
|
res.end(JSON.stringify(resp));
|
|
} catch (err) {
|
|
res.writeHead(400, { "content-type": "application/json" });
|
|
res.end(JSON.stringify({ ok: false, error: { code: -32700, message: String(err) } }));
|
|
}
|
|
}
|
|
|
|
main().catch((err: unknown) => {
|
|
process.stderr.write(`[keisei-mcp] fatal: ${String(err)}\n`);
|
|
process.exit(1);
|
|
});
|