202 lines
6.9 KiB
TypeScript
202 lines
6.9 KiB
TypeScript
import { realpathSync } from "node:fs";
|
|
import { isAbsolute, resolve } from "node:path";
|
|
import { valid } from "semver";
|
|
|
|
import { PRODUCT_IDENTITY } from "../identity.js";
|
|
|
|
/** Package runners whose pinned launcher setup can register. */
|
|
export type PackageRunner = "npx" | "bunx";
|
|
|
|
/** Read one variable; Windows variable names are case-insensitive. */
|
|
const environmentValue = (
|
|
environment: Readonly<NodeJS.ProcessEnv>,
|
|
name: string,
|
|
platform: NodeJS.Platform,
|
|
): string | undefined => {
|
|
if (platform !== "win32") return environment[name];
|
|
const target = name.toLowerCase();
|
|
const key = Object.keys(environment).findLast(
|
|
(candidate) => candidate.toLowerCase() === target,
|
|
);
|
|
return key === undefined ? undefined : environment[key];
|
|
};
|
|
|
|
/** Identify the package runner that launched this process, if any. */
|
|
export const invokingPackageRunner = (
|
|
environment: Readonly<NodeJS.ProcessEnv>,
|
|
platform: NodeJS.Platform = process.platform,
|
|
): PackageRunner | undefined => {
|
|
if (environmentValue(environment, "npm_command", platform) !== "exec")
|
|
return undefined;
|
|
// `bunx` and `bun x` also report npm_command=exec, with a bun user agent.
|
|
return environmentValue(
|
|
environment,
|
|
"npm_config_user_agent",
|
|
platform,
|
|
)?.startsWith("bun/") === true
|
|
? "bunx"
|
|
: "npx";
|
|
};
|
|
|
|
/** Copy an environment without the variable that marks a package-runner launch. */
|
|
export const withoutPackageRunnerIdentity = (
|
|
environment: Readonly<NodeJS.ProcessEnv>,
|
|
platform: NodeJS.Platform = process.platform,
|
|
): NodeJS.ProcessEnv =>
|
|
Object.fromEntries(
|
|
Object.entries(environment).filter(([key]) =>
|
|
platform === "win32"
|
|
? key.toLowerCase() !== "npm_command"
|
|
: key !== "npm_command",
|
|
),
|
|
);
|
|
|
|
/** Resolve a pinned package-runner launcher that stdio clients can spawn on the host. */
|
|
export const packageRunnerRegistrationCommand = (
|
|
runner: PackageRunner,
|
|
platform: NodeJS.Platform = process.platform,
|
|
): readonly string[] => {
|
|
const command =
|
|
runner === "npx"
|
|
? ["npx", "-y", PRODUCT_IDENTITY.registrationPackageSpecifier, "mcp"]
|
|
: ["bunx", PRODUCT_IDENTITY.registrationPackageSpecifier, "mcp"];
|
|
// Both runners may be .cmd shims on Windows, which clients cannot spawn directly.
|
|
return platform === "win32" ? ["cmd.exe", "/d", "/c", ...command] : command;
|
|
};
|
|
|
|
/** Resolve a pinned npx launcher that stdio clients can spawn on the host. */
|
|
export const npxRegistrationCommand = (
|
|
platform: NodeJS.Platform = process.platform,
|
|
): readonly string[] => packageRunnerRegistrationCommand("npx", platform);
|
|
|
|
const unwrapCmdCommand = (command: readonly string[]): readonly string[] => {
|
|
const executable = command[0]?.toLowerCase();
|
|
if (executable !== "cmd" && executable !== "cmd.exe") return command;
|
|
if (command[1]?.toLowerCase() === "/c") return command.slice(2);
|
|
if (command[1]?.toLowerCase() === "/d" && command[2]?.toLowerCase() === "/c")
|
|
return command.slice(3);
|
|
return [];
|
|
};
|
|
|
|
/** Preserve the package argument from a supported package-runner command. */
|
|
export const clientRegistrationPackageReference = (
|
|
command: readonly string[],
|
|
): string | undefined => {
|
|
const arguments_ = unwrapCmdCommand(command);
|
|
if (
|
|
arguments_.length === 4 &&
|
|
arguments_[0] === "npx" &&
|
|
arguments_[1] === "-y" &&
|
|
arguments_[3] === "mcp"
|
|
)
|
|
return arguments_[2];
|
|
if (
|
|
arguments_.length === 3 &&
|
|
arguments_[0] === "bunx" &&
|
|
arguments_[2] === "mcp"
|
|
)
|
|
return arguments_[1];
|
|
return undefined;
|
|
};
|
|
|
|
const isOwnedPackageReference = (packageReference: string): boolean => {
|
|
if (
|
|
packageReference === PRODUCT_IDENTITY.packageName ||
|
|
packageReference === PRODUCT_IDENTITY.packageSpecifier ||
|
|
packageReference === PRODUCT_IDENTITY.registrationPackageSpecifier
|
|
)
|
|
return true;
|
|
const versionPrefix = `${PRODUCT_IDENTITY.packageName}@`;
|
|
return (
|
|
packageReference.startsWith(versionPrefix) &&
|
|
valid(packageReference.slice(versionPrefix.length)) !== null
|
|
);
|
|
};
|
|
|
|
/** Check that one parsed command points to REA's MCP entry point. */
|
|
export type ClientRegistrationCommandIdentity = "owned" | "foreign" | "unknown";
|
|
|
|
/** POSIX bin links can name the same dispatcher without changing stored commands. */
|
|
const sameEntrypointPath = (candidate: string, current: string): boolean => {
|
|
if (!isAbsolute(candidate) || !isAbsolute(current)) return false;
|
|
if (resolve(candidate) === current) return true;
|
|
// Windows package bins are command shims, not links to the dispatcher.
|
|
if (process.platform === "win32") return false;
|
|
try {
|
|
return realpathSync(candidate) === realpathSync(current);
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/** Classify only what the command and current executable identity establish. */
|
|
export const classifyClientRegistrationCommand = (
|
|
command: readonly string[],
|
|
currentCommandPath: string | undefined,
|
|
): ClientRegistrationCommandIdentity => {
|
|
const packageReference = clientRegistrationPackageReference(command);
|
|
if (packageReference !== undefined)
|
|
return isOwnedClientRegistrationCommand(command, currentCommandPath)
|
|
? "owned"
|
|
: "foreign";
|
|
const currentPathAvailable =
|
|
currentCommandPath !== undefined && currentCommandPath.length > 0;
|
|
const nodeScript = command.length === 3 && command[2] === "mcp";
|
|
if (nodeScript) {
|
|
if (
|
|
!isAbsolute(command[0] ?? "") ||
|
|
!isAbsolute(command[1] ?? "") ||
|
|
!currentPathAvailable
|
|
)
|
|
return "unknown";
|
|
return isOwnedClientRegistrationCommand(command, currentCommandPath)
|
|
? "owned"
|
|
: "foreign";
|
|
}
|
|
if (command.length === 2 && command[1] === "mcp") {
|
|
const executable = command[0] ?? "";
|
|
if (executable === PRODUCT_IDENTITY.cliBinary) return "owned";
|
|
const relativePathLike =
|
|
!isAbsolute(executable) && /[./\\]/u.test(executable);
|
|
if (relativePathLike || (isAbsolute(executable) && !currentPathAvailable))
|
|
return "unknown";
|
|
}
|
|
if (isOwnedClientRegistrationCommand(command, currentCommandPath))
|
|
return "owned";
|
|
return "foreign";
|
|
};
|
|
|
|
export const isOwnedClientRegistrationCommand = (
|
|
command: readonly string[],
|
|
currentCommandPath: string | undefined,
|
|
): boolean => {
|
|
if (
|
|
currentCommandPath !== undefined &&
|
|
currentCommandPath.length > 0 &&
|
|
command.length === 3 &&
|
|
command[2] === "mcp" &&
|
|
isAbsolute(command[0] ?? "") &&
|
|
isAbsolute(command[1] ?? "") &&
|
|
resolve(command[0] ?? "") === resolve(process.execPath) &&
|
|
sameEntrypointPath(command[1] ?? "", currentCommandPath)
|
|
)
|
|
return true;
|
|
|
|
if (command.length === 2 && command[1] === "mcp") {
|
|
const executable = command[0] ?? "";
|
|
if (
|
|
executable === PRODUCT_IDENTITY.cliBinary ||
|
|
(isAbsolute(executable) &&
|
|
currentCommandPath !== undefined &&
|
|
currentCommandPath.length > 0 &&
|
|
sameEntrypointPath(executable, currentCommandPath))
|
|
)
|
|
return true;
|
|
}
|
|
|
|
const packageReference = clientRegistrationPackageReference(command);
|
|
return (
|
|
packageReference !== undefined && isOwnedPackageReference(packageReference)
|
|
);
|
|
};
|