Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitlab/ci/eval-gatekeeper.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ download-secure-files:
before_script:
# Add path of UV executable to PATH & Install dependencies
- export PATH="$CI_PROJECT_DIR/.local/bin:$PATH"
- uv sync --locked
- uv sync --locked --extra gcp

# Download Red Hat CA certifacate and combine it with root CA
- export SSL_CERT_FILE="$CI_PROJECT_DIR/cert.pem"
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ make verify # All checks (required before commit)
- Use pytest-mock (`mocker` fixture) for mocking instead of `unittest.mock` imports
- Use `autospec=True` when patching; `spec=<object>` with Mock
- 100% patch coverage for new code
- Check the project config and test runners and only decorate async functions if the ascyio_mode is not set to auto.

**Security (Critical):**
- All tools must be read-only with `readOnlyHint=True`
Expand Down
22 changes: 15 additions & 7 deletions docs/config-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,21 @@ These are used when `LINUX_MCP_TOOLSET` is set to `run_script` or `both`.
| Option / Env Var | Default | Description |
| ---------------- | ------- | ----------- |
| `--always-confirm-scripts` / `--no-always-confirm-scripts`<br>`LINUX_MCP_ALWAYS_CONFIRM_SCRIPTS` | `False` | All scripts must be confirmed by the user |
| `--gatekeeper.model`<br>`LINUX_MCP_GATEKEEPER__MODEL` | _(none)_ | Required: [LiteLLM model name](https://docs.litellm.ai/docs/providers) to use |
| `--gatekeeper.quantization`<br>`LINUX_MCP_GATEKEEPER__QUANTIZATION` | _(model specific)_ | _Not usually needed_ - Particular model quantization to use (openrouter only) |
| `--gatekeeper.reasoning_effort`<br>`LINUX_MCP_GATEKEEPER__REASONING_EFFORT` | _(model specific)_ | Reasoning effort to use for gatekeeper model (`none`, `minimal`, `low`, `medium`, `high`, `xhigh`). Not all values are supported for all models. |
| `--gatekeeper.structured_output`<br>`LINUX_MCP_GATEKEEPER__STRUCTURED_OUTPUT` | _(autodetected)_ | _Not usually needed_ - Whether to use structured output generation for the model. Default is to use if detected as available. |
| `--gatekeeper.temperature`<br>`LINUX_MCP_GATEKEEPER__TEMPERATURE` | 0.0 | _Not usually needed_ - Temperature to use for model - for some models, a non-zero value may be necessary when enabling reasoning. |
| `--gatekeeper.template_kwargs`<br>`LINUX_MCP_GATEKEEPER__TEMPLATE_KWARGS` | _(none)_ | _Not usually needed_ - Extra arguments for the model's chat template, formatted as a JSON string. Example: `{ "enable_thinking": false }` |
| Other environment variables | _(none)_ | As required by the LiteLLM provider, e.g. `OPENAI_API_KEY` |
| `--gatekeeper.provider`<br>`LINUX_MCP_GATEKEEPER__PROVIDER` | `openai` (inferred from model if unset) | LLM provider: `openai`, `anthropic`, `gemini`, `openrouter`, or `vertex_ai` |
| `--gatekeeper.model`<br>`LINUX_MCP_GATEKEEPER__MODEL` | _(none)_ | Required: provider-native model ID (e.g. `gpt-5.4`, `claude-sonnet-4-6`, `gemini-2.0-flash`, `openai/gpt-oss-120b` for OpenRouter) |
| `--gatekeeper.reasoning_effort`<br>`LINUX_MCP_GATEKEEPER__REASONING_EFFORT` | _(model specific)_ | Reasoning effort (`none`, `minimal`, `low`, `medium`, `high`, `xhigh`). Not all values are supported for all models. |
| `--gatekeeper.structured_output`<br>`LINUX_MCP_GATEKEEPER__STRUCTURED_OUTPUT` | `True` | Whether to use structured JSON output from the model |
| `--gatekeeper.temperature`<br>`LINUX_MCP_GATEKEEPER__TEMPERATURE` | 0.0 | Temperature to use for the model |
| `--gatekeeper.cost`<br>`LINUX_MCP_GATEKEEPER__COST` | _(none)_ | Optional per-token cost override for gatekeeper accounting (`input$/token:output$/token`). When unset, cost is estimated from [models.dev](https://models.dev) pricing (with a vendored fallback), except OpenRouter which uses API-reported `usage.cost` when available. Local OpenAI-compatible inference (e.g. llama.cpp on localhost) is reported as `$0`. |
| `--gatekeeper.openai.base_url`<br>`LINUX_MCP_GATEKEEPER__OPENAI__BASE_URL` / `OPENAI_API_BASE` | `https://api.openai.com/v1` | OpenAI provider: API base URL |
| `--gatekeeper.openai.template_kwargs`<br>`LINUX_MCP_GATEKEEPER__OPENAI__TEMPLATE_KWARGS` | _(none)_ | OpenAI provider: extra chat-template arguments (e.g. llama.cpp `enable_thinking`), sent as `chat_template_kwargs` |
| `--gatekeeper.openrouter.base_url`<br>`LINUX_MCP_GATEKEEPER__OPENROUTER__BASE_URL` | `https://openrouter.ai/api/v1` | OpenRouter provider: API base URL |
| `--gatekeeper.openrouter.quantization`<br>`LINUX_MCP_GATEKEEPER__OPENROUTER__QUANTIZATION` | _(none)_ | OpenRouter provider: filter providers by quantization level (e.g. `fp4`, `bf16`) |
| `--gatekeeper.openrouter.template_kwargs`<br>`LINUX_MCP_GATEKEEPER__OPENROUTER__TEMPLATE_KWARGS` | _(none)_ | OpenRouter provider: extra chat-template arguments |
| `--gatekeeper.vertex_ai.project`<br>`LINUX_MCP_GATEKEEPER__VERTEX_AI__PROJECT` / `VERTEXAI_PROJECT` | _(none)_ | Vertex AI provider: GCP project |
| `--gatekeeper.vertex_ai.location`<br>`LINUX_MCP_GATEKEEPER__VERTEX_AI__LOCATION` / `VERTEXAI_LOCATION` | `global` | Vertex AI provider: GCP region |
| `--gatekeeper.vertex_ai.base_url`<br>`LINUX_MCP_GATEKEEPER__VERTEX_AI__BASE_URL` | _(computed)_ | Vertex AI provider: override OpenAI-compatible MaaS endpoint URL (default derived from project/location) |
| Provider credentials | _(none)_ | `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_API_KEY` / `GEMINI_API_KEY`, or `OPENROUTER_API_KEY` for direct providers; `GOOGLE_APPLICATION_CREDENTIALS` for `vertex_ai` |

## Logging Configuration

Expand Down
49 changes: 44 additions & 5 deletions docs/guarded-command-execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,56 @@ LINUX_MCP_TOOLSET=run_script

**Configure a Gatekeeper Model**

Set `LINUX_MCP_GATEKEEPER__MODEL` to the name of the model you want to use. Additional environment
variables may be needed to configure credentials. See the
[LiteLLM documentation](https://docs.litellm.ai/docs/providers) for details on how to configure your provider.
Set `LINUX_MCP_GATEKEEPER__PROVIDER` and `LINUX_MCP_GATEKEEPER__MODEL` to configure the gatekeeper.
Set the matching API credentials for your provider (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`,
`GOOGLE_API_KEY`, or `OPENROUTER_API_KEY`). For the `vertex_ai` provider, install the `gcp` extra and configure
`GOOGLE_APPLICATION_CREDENTIALS`, `VERTEXAI_PROJECT`, and `VERTEXAI_LOCATION`.

Example:
Example (OpenAI):

```sh
LINUX_MCP_GATEKEEPER__MODEL=openai/chatgpt-5.2
LINUX_MCP_GATEKEEPER__PROVIDER=openai
LINUX_MCP_GATEKEEPER__MODEL=gpt-5.4
OPENAI_API_KEY=<....>
```

Example (Anthropic):

```sh
LINUX_MCP_GATEKEEPER__PROVIDER=anthropic
LINUX_MCP_GATEKEEPER__MODEL=claude-sonnet-4-6
ANTHROPIC_API_KEY=<....>
```

Example (OpenRouter):

```sh
LINUX_MCP_GATEKEEPER__PROVIDER=openrouter
LINUX_MCP_GATEKEEPER__MODEL=openai/gpt-oss-120b
LINUX_MCP_GATEKEEPER__OPENROUTER__QUANTIZATION=fp4
OPENROUTER_API_KEY=<....>
```

Example (Vertex AI):

```sh
LINUX_MCP_GATEKEEPER__PROVIDER=vertex_ai
LINUX_MCP_GATEKEEPER__MODEL=gemini-3.1-pro-preview
LINUX_MCP_GATEKEEPER__VERTEX_AI__PROJECT=my-gcp-project
GOOGLE_APPLICATION_CREDENTIALS=<path-to-service-account.json>
```

**Gatekeeper cost estimation**

Eval runs and gatekeeper stats report an estimated dollar cost per check. Resolution order:

1. **API-reported cost** — OpenRouter `usage.cost` when present.
2. **Config override** — `LINUX_MCP_GATEKEEPER__COST` as `input$/token:output$/token` (useful for Vertex MaaS eval models).
3. **models.dev** — runtime pricing lookup with a vendored snapshot fallback when the network is unavailable.
4. **Local inference** — OpenAI-compatible providers pointed at `localhost` / `127.0.0.1` (e.g. llama.cpp) report `$0`.
5. **Fallback** — hardcoded rates for known eval models, then a conservative cloud default.

Totals are **estimates** except when OpenRouter returns API-reported cost. Token counts come from each provider's usage metadata in the completion response.

**Configure your client's tool policy**

Expand Down
3 changes: 2 additions & 1 deletion docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ Get the Linux MCP Server running quickly with your favorite MCP client.
!!! note "Optional dependencies"
The `gssapi` package is needed for SSH authentication to Kerberos-registered systems. Install with `uv tool install linux-mcp-server[gssapi]`.

The `gcp` package is needed for Google Cloud Platform integration with the gatekeeper model. Install with `uv tool install linux-mcp-server[gcp]`.
The `gcp` optional dependency provides Google Cloud authentication for Vertex AI gatekeeper backends.
Install with `uv tool install linux-mcp-server[gcp]`.

---

Expand Down
12 changes: 11 additions & 1 deletion eval/gatekeeper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,21 @@ Runs test cases through the gatekeeper and reports results.

```bash
# Set the gatekeeper model
export LINUX_MCP_GATEKEEPER__MODEL="openrouter/anthropic/claude-3.5-sonnet"
export LINUX_MCP_GATEKEEPER__PROVIDER="anthropic"
export LINUX_MCP_GATEKEEPER__MODEL="claude-sonnet-4-6"
export ANTHROPIC_API_KEY="..."

# Run evaluation on a single file
uv run eval/gatekeeper/run-eval.py testcases/selinux-port-denial.yaml -o results.yaml

# Run via standard-evals.sh (OpenRouter example)
export OPENROUTER_API_KEY="..."
./eval/gatekeeper/standard-evals.sh --no-save gpt-oss-120b:low,fp4@openrouter

# Run via standard-evals.sh (Vertex AI example)
export VERTEXAI_PROJECT="my-gcp-project"
./eval/gatekeeper/standard-evals.sh --no-save gemini-3.1-pro-preview:low@vertex_ai

# Run all test case files in testcases/
uv run eval/gatekeeper/run-eval.py --all -o results.yaml

Expand Down
82 changes: 40 additions & 42 deletions eval/gatekeeper/standard-evals.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
#
# Run the gatekeeper eval suite against a specific model/provider combination.
#
# Configures provider-specific authentication, model routing (via LiteLLM
# model strings), reasoning effort, and cost overrides, then delegates to
# run-eval.py.
# Configures provider-specific authentication, gatekeeper env vars, reasoning
# effort, and cost overrides, then delegates to run-eval.py.
#
# Usage:
# standard-evals.sh [--no-save] [--variant=NAME] <MODEL>[:<REASONING_EFFORT>,<QUANTIZATION>][@PROVIDER]
Expand Down Expand Up @@ -237,7 +236,6 @@ LINUX_MCP_GATEKEEPER__COST=""
case "$model" in
claude-*)
if [[ $reasoning == "none" ]] ; then
# LiteLLM doesn't support reasoning_effort=none for Anthropic models
LINUX_MCP_GATEKEEPER__REASONING_EFFORT=
else
echo "Setting T=1, as required for thinking with Claude models"
Expand All @@ -264,44 +262,45 @@ esac
case "$provider" in
anthropic)
: "${ANTHROPIC_API_KEY:?'api key must be set'}"
LINUX_MCP_GATEKEEPER__MODEL="anthropic/$model"
LINUX_MCP_GATEKEEPER__PROVIDER=anthropic
LINUX_MCP_GATEKEEPER__MODEL="$model"
;;
llama_cpp)
OPENAI_API_KEY=tasty
OPENAI_API_BASE=http://localhost:8080/v1
# gemma-4 and qwen3.5 support a boolean enable_thinking tenplate parameter
# granite-4 doesn't have any explict reasoning control
LINUX_MCP_GATEKEEPER__PROVIDER=openai
LINUX_MCP_GATEKEEPER__OPENAI__BASE_URL=http://localhost:8080/v1
# gemma-4 and qwen3.5 support enable_thinking via chat_template_kwargs; granite-4 has no control
case "$reasoning" in
none)
LINUX_MCP_GATEKEEPER__TEMPLATE_KWARGS='{ "enable_thinking": false }'
LINUX_MCP_GATEKEEPER__OPENAI__TEMPLATE_KWARGS='{ "enable_thinking": false }'
;;
default)
LINUX_MCP_GATEKEEPER__TEMPLATE_KWARGS='{ "enable_thinking": true }'
LINUX_MCP_GATEKEEPER__OPENAI__TEMPLATE_KWARGS='{ "enable_thinking": true }'
;;
esac
export OPENAI_API_KEY
export OPENAI_API_BASE
export LINUX_MCP_GATEKEEPER__OPENAI__BASE_URL
max_parallel=1
quant_tag="${quantization^^}"
case "$model" in
gemma-4-26b-a4b-it)
LINUX_MCP_GATEKEEPER__MODEL=openai/google/gemma-4-26b-a4b
LINUX_MCP_GATEKEEPER__MODEL=google/gemma-4-26b-a4b
hf_repo=ggml-org/gemma-4-26B-A4B-it-GGUF
;;
granite-4.0-h-small)
LINUX_MCP_GATEKEEPER__MODEL=openai/ibm-granite/granite-4.0-h-small
LINUX_MCP_GATEKEEPER__MODEL=ibm-granite/granite-4.0-h-small
hf_repo=ibm-granite/granite-4.0-h-small-GGUF
;;
granite-4.1-8b)
LINUX_MCP_GATEKEEPER__MODEL=openai/ibm-granite/granite-4.1-8b
LINUX_MCP_GATEKEEPER__MODEL=ibm-granite/granite-4.1-8b
hf_repo=ibm-granite/granite-4.1-8b-GGUF
;;
qwen3.5-35b-a3b)
LINUX_MCP_GATEKEEPER__MODEL=openai/qwen/qwen3.5-35b-a3b
LINUX_MCP_GATEKEEPER__MODEL=qwen/qwen3.5-35b-a3b
hf_repo=unsloth/Qwen3.5-35B-A3B-GGUF
;;
qwen3.5-9b)
LINUX_MCP_GATEKEEPER__MODEL=openai/qwen/qwen3.5-9b
LINUX_MCP_GATEKEEPER__MODEL=qwen/qwen3.5-9b
hf_repo=unsloth/Qwen3.5-9B-GGUF
;;
*)
Expand All @@ -316,30 +315,25 @@ case "$provider" in
: "${VERTEXAI_PROJECT:?'project must be set'}"
uv_args+=("--extra" "gcp")
max_parallel=50
LINUX_MCP_GATEKEEPER__PROVIDER=vertex_ai
LINUX_MCP_GATEKEEPER__VERTEX_AI__PROJECT="${VERTEXAI_PROJECT}"
case $model in
claude-*)
LINUX_MCP_GATEKEEPER__MODEL="vertex_ai/$model"
;;
gemini-*)
LINUX_MCP_GATEKEEPER__MODEL="vertex_ai/$model"
claude-*|gemini-*)
LINUX_MCP_GATEKEEPER__MODEL="$model"
;;
gemma-4-26b-a4b-it)
if [[ $reasoning == "none" ]] ; then
# LiteLLM doesn't know about this model, so doesn't support reasoning_effort
LINUX_MCP_GATEKEEPER__REASONING_EFFORT=
fi
LINUX_MCP_GATEKEEPER__MODEL=openai/google/$model-maas
LINUX_MCP_GATEKEEPER__MODEL="${model}-maas"
LINUX_MCP_GATEKEEPER__COST=0.15e-6:0.60e-6
OPENAI_API_BASE=https://aiplatform.googleapis.com/v1/projects/${VERTEXAI_PROJECT}/locations/global/endpoints/openapi
OPENAI_API_KEY=$(gcloud auth print-access-token)
export OPENAI_API_BASE OPENAI_API_KEY
;;
gpt-oss-20b)
LINUX_MCP_GATEKEEPER__MODEL="vertex_ai/openai/$model-maas"
LINUX_MCP_GATEKEEPER__MODEL="${model}-maas"
LINUX_MCP_GATEKEEPER__COST="0.07e-6:0.25e-6"
;;
gpt-oss-120b)
LINUX_MCP_GATEKEEPER__MODEL="vertex_ai/openai/$model-maas"
LINUX_MCP_GATEKEEPER__MODEL="${model}-maas"
LINUX_MCP_GATEKEEPER__COST="0.09e-6:0.36e-6"
;;
esac
Expand All @@ -348,49 +342,53 @@ case "$provider" in
: "${OPENAI_API_KEY:?'api key must be set'}"
SSL_CERT_FILE=${SSL_CERT_FILE:-/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem}
export SSL_CERT_FILE
LINUX_MCP_GATEKEEPER__PROVIDER=openai
case $model in
granite-*)
LINUX_MCP_GATEKEEPER__MODEL="openai/ibm-granite/$model"
LINUX_MCP_GATEKEEPER__MODEL="ibm-granite/$model"
;;
gpt-oss*)
LINUX_MCP_GATEKEEPER__MODEL="openai/openai/$model"
echo "$LINUX_MCP_GATEKEEPER__MODEL"
LINUX_MCP_GATEKEEPER__MODEL="openai/$model"
;;
esac
OPENAI_API_BASE="$(get_MC_base_url "$model")"
export OPENAI_API_BASE
LINUX_MCP_GATEKEEPER__OPENAI__BASE_URL="$(get_MC_base_url "$model")"
max_parallel=5
;;
openrouter)
: "${OPENROUTER_API_KEY:?'api key must be set'}"
LINUX_MCP_GATEKEEPER__PROVIDER=openrouter
if [[ -n $quantization ]] ; then
LINUX_MCP_GATEKEEPER__QUANTIZATION=$quantization
export LINUX_MCP_GATEKEEPER__QUANTIZATION
LINUX_MCP_GATEKEEPER__OPENROUTER__QUANTIZATION=$quantization
fi
case $model in
claude-*)
LINUX_MCP_GATEKEEPER__MODEL="openrouter/anthropic/$model"
LINUX_MCP_GATEKEEPER__MODEL="anthropic/$model"
;;
gemma-*)
LINUX_MCP_GATEKEEPER__MODEL="openrouter/google/$model"
LINUX_MCP_GATEKEEPER__MODEL="google/$model"
;;
gpt-oss-*)
LINUX_MCP_GATEKEEPER__MODEL="openrouter/openai/$model"
LINUX_MCP_GATEKEEPER__MODEL="openai/$model"
;;
gpt-*)
LINUX_MCP_GATEKEEPER__MODEL="openrouter/openai/$model"
LINUX_MCP_GATEKEEPER__MODEL="openai/$model"
;;
granite-*)
LINUX_MCP_GATEKEEPER__MODEL="openrouter/ibm-granite/$model"
LINUX_MCP_GATEKEEPER__MODEL="ibm-granite/$model"
;;
qwen*)
LINUX_MCP_GATEKEEPER__MODEL="openrouter/qwen/$model"
LINUX_MCP_GATEKEEPER__MODEL="qwen/$model"
;;
esac
;;
esac

export LINUX_MCP_GATEKEEPER__MODEL LINUX_MCP_GATEKEEPER__COST LINUX_MCP_GATEKEEPER__REASONING_EFFORT
export LINUX_MCP_GATEKEEPER__PROVIDER LINUX_MCP_GATEKEEPER__MODEL
export LINUX_MCP_GATEKEEPER__COST LINUX_MCP_GATEKEEPER__REASONING_EFFORT
[[ -n "${LINUX_MCP_GATEKEEPER__OPENROUTER__QUANTIZATION:-}" ]] && export LINUX_MCP_GATEKEEPER__OPENROUTER__QUANTIZATION
[[ -n "${LINUX_MCP_GATEKEEPER__OPENAI__BASE_URL:-}" ]] && export LINUX_MCP_GATEKEEPER__OPENAI__BASE_URL
[[ -n "${LINUX_MCP_GATEKEEPER__OPENAI__TEMPLATE_KWARGS:-}" ]] && export LINUX_MCP_GATEKEEPER__OPENAI__TEMPLATE_KWARGS
[[ -n "${LINUX_MCP_GATEKEEPER__VERTEX_AI__PROJECT:-}" ]] && export LINUX_MCP_GATEKEEPER__VERTEX_AI__PROJECT

variant_suffix=""
if [[ -n "$variant" ]] ; then
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ requires-python = ">=3.10"
dependencies = [
"asyncssh[bcrypt] >= 2.22.0",
"fastmcp >= 3.2.4",
"litellm>=1.80.16",
"httpx>=0.28.1",
"pydantic-settings >= 2.12.0",
"pydantic >= 2.12.5",
# pydocket is incompatible with newer versions of fakeredis.
Expand Down Expand Up @@ -48,7 +48,7 @@ classifiers = [

[project.optional-dependencies]
gssapi = ["gssapi >=1.11.1"]
gcp = ["google-cloud-aiplatform>=1.147.0"]
gcp = ["google-auth>=2.40.0"]

[project.urls]
"Source code" = "https://github.com/rhel-lightspeed/linux-mcp-server"
Expand Down
Loading