mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
feat: codex-shell-tool-mcp (#7005)
This adds a GitHub workflow for building a new npm module we are
experimenting with that contains an MCP server for running Bash
commands. The new workflow, `shell-tool-mcp`, is a dependency of the
general `release` workflow so that we continue to use one version number
for all artifacts across the project in one GitHub release.
`.github/workflows/shell-tool-mcp.yml` is the primary workflow
introduced by this PR, which does the following:
- builds the `codex-exec-mcp-server` and `codex-execve-wrapper`
executables for both arm64 and x64 versions of Mac and Linux (preferring
the MUSL version for Linux)
- builds Bash (dynamically linked) for a [comically] large number of
platforms (both x64 and arm64 for most) with a small patch specified by
`shell-tool-mcp/patches/bash-exec-wrapper.patch`:
- `debian-11`
- `debian-12`
- `ubuntu-20.04`
- `ubuntu-22.04`
- `ubuntu-24.04`
- `centos-9`
- `macos-13` (x64 only)
- `macos-14` (arm64 only)
- `macos-15` (arm64 only)
- builds the TypeScript for the [new] Node module declared in the
`shell-tool-mcp/` folder, which creates `bin/mcp-server.js`
- adds all of the native binaries to `shell-tool-mcp/vendor/` folder;
`bin/mcp-server.js` does a runtime check to determine which ones to
execute
- uses `npm pack` to create the `.tgz` for the module
- if `publish: true` is set, invokes the `npm publish` call with the
`.tgz`
The justification for building Bash for so many different operating
systems is because, since it is dynamically linked, we want to increase
our confidence that the version we build is compatible with the glibc
whatever OS we end up running on. (Note this is less of a concern with
`codex-exec-mcp-server` and `codex-execve-wrapper` on Linux, as they are
statically linked.)
This PR also introduces the code for the npm module in `shell-tool-mcp/`
(the proposed module name is `@openai/codex-shell-tool-mcp`). Initially,
I intended the module to be a single file of vanilla JavaScript (like
[`codex-cli/bin/codex.js`](ab5972d447/codex-cli/bin/codex.js)),
but some of the logic seemed a bit tricky, so I decided to port it to
TypeScript and add unit tests.
`shell-tool-mcp/src/index.ts` defines the `main()` function for the
module, which performs runtime checks to determine the clang triple to
find the path to the Rust executables within the `vendor/` folder
(`resolveTargetTriple()`). It uses a combination of `readOsRelease()`
and `resolveBashPath()` to determine the correct Bash executable to run
in the environment. Ultimately, it spawns a command like the following:
```
codex-exec-mcp-server \
--execve codex-execve-wrapper \
--bash custom-bash "$@"
```
Note `.github/workflows/shell-tool-mcp-ci.yml` defines a fairly standard
CI job for the module (`format`/`build`/`test`).
To test this PR, I pushed this branch to my personal fork of Codex and
ran the CI job there:
https://github.com/bolinfest/codex/actions/runs/19564311320
Admittedly, the graph looks a bit wild now:
<img width="5115" height="2969" alt="Screenshot 2025-11-20 at 11 44
58 PM"
src="https://github.com/user-attachments/assets/cc5ef306-efc1-4ed7-a137-5347e394f393"
/>
But when it finished, I was able to download `codex-shell-tool-mcp-npm`
from the **Artifacts** for the workflow in an empty temp directory,
unzip the `.zip` and then the `.tgz` inside it, followed by `xattr -rc
.` to remove the quarantine bits. Then I ran:
```shell
npx @modelcontextprotocol/inspector node /private/tmp/foobar4/package/bin/mcp-server.js
```
which launched the MCP Inspector and I was able to use it as expected!
This bodes well that this should work once the package is published to
npm:
```shell
npx @modelcontextprotocol/inspector npx @openai/codex-shell-tool-mcp
```
Also, to verify the package contains what I expect:
```shell
/tmp/foobar4/package$ tree
.
├── bin
│ └── mcp-server.js
├── package.json
├── README.md
└── vendor
├── aarch64-apple-darwin
│ ├── bash
│ │ ├── macos-14
│ │ │ └── bash
│ │ └── macos-15
│ │ └── bash
│ ├── codex-exec-mcp-server
│ └── codex-execve-wrapper
├── aarch64-unknown-linux-musl
│ ├── bash
│ │ ├── centos-9
│ │ │ └── bash
│ │ ├── debian-11
│ │ │ └── bash
│ │ ├── debian-12
│ │ │ └── bash
│ │ ├── ubuntu-20.04
│ │ │ └── bash
│ │ ├── ubuntu-22.04
│ │ │ └── bash
│ │ └── ubuntu-24.04
│ │ └── bash
│ ├── codex-exec-mcp-server
│ └── codex-execve-wrapper
├── x86_64-apple-darwin
│ ├── bash
│ │ └── macos-13
│ │ └── bash
│ ├── codex-exec-mcp-server
│ └── codex-execve-wrapper
└── x86_64-unknown-linux-musl
├── bash
│ ├── centos-9
│ │ └── bash
│ ├── debian-11
│ │ └── bash
│ ├── debian-12
│ │ └── bash
│ ├── ubuntu-20.04
│ │ └── bash
│ ├── ubuntu-22.04
│ │ └── bash
│ └── ubuntu-24.04
│ └── bash
├── codex-exec-mcp-server
└── codex-execve-wrapper
26 directories, 26 files
```
This commit is contained in:
14
.github/workflows/rust-release.yml
vendored
14
.github/workflows/rust-release.yml
vendored
@@ -371,8 +371,20 @@ jobs:
|
||||
path: |
|
||||
codex-rs/dist/${{ matrix.target }}/*
|
||||
|
||||
shell-tool-mcp:
|
||||
name: shell-tool-mcp
|
||||
needs: tag-check
|
||||
uses: ./.github/workflows/shell-tool-mcp.yml
|
||||
with:
|
||||
release-tag: ${{ github.ref_name }}
|
||||
# We are not ready to publish yet.
|
||||
publish: false
|
||||
secrets: inherit
|
||||
|
||||
release:
|
||||
needs: build
|
||||
needs:
|
||||
- build
|
||||
- shell-tool-mcp
|
||||
name: release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
|
||||
48
.github/workflows/shell-tool-mcp-ci.yml
vendored
Normal file
48
.github/workflows/shell-tool-mcp-ci.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: shell-tool-mcp CI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "shell-tool-mcp/**"
|
||||
- ".github/workflows/shell-tool-mcp-ci.yml"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "shell-tool-mcp/**"
|
||||
- ".github/workflows/shell-tool-mcp-ci.yml"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Format check
|
||||
run: pnpm --filter @openai/codex-shell-tool-mcp run format
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm --filter @openai/codex-shell-tool-mcp test
|
||||
|
||||
- name: Build
|
||||
run: pnpm --filter @openai/codex-shell-tool-mcp run build
|
||||
412
.github/workflows/shell-tool-mcp.yml
vendored
Normal file
412
.github/workflows/shell-tool-mcp.yml
vendored
Normal file
@@ -0,0 +1,412 @@
|
||||
name: shell-tool-mcp
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
release-version:
|
||||
description: Version to publish (x.y.z or x.y.z-alpha.N). Defaults to GITHUB_REF_NAME when it starts with rust-v.
|
||||
required: false
|
||||
type: string
|
||||
release-tag:
|
||||
description: Tag name to use when downloading release artifacts (defaults to rust-v<version>).
|
||||
required: false
|
||||
type: string
|
||||
publish:
|
||||
description: Whether to publish to npm when the version is releasable.
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22
|
||||
|
||||
jobs:
|
||||
metadata:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.compute.outputs.version }}
|
||||
release_tag: ${{ steps.compute.outputs.release_tag }}
|
||||
should_publish: ${{ steps.compute.outputs.should_publish }}
|
||||
npm_tag: ${{ steps.compute.outputs.npm_tag }}
|
||||
steps:
|
||||
- name: Compute version and tags
|
||||
id: compute
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
version="${{ inputs.release-version }}"
|
||||
release_tag="${{ inputs.release-tag }}"
|
||||
|
||||
if [[ -z "$version" ]]; then
|
||||
if [[ -n "$release_tag" && "$release_tag" =~ ^rust-v.+ ]]; then
|
||||
version="${release_tag#rust-v}"
|
||||
elif [[ "${GITHUB_REF_NAME:-}" =~ ^rust-v.+ ]]; then
|
||||
version="${GITHUB_REF_NAME#rust-v}"
|
||||
release_tag="${GITHUB_REF_NAME}"
|
||||
else
|
||||
echo "release-version is required when GITHUB_REF_NAME is not a rust-v tag."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$release_tag" ]]; then
|
||||
release_tag="rust-v${version}"
|
||||
fi
|
||||
|
||||
npm_tag=""
|
||||
should_publish="false"
|
||||
if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
should_publish="true"
|
||||
elif [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then
|
||||
should_publish="true"
|
||||
npm_tag="alpha"
|
||||
fi
|
||||
|
||||
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
||||
echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT"
|
||||
echo "npm_tag=${npm_tag}" >> "$GITHUB_OUTPUT"
|
||||
echo "should_publish=${should_publish}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
rust-binaries:
|
||||
name: Build Rust - ${{ matrix.target }}
|
||||
needs: metadata
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 30
|
||||
defaults:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
- runner: macos-15-xlarge
|
||||
target: x86_64-apple-darwin
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
install_musl: true
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
install_musl: true
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- uses: dtolnay/rust-toolchain@1.90
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- if: ${{ matrix.install_musl }}
|
||||
name: Install musl build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y musl-tools pkg-config
|
||||
|
||||
- name: Build exec server binaries
|
||||
run: cargo build --release --target ${{ matrix.target }} --bin codex-exec-mcp-server --bin codex-execve-wrapper
|
||||
|
||||
- name: Stage exec server binaries
|
||||
run: |
|
||||
dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}"
|
||||
mkdir -p "$dest"
|
||||
cp "target/${{ matrix.target }}/release/codex-exec-mcp-server" "$dest/"
|
||||
cp "target/${{ matrix.target }}/release/codex-execve-wrapper" "$dest/"
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: shell-tool-mcp-rust-${{ matrix.target }}
|
||||
path: artifacts/**
|
||||
if-no-files-found: error
|
||||
|
||||
bash-linux:
|
||||
name: Build Bash (Linux) - ${{ matrix.variant }} - ${{ matrix.target }}
|
||||
needs: metadata
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 30
|
||||
container:
|
||||
image: ${{ matrix.image }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
variant: ubuntu-24.04
|
||||
image: ubuntu:24.04
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
variant: ubuntu-22.04
|
||||
image: ubuntu:22.04
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
variant: ubuntu-20.04
|
||||
image: ubuntu:20.04
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
variant: debian-12
|
||||
image: debian:12
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
variant: debian-11
|
||||
image: debian:11
|
||||
- runner: ubuntu-24.04
|
||||
target: x86_64-unknown-linux-musl
|
||||
variant: centos-9
|
||||
image: quay.io/centos/centos:stream9
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: ubuntu-24.04
|
||||
image: arm64v8/ubuntu:24.04
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: ubuntu-22.04
|
||||
image: arm64v8/ubuntu:22.04
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: ubuntu-20.04
|
||||
image: arm64v8/ubuntu:20.04
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: debian-12
|
||||
image: arm64v8/debian:12
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: debian-11
|
||||
image: arm64v8/debian:11
|
||||
- runner: ubuntu-24.04-arm
|
||||
target: aarch64-unknown-linux-musl
|
||||
variant: centos-9
|
||||
image: quay.io/centos/centos:stream9
|
||||
steps:
|
||||
- name: Install build prerequisites
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential bison autoconf gettext
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y git gcc gcc-c++ make bison autoconf gettext
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
yum install -y git gcc gcc-c++ make bison autoconf gettext
|
||||
else
|
||||
echo "Unsupported package manager in container"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Build patched Bash
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git clone --depth 1 https://github.com/bminor/bash /tmp/bash
|
||||
cd /tmp/bash
|
||||
git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
|
||||
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
|
||||
git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch"
|
||||
./configure --without-bash-malloc
|
||||
cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)"
|
||||
make -j"${cores}"
|
||||
|
||||
dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}"
|
||||
mkdir -p "$dest"
|
||||
cp bash "$dest/bash"
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }}
|
||||
path: artifacts/**
|
||||
if-no-files-found: error
|
||||
|
||||
bash-darwin:
|
||||
name: Build Bash (macOS) - ${{ matrix.variant }} - ${{ matrix.target }}
|
||||
needs: metadata
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: macos-15-xlarge
|
||||
target: aarch64-apple-darwin
|
||||
variant: macos-15
|
||||
- runner: macos-14
|
||||
target: aarch64-apple-darwin
|
||||
variant: macos-14
|
||||
- runner: macos-13
|
||||
target: x86_64-apple-darwin
|
||||
variant: macos-13
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Build patched Bash
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git clone --depth 1 https://github.com/bminor/bash /tmp/bash
|
||||
cd /tmp/bash
|
||||
git fetch --depth 1 origin a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
|
||||
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
|
||||
git apply "${GITHUB_WORKSPACE}/shell-tool-mcp/patches/bash-exec-wrapper.patch"
|
||||
./configure --without-bash-malloc
|
||||
cores="$(getconf _NPROCESSORS_ONLN)"
|
||||
make -j"${cores}"
|
||||
|
||||
dest="${GITHUB_WORKSPACE}/artifacts/vendor/${{ matrix.target }}/bash/${{ matrix.variant }}"
|
||||
mkdir -p "$dest"
|
||||
cp bash "$dest/bash"
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: shell-tool-mcp-bash-${{ matrix.target }}-${{ matrix.variant }}
|
||||
path: artifacts/**
|
||||
if-no-files-found: error
|
||||
|
||||
package:
|
||||
name: Package npm module
|
||||
needs:
|
||||
- metadata
|
||||
- rust-binaries
|
||||
- bash-linux
|
||||
- bash-darwin
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PACKAGE_VERSION: ${{ needs.metadata.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.8.1
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Install JavaScript dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build (shell-tool-mcp)
|
||||
run: pnpm --filter @openai/codex-shell-tool-mcp run build
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: Assemble staging directory
|
||||
id: staging
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
staging="${STAGING_DIR}"
|
||||
mkdir -p "$staging" "$staging/vendor"
|
||||
cp shell-tool-mcp/README.md "$staging/"
|
||||
cp shell-tool-mcp/package.json "$staging/"
|
||||
cp -R shell-tool-mcp/bin "$staging/"
|
||||
|
||||
found_vendor="false"
|
||||
shopt -s nullglob
|
||||
for vendor_dir in artifacts/*/vendor; do
|
||||
rsync -av "$vendor_dir/" "$staging/vendor/"
|
||||
found_vendor="true"
|
||||
done
|
||||
if [[ "$found_vendor" == "false" ]]; then
|
||||
echo "No vendor payloads were downloaded."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
node - <<'NODE'
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const stagingDir = process.env.STAGING_DIR;
|
||||
const version = process.env.PACKAGE_VERSION;
|
||||
const pkgPath = path.join(stagingDir, "package.json");
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
||||
pkg.version = version;
|
||||
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
||||
NODE
|
||||
|
||||
echo "dir=$staging" >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
STAGING_DIR: ${{ runner.temp }}/shell-tool-mcp
|
||||
|
||||
- name: Ensure binaries are executable
|
||||
run: |
|
||||
set -euo pipefail
|
||||
staging="${{ steps.staging.outputs.dir }}"
|
||||
chmod +x \
|
||||
"$staging"/vendor/*/codex-exec-mcp-server \
|
||||
"$staging"/vendor/*/codex-execve-wrapper \
|
||||
"$staging"/vendor/*/bash/*/bash
|
||||
|
||||
- name: Create npm tarball
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p dist/npm
|
||||
staging="${{ steps.staging.outputs.dir }}"
|
||||
pack_info=$(cd "$staging" && npm pack --ignore-scripts --json --pack-destination "${GITHUB_WORKSPACE}/dist/npm")
|
||||
filename=$(PACK_INFO="$pack_info" node -e 'const data = JSON.parse(process.env.PACK_INFO); console.log(data[0].filename);')
|
||||
mv "dist/npm/${filename}" "dist/npm/codex-shell-tool-mcp-npm-${PACKAGE_VERSION}.tgz"
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: codex-shell-tool-mcp-npm
|
||||
path: dist/npm/codex-shell-tool-mcp-npm-${{ env.PACKAGE_VERSION }}.tgz
|
||||
if-no-files-found: error
|
||||
|
||||
publish:
|
||||
name: Publish npm package
|
||||
needs:
|
||||
- metadata
|
||||
- package
|
||||
if: ${{ inputs.publish && needs.metadata.outputs.should_publish == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.8.1
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
registry-url: https://registry.npmjs.org
|
||||
scope: "@openai"
|
||||
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Download npm tarball
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: codex-shell-tool-mcp-npm
|
||||
path: dist/npm
|
||||
|
||||
- name: Publish to npm
|
||||
env:
|
||||
NPM_TAG: ${{ needs.metadata.outputs.npm_tag }}
|
||||
VERSION: ${{ needs.metadata.outputs.version }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tag_args=()
|
||||
if [[ -n "${NPM_TAG}" ]]; then
|
||||
tag_args+=(--tag "${NPM_TAG}")
|
||||
fi
|
||||
npm publish "dist/npm/codex-shell-tool-mcp-npm-${VERSION}.tgz" "${tag_args[@]}"
|
||||
24
pnpm-lock.yaml
generated
24
pnpm-lock.yaml
generated
@@ -71,6 +71,30 @@ importers:
|
||||
specifier: ^3.24.6
|
||||
version: 3.24.6(zod@3.25.76)
|
||||
|
||||
shell-tool-mcp:
|
||||
devDependencies:
|
||||
'@types/jest':
|
||||
specifier: ^29.5.14
|
||||
version: 29.5.14
|
||||
'@types/node':
|
||||
specifier: ^20.19.18
|
||||
version: 20.19.18
|
||||
jest:
|
||||
specifier: ^29.7.0
|
||||
version: 29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2))
|
||||
prettier:
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2
|
||||
ts-jest:
|
||||
specifier: ^29.3.4
|
||||
version: 29.4.4(@babel/core@7.28.4)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.4))(esbuild@0.25.10)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.18)(ts-node@10.9.2(@types/node@20.19.18)(typescript@5.9.2)))(typescript@5.9.2)
|
||||
tsup:
|
||||
specifier: ^8.5.0
|
||||
version: 8.5.0(postcss@8.5.6)(typescript@5.9.2)(yaml@2.8.1)
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.2
|
||||
|
||||
packages:
|
||||
|
||||
'@babel/code-frame@7.27.1':
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
packages:
|
||||
- docs
|
||||
- sdk/typescript
|
||||
- shell-tool-mcp
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- esbuild
|
||||
|
||||
2
shell-tool-mcp/.gitignore
vendored
Normal file
2
shell-tool-mcp/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/bin/
|
||||
/node_modules/
|
||||
31
shell-tool-mcp/README.md
Normal file
31
shell-tool-mcp/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# @openai/codex-shell-tool-mcp
|
||||
|
||||
This package wraps the `codex-exec-mcp-server` binary and its helpers so that the shell MCP can be invoked via `npx @openai/codex-shell-tool-mcp`. It bundles:
|
||||
|
||||
- `codex-exec-mcp-server` and `codex-execve-wrapper` built for macOS (arm64, x64) and Linux (musl arm64, musl x64).
|
||||
- A patched Bash that honors `BASH_EXEC_WRAPPER`, built for multiple glibc baselines (Ubuntu 24.04/22.04/20.04, Debian 12/11, CentOS-like 9) and macOS (15/14/13).
|
||||
- A launcher (`bin/mcp-server.js`) that picks the correct binaries for the current `process.platform` / `process.arch`, specifying `--execve` and `--bash` for the MCP, as appropriate.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
npx @openai/codex-shell-tool-mcp --help
|
||||
```
|
||||
|
||||
The launcher selects a Rust target triple based on the host and chooses the closest Bash variant by inspecting `/etc/os-release` on Linux or the Darwin major version on macOS.
|
||||
|
||||
## Patched Bash
|
||||
|
||||
We carry a small patch to `execute_cmd.c` (see `patches/bash-exec-wrapper.patch`) that adds support for `BASH_EXEC_WRAPPER`. The original commit message is “add support for BASH_EXEC_WRAPPER” and the patch applies cleanly to `a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b` from https://github.com/bminor/bash. To rebuild manually:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/bminor/bash
|
||||
git checkout a8a1c2fac029404d3f42cd39f5a20f24b6e4fe4b
|
||||
git apply /path/to/patches/bash-exec-wrapper.patch
|
||||
./configure --without-bash-malloc
|
||||
make -j"$(nproc)"
|
||||
```
|
||||
|
||||
## Release workflow
|
||||
|
||||
`.github/workflows/shell-tool-mcp.yml` builds the Rust binaries, compiles the patched Bash variants, assembles the `vendor/` tree, and creates `codex-shell-tool-mcp-npm-<version>.tgz` for inclusion in the Rust GitHub Release. When the version is a stable or alpha tag, the workflow also publishes the tarball to npm using OIDC. The workflow is invoked from `rust-release.yml` so the package ships alongside other Codex artifacts.
|
||||
6
shell-tool-mcp/jest.config.cjs
Normal file
6
shell-tool-mcp/jest.config.cjs
Normal file
@@ -0,0 +1,6 @@
|
||||
/** @type {import('jest').Config} */
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
roots: ["<rootDir>/tests"],
|
||||
};
|
||||
40
shell-tool-mcp/package.json
Normal file
40
shell-tool-mcp/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@openai/codex-shell-tool-mcp",
|
||||
"version": "0.0.0-dev",
|
||||
"description": "Codex MCP server for the shell tool with patched Bash and exec wrappers.",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"codex-shell-tool-mcp": "bin/mcp-server.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"vendor",
|
||||
"README.md"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/openai/codex.git",
|
||||
"directory": "shell-tool-mcp"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf bin",
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"format": "prettier --check .",
|
||||
"format:fix": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.19.18",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.6.2",
|
||||
"ts-jest": "^29.3.4",
|
||||
"tsup": "^8.5.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
24
shell-tool-mcp/patches/bash-exec-wrapper.patch
Normal file
24
shell-tool-mcp/patches/bash-exec-wrapper.patch
Normal file
@@ -0,0 +1,24 @@
|
||||
diff --git a/execute_cmd.c b/execute_cmd.c
|
||||
index 070f5119..d20ad2b9 100644
|
||||
--- a/execute_cmd.c
|
||||
+++ b/execute_cmd.c
|
||||
@@ -6129,6 +6129,19 @@ shell_execve (char *command, char **args, char **env)
|
||||
char sample[HASH_BANG_BUFSIZ];
|
||||
size_t larray;
|
||||
|
||||
+ char* exec_wrapper = getenv("BASH_EXEC_WRAPPER");
|
||||
+ if (exec_wrapper && *exec_wrapper && !whitespace (*exec_wrapper))
|
||||
+ {
|
||||
+ char *orig_command = command;
|
||||
+
|
||||
+ larray = strvec_len (args);
|
||||
+
|
||||
+ memmove (args + 2, args, (++larray) * sizeof (char *));
|
||||
+ args[0] = exec_wrapper;
|
||||
+ args[1] = orig_command;
|
||||
+ command = exec_wrapper;
|
||||
+ }
|
||||
+
|
||||
SETOSTYPE (0); /* Some systems use for USG/POSIX semantics */
|
||||
execve (command, args, env);
|
||||
i = errno; /* error from execve() */
|
||||
115
shell-tool-mcp/src/bashSelection.ts
Normal file
115
shell-tool-mcp/src/bashSelection.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import { DARWIN_BASH_VARIANTS, LINUX_BASH_VARIANTS } from "./constants";
|
||||
import { BashSelection, OsReleaseInfo } from "./types";
|
||||
|
||||
function supportedDetail(variants: ReadonlyArray<{ name: string }>): string {
|
||||
return `Supported variants: ${variants.map((variant) => variant.name).join(", ")}`;
|
||||
}
|
||||
|
||||
export function selectLinuxBash(
|
||||
bashRoot: string,
|
||||
info: OsReleaseInfo,
|
||||
): BashSelection {
|
||||
const versionId = info.versionId;
|
||||
const candidates: Array<{
|
||||
variant: (typeof LINUX_BASH_VARIANTS)[number];
|
||||
matchesVersion: boolean;
|
||||
}> = [];
|
||||
for (const variant of LINUX_BASH_VARIANTS) {
|
||||
const matchesId =
|
||||
variant.ids.includes(info.id) ||
|
||||
variant.ids.some((id) => info.idLike.includes(id));
|
||||
if (!matchesId) {
|
||||
continue;
|
||||
}
|
||||
const matchesVersion = Boolean(
|
||||
versionId &&
|
||||
variant.versions.some((prefix) => versionId.startsWith(prefix)),
|
||||
);
|
||||
candidates.push({ variant, matchesVersion });
|
||||
}
|
||||
|
||||
const pickVariant = (list: typeof candidates) =>
|
||||
list.find((item) => item.variant)?.variant;
|
||||
|
||||
const preferred = pickVariant(
|
||||
candidates.filter((item) => item.matchesVersion),
|
||||
);
|
||||
if (preferred) {
|
||||
return {
|
||||
path: path.join(bashRoot, preferred.name, "bash"),
|
||||
variant: preferred.name,
|
||||
};
|
||||
}
|
||||
|
||||
const fallbackMatch = pickVariant(candidates);
|
||||
if (fallbackMatch) {
|
||||
return {
|
||||
path: path.join(bashRoot, fallbackMatch.name, "bash"),
|
||||
variant: fallbackMatch.name,
|
||||
};
|
||||
}
|
||||
|
||||
const fallback = LINUX_BASH_VARIANTS[0];
|
||||
if (fallback) {
|
||||
return {
|
||||
path: path.join(bashRoot, fallback.name, "bash"),
|
||||
variant: fallback.name,
|
||||
};
|
||||
}
|
||||
|
||||
const detail = supportedDetail(LINUX_BASH_VARIANTS);
|
||||
throw new Error(
|
||||
`Unable to select a Bash variant for ${info.id || "unknown"} ${versionId || ""}. ${detail}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function selectDarwinBash(
|
||||
bashRoot: string,
|
||||
darwinRelease: string,
|
||||
): BashSelection {
|
||||
const darwinMajor = Number.parseInt(darwinRelease.split(".")[0] || "0", 10);
|
||||
const preferred = DARWIN_BASH_VARIANTS.find(
|
||||
(variant) => darwinMajor >= variant.minDarwin,
|
||||
);
|
||||
if (preferred) {
|
||||
return {
|
||||
path: path.join(bashRoot, preferred.name, "bash"),
|
||||
variant: preferred.name,
|
||||
};
|
||||
}
|
||||
|
||||
const fallback = DARWIN_BASH_VARIANTS[0];
|
||||
if (fallback) {
|
||||
return {
|
||||
path: path.join(bashRoot, fallback.name, "bash"),
|
||||
variant: fallback.name,
|
||||
};
|
||||
}
|
||||
|
||||
const detail = supportedDetail(DARWIN_BASH_VARIANTS);
|
||||
throw new Error(
|
||||
`Unable to select a macOS Bash build (darwin ${darwinMajor}). ${detail}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveBashPath(
|
||||
targetRoot: string,
|
||||
platform: NodeJS.Platform,
|
||||
darwinRelease = os.release(),
|
||||
osInfo: OsReleaseInfo | null = null,
|
||||
): BashSelection {
|
||||
const bashRoot = path.join(targetRoot, "bash");
|
||||
|
||||
if (platform === "linux") {
|
||||
if (!osInfo) {
|
||||
throw new Error("Linux OS info is required to select a Bash variant.");
|
||||
}
|
||||
return selectLinuxBash(bashRoot, osInfo);
|
||||
}
|
||||
if (platform === "darwin") {
|
||||
return selectDarwinBash(bashRoot, darwinRelease);
|
||||
}
|
||||
throw new Error(`Unsupported platform for Bash selection: ${platform}`);
|
||||
}
|
||||
20
shell-tool-mcp/src/constants.ts
Normal file
20
shell-tool-mcp/src/constants.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { DarwinBashVariant, LinuxBashVariant } from "./types";
|
||||
|
||||
export const LINUX_BASH_VARIANTS: ReadonlyArray<LinuxBashVariant> = [
|
||||
{ name: "ubuntu-24.04", ids: ["ubuntu"], versions: ["24.04"] },
|
||||
{ name: "ubuntu-22.04", ids: ["ubuntu"], versions: ["22.04"] },
|
||||
{ name: "ubuntu-20.04", ids: ["ubuntu"], versions: ["20.04"] },
|
||||
{ name: "debian-12", ids: ["debian"], versions: ["12"] },
|
||||
{ name: "debian-11", ids: ["debian"], versions: ["11"] },
|
||||
{
|
||||
name: "centos-9",
|
||||
ids: ["centos", "rhel", "rocky", "almalinux"],
|
||||
versions: ["9"],
|
||||
},
|
||||
];
|
||||
|
||||
export const DARWIN_BASH_VARIANTS: ReadonlyArray<DarwinBashVariant> = [
|
||||
{ name: "macos-15", minDarwin: 24 },
|
||||
{ name: "macos-14", minDarwin: 23 },
|
||||
{ name: "macos-13", minDarwin: 22 },
|
||||
];
|
||||
101
shell-tool-mcp/src/index.ts
Normal file
101
shell-tool-mcp/src/index.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
// Launches the codex-exec-mcp-server binary bundled in this package.
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { accessSync, constants } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { resolveBashPath } from "./bashSelection";
|
||||
import { readOsRelease } from "./osRelease";
|
||||
import { resolveTargetTriple } from "./platform";
|
||||
|
||||
const scriptPath = process.argv[1]
|
||||
? path.resolve(process.argv[1])
|
||||
: process.cwd();
|
||||
const __dirname = path.dirname(scriptPath);
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const targetTriple = resolveTargetTriple(process.platform, process.arch);
|
||||
const vendorRoot = path.join(__dirname, "..", "vendor");
|
||||
const targetRoot = path.join(vendorRoot, targetTriple);
|
||||
const execveWrapperPath = path.join(targetRoot, "codex-execve-wrapper");
|
||||
const serverPath = path.join(targetRoot, "codex-exec-mcp-server");
|
||||
|
||||
const osInfo = process.platform === "linux" ? readOsRelease() : null;
|
||||
const { path: bashPath } = resolveBashPath(
|
||||
targetRoot,
|
||||
process.platform,
|
||||
os.release(),
|
||||
osInfo,
|
||||
);
|
||||
|
||||
[execveWrapperPath, serverPath, bashPath].forEach((checkPath) => {
|
||||
try {
|
||||
accessSync(checkPath, constants.F_OK);
|
||||
} catch {
|
||||
throw new Error(`Required binary missing: ${checkPath}`);
|
||||
}
|
||||
});
|
||||
|
||||
const args = [
|
||||
"--execve",
|
||||
execveWrapperPath,
|
||||
"--bash",
|
||||
bashPath,
|
||||
...process.argv.slice(2),
|
||||
];
|
||||
const child = spawn(serverPath, args, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
const forwardSignal = (signal: NodeJS.Signals) => {
|
||||
if (child.killed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
child.kill(signal);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
(["SIGINT", "SIGTERM", "SIGHUP"] as const).forEach((sig) => {
|
||||
process.on(sig, () => forwardSignal(sig));
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
const childResult = await new Promise<
|
||||
| { type: "signal"; signal: NodeJS.Signals }
|
||||
| { type: "code"; exitCode: number }
|
||||
>((resolve) => {
|
||||
child.on("exit", (code, signal) => {
|
||||
if (signal) {
|
||||
resolve({ type: "signal", signal });
|
||||
} else {
|
||||
resolve({ type: "code", exitCode: code ?? 1 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (childResult.type === "signal") {
|
||||
// This environment running under `node --test` may not allow rethrowing a signal.
|
||||
// Wrap in a try to avoid masking the original termination reason.
|
||||
try {
|
||||
process.kill(process.pid, childResult.signal);
|
||||
} catch {
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
process.exit(childResult.exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
void main().catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
34
shell-tool-mcp/src/osRelease.ts
Normal file
34
shell-tool-mcp/src/osRelease.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { OsReleaseInfo } from "./types";
|
||||
|
||||
export function parseOsRelease(contents: string): OsReleaseInfo {
|
||||
const lines = contents.split("\n").filter(Boolean);
|
||||
const info: Record<string, string> = {};
|
||||
for (const line of lines) {
|
||||
const [rawKey, rawValue] = line.split("=", 2);
|
||||
if (!rawKey || rawValue === undefined) {
|
||||
continue;
|
||||
}
|
||||
const key = rawKey.toLowerCase();
|
||||
const value = rawValue.replace(/^"/, "").replace(/"$/, "");
|
||||
info[key] = value;
|
||||
}
|
||||
const idLike = (info.id_like || "")
|
||||
.split(/\s+/)
|
||||
.map((item) => item.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
return {
|
||||
id: (info.id || "").toLowerCase(),
|
||||
idLike,
|
||||
versionId: info.version_id || "",
|
||||
};
|
||||
}
|
||||
|
||||
export function readOsRelease(pathname = "/etc/os-release"): OsReleaseInfo {
|
||||
try {
|
||||
const contents = readFileSync(pathname, "utf8");
|
||||
return parseOsRelease(contents);
|
||||
} catch {
|
||||
return { id: "", idLike: [], versionId: "" };
|
||||
}
|
||||
}
|
||||
21
shell-tool-mcp/src/platform.ts
Normal file
21
shell-tool-mcp/src/platform.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export function resolveTargetTriple(
|
||||
platform: NodeJS.Platform,
|
||||
arch: NodeJS.Architecture,
|
||||
): string {
|
||||
if (platform === "linux") {
|
||||
if (arch === "x64") {
|
||||
return "x86_64-unknown-linux-musl";
|
||||
}
|
||||
if (arch === "arm64") {
|
||||
return "aarch64-unknown-linux-musl";
|
||||
}
|
||||
} else if (platform === "darwin") {
|
||||
if (arch === "x64") {
|
||||
return "x86_64-apple-darwin";
|
||||
}
|
||||
if (arch === "arm64") {
|
||||
return "aarch64-apple-darwin";
|
||||
}
|
||||
}
|
||||
throw new Error(`Unsupported platform: ${platform} (${arch})`);
|
||||
}
|
||||
21
shell-tool-mcp/src/types.ts
Normal file
21
shell-tool-mcp/src/types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export type LinuxBashVariant = {
|
||||
name: string;
|
||||
ids: Array<string>;
|
||||
versions: Array<string>;
|
||||
};
|
||||
|
||||
export type DarwinBashVariant = {
|
||||
name: string;
|
||||
minDarwin: number;
|
||||
};
|
||||
|
||||
export type OsReleaseInfo = {
|
||||
id: string;
|
||||
idLike: Array<string>;
|
||||
versionId: string;
|
||||
};
|
||||
|
||||
export type BashSelection = {
|
||||
path: string;
|
||||
variant: string;
|
||||
};
|
||||
41
shell-tool-mcp/tests/bashSelection.test.ts
Normal file
41
shell-tool-mcp/tests/bashSelection.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { selectDarwinBash, selectLinuxBash } from "../src/bashSelection";
|
||||
import { DARWIN_BASH_VARIANTS, LINUX_BASH_VARIANTS } from "../src/constants";
|
||||
import { OsReleaseInfo } from "../src/types";
|
||||
import path from "node:path";
|
||||
|
||||
describe("selectLinuxBash", () => {
|
||||
const bashRoot = "/vendor/bash";
|
||||
|
||||
it("prefers exact version match when id is present", () => {
|
||||
const info: OsReleaseInfo = {
|
||||
id: "ubuntu",
|
||||
idLike: ["debian"],
|
||||
versionId: "24.04.1",
|
||||
};
|
||||
const selection = selectLinuxBash(bashRoot, info);
|
||||
expect(selection.variant).toBe("ubuntu-24.04");
|
||||
expect(selection.path).toBe(path.join(bashRoot, "ubuntu-24.04", "bash"));
|
||||
});
|
||||
|
||||
it("falls back to first supported variant when no matches", () => {
|
||||
const info: OsReleaseInfo = { id: "unknown", idLike: [], versionId: "1.0" };
|
||||
const selection = selectLinuxBash(bashRoot, info);
|
||||
expect(selection.variant).toBe(LINUX_BASH_VARIANTS[0].name);
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectDarwinBash", () => {
|
||||
const bashRoot = "/vendor/bash";
|
||||
|
||||
it("selects compatible darwin version", () => {
|
||||
const darwinRelease = "24.0.0";
|
||||
const selection = selectDarwinBash(bashRoot, darwinRelease);
|
||||
expect(selection.variant).toBe("macos-15");
|
||||
});
|
||||
|
||||
it("falls back to first darwin variant when release too old", () => {
|
||||
const darwinRelease = "20.0.0";
|
||||
const selection = selectDarwinBash(bashRoot, darwinRelease);
|
||||
expect(selection.variant).toBe(DARWIN_BASH_VARIANTS[0].name);
|
||||
});
|
||||
});
|
||||
30
shell-tool-mcp/tests/osRelease.test.ts
Normal file
30
shell-tool-mcp/tests/osRelease.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { parseOsRelease } from "../src/osRelease";
|
||||
|
||||
describe("parseOsRelease", () => {
|
||||
it("parses basic fields", () => {
|
||||
const contents = `ID="ubuntu"
|
||||
ID_LIKE="debian"
|
||||
VERSION_ID=24.04
|
||||
OTHER=ignored`;
|
||||
|
||||
const info = parseOsRelease(contents);
|
||||
expect(info).toEqual({
|
||||
id: "ubuntu",
|
||||
idLike: ["debian"],
|
||||
versionId: "24.04",
|
||||
});
|
||||
});
|
||||
|
||||
it("handles missing fields", () => {
|
||||
const contents = "SOMETHING=else";
|
||||
const info = parseOsRelease(contents);
|
||||
expect(info).toEqual({ id: "", idLike: [], versionId: "" });
|
||||
});
|
||||
|
||||
it("normalizes id_like entries", () => {
|
||||
const contents = `ID="rhel"
|
||||
ID_LIKE="CentOS Rocky"`;
|
||||
const info = parseOsRelease(contents);
|
||||
expect(info.idLike).toEqual(["centos", "rocky"]);
|
||||
});
|
||||
});
|
||||
13
shell-tool-mcp/tsconfig.json
Normal file
13
shell-tool-mcp/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src", "tests"]
|
||||
}
|
||||
15
shell-tool-mcp/tsup.config.ts
Normal file
15
shell-tool-mcp/tsup.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
"mcp-server": "src/index.ts",
|
||||
},
|
||||
outDir: "bin",
|
||||
format: ["cjs"],
|
||||
target: "node18",
|
||||
clean: true,
|
||||
sourcemap: false,
|
||||
banner: {
|
||||
js: "#!/usr/bin/env node",
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user