File size: 5,078 Bytes
1f21206 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | import { constants } from 'node:fs'
import { access } from 'node:fs/promises'
import path from 'node:path'
import { getCwd } from '../../utils/cwd.js'
import { getMcpStdioEnvironment } from '../../utils/mcpStdioEnvironment.js'
type HostCommandCheckResult =
| {
ok: true
resolvedCommand: string
}
| {
ok: false
message: string
}
function getPathSearchList(envPath?: string) {
return (envPath ?? process.env.PATH ?? '')
.split(path.delimiter)
.map((entry) => entry.trim())
.filter(Boolean)
}
function getWindowsExecutableCandidates(command: string) {
if (process.platform !== 'win32') {
return [command]
}
const ext = path.extname(command)
if (ext) {
return [command]
}
const pathext = (process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM')
.split(';')
.map((entry) => entry.trim())
.filter(Boolean)
return [command, ...pathext.map((entry) => `${command}${entry.toLowerCase()}`)]
}
function isPathLikeCommand(command: string) {
return (
path.isAbsolute(command) ||
command.startsWith('./') ||
command.startsWith('../') ||
command.startsWith('.\\') ||
command.startsWith('..\\') ||
command.includes('/') ||
command.includes('\\')
)
}
function buildRuntimeHint(command: string) {
const normalized = path.basename(command).toLowerCase()
if (['node', 'npm', 'npx', 'pnpm', 'yarn'].includes(normalized)) {
return `Install Node.js on this machine so "${command}" is available in PATH, then retry.`
}
if (['python', 'python3', 'pip', 'pip3'].includes(normalized)) {
return `Install Python on this machine so "${command}" is available in PATH, then retry.`
}
if (normalized === 'uv') {
return 'Install uv on this machine so "uv" is available in PATH, then retry.'
}
if (normalized === 'bun') {
return 'Install Bun on this machine so "bun" is available in PATH, then retry.'
}
return `Install "${command}" on this machine or update PATH, then retry.`
}
function buildMissingCommandMessage(command: string) {
return `Host command "${command}" is not available in PATH. This STDIO MCP runs on the host machine. ${buildRuntimeHint(command)}`
}
function buildMissingPathMessage(command: string, resolvedPath: string) {
return `Host command path "${command}" could not be found at "${resolvedPath}". This STDIO MCP runs on the host machine, so the configured executable path must exist locally.`
}
function buildNonExecutablePathMessage(command: string, resolvedPath: string) {
return `Host command path "${command}" exists at "${resolvedPath}" but is not executable. This STDIO MCP runs on the host machine, so the configured executable must be runnable by the local OS user.`
}
async function resolveCommandFromPath(
command: string,
envPath?: string,
): Promise<string | null> {
const pathEntries = getPathSearchList(envPath)
for (const entry of pathEntries) {
for (const candidate of getWindowsExecutableCandidates(command)) {
const resolvedPath = path.join(entry, candidate)
try {
await access(
resolvedPath,
process.platform === 'win32' ? constants.F_OK : constants.X_OK,
)
return resolvedPath
} catch {
// Continue searching other PATH entries.
}
}
}
return null
}
export async function inspectMcpHostCommand(
command: string,
cwd: string = getCwd(),
env?: Record<string, string>,
): Promise<HostCommandCheckResult> {
const trimmedCommand = command.trim()
if (!trimmedCommand) {
return {
ok: false,
message: 'STDIO MCP command is empty.',
}
}
if (isPathLikeCommand(trimmedCommand)) {
const resolvedPath = path.isAbsolute(trimmedCommand)
? trimmedCommand
: path.resolve(cwd, trimmedCommand)
try {
await access(
resolvedPath,
process.platform === 'win32' ? constants.F_OK : constants.X_OK,
)
return {
ok: true,
resolvedCommand: resolvedPath,
}
} catch (error) {
const maybeErr = error as NodeJS.ErrnoException
return {
ok: false,
message:
maybeErr.code === 'ENOENT'
? buildMissingPathMessage(trimmedCommand, resolvedPath)
: buildNonExecutablePathMessage(trimmedCommand, resolvedPath),
}
}
}
const hasExplicitPath = env ? Object.hasOwn(env, 'PATH') : false
const resolvedCommand = hasExplicitPath
? await resolveCommandFromPath(trimmedCommand, env?.PATH)
: await resolveCommandFromPath(trimmedCommand, process.env.PATH)
if (resolvedCommand) {
return {
ok: true,
resolvedCommand,
}
}
if (!hasExplicitPath) {
const stdioEnv = await getMcpStdioEnvironment(env)
const shellResolvedCommand = await resolveCommandFromPath(
trimmedCommand,
stdioEnv.PATH,
)
if (shellResolvedCommand) {
return {
ok: true,
resolvedCommand: shellResolvedCommand,
}
}
}
return {
ok: false,
message: buildMissingCommandMessage(trimmedCommand),
}
}
|