Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
14 changes: 2 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ docker buildx build -f mcp-local/Dockerfile -t armlimited/arm-mcp . --load

Choose the configuration that matches your MCP client:

The examples below include the optional Docker arguments required for **Arm Performix**. These SSH-related settings are only needed when you want the MCP server to run remote commands on a target device through Arm Performix. If you are not using Arm Performix, you can omit the `-e` and SSH `-v` lines.
The examples below include the optional Docker arguments required for **Arm Performix**. These SSH-related settings are only needed when you want the MCP server to run remote commands on a target device through Arm Performix. If you are not using Arm Performix, you can omit the SSH `-v` lines.

#### Claude Code

Expand All @@ -66,8 +66,6 @@ Add to `.mcp.json` in your project:
"-v", "/path/to/your/workspace:/workspace",
"-v", "/path/to/your/ssh/private_key:/run/keys/ssh-key.pem:ro",
"-v", "/path/to/your/ssh/known_hosts:/run/keys/known_hosts:ro",
"-e", "SSH_KEY_PATH=/run/keys/ssh-key.pem",
"-e", "KNOWN_HOSTS_PATH=/run/keys/known_hosts",
"armlimited/arm-mcp"
]
}
Expand All @@ -92,8 +90,6 @@ Add to `.vscode/mcp.json` in your project, or globally at `~/Library/Application
"-v", "/path/to/your/workspace:/workspace",
"-v", "/path/to/your/ssh/private_key:/run/keys/ssh-key.pem:ro",
"-v", "/path/to/your/ssh/known_hosts:/run/keys/known_hosts:ro",
"-e", "SSH_KEY_PATH=/run/keys/ssh-key.pem",
"-e", "KNOWN_HOSTS_PATH=/run/keys/known_hosts",
"armlimited/arm-mcp"
]
}
Expand Down Expand Up @@ -121,8 +117,6 @@ Add to `~/.kiro/settings/mcp.json`:
"-v", "/path/to/your/workspace:/workspace",
"-v", "/path/to/your/ssh/private_key:/run/keys/ssh-key.pem:ro",
"-v", "/path/to/your/ssh/known_hosts:/run/keys/known_hosts:ro",
"-e", "SSH_KEY_PATH=/run/keys/ssh-key.pem",
"-e", "KNOWN_HOSTS_PATH=/run/keys/known_hosts",
"--name", "arm-mcp",
"armlimited/arm-mcp"
],
Expand Down Expand Up @@ -150,8 +144,6 @@ Add to `.gemini/settings.json` in your project root:
"-v", "/path/to/your/workspace:/workspace",
"-v", "/path/to/your/ssh/private_key:/run/keys/ssh-key.pem:ro",
"-v", "/path/to/your/ssh/known_hosts:/run/keys/known_hosts:ro",
"-e", "SSH_KEY_PATH=/run/keys/ssh-key.pem",
"-e", "KNOWN_HOSTS_PATH=/run/keys/known_hosts",
"armlimited/arm-mcp"
]
}
Expand All @@ -171,13 +163,11 @@ args = [
"-v", "/path/to/your/workspace:/workspace",
"-v", "/path/to/your/ssh/private_key:/run/keys/ssh-key.pem:ro",
"-v", "/path/to/your/ssh/known_hosts:/run/keys/known_hosts:ro",
"-e", "SSH_KEY_PATH=/run/keys/ssh-key.pem",
"-e", "KNOWN_HOSTS_PATH=/run/keys/known_hosts",
"armlimited/arm-mcp"
]
```

**Note**: Replace `/path/to/your/workspace` with the actual path to your project directory that you want the MCP server to access. If you are enabling Arm Performix, also replace the `/path/to/your/ssh/private_key` and `/path/to/your/ssh/known_hosts` paths with your local files.
**Note**: Replace `/path/to/your/workspace` with the actual path to your project directory that you want the MCP server to access. If you are enabling Arm Performix, also replace the `/path/to/your/ssh/private_key` and `/path/to/your/ssh/known_hosts` paths with your local files. The MCP container auto-discovers files mounted under `/run/keys`, as shown in the configs above.

### 3. Restart Your MCP Client

Expand Down
14 changes: 8 additions & 6 deletions mcp-local/performix-deployment-scenarios.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ Validate SSH access:
ssh -i /path/to/key <remote_user>@<target_ip>
```

### 2. MCP server runtime must have SSH config
### 2. MCP server runtime must have the SSH files mounted

Your MCP server/container configuration must include:
Your MCP server/container configuration must mount:

- `SSH_KEY_PATH`
- `KNOWN_HOSTS_PATH`
- the private key file under `/run/keys`
- the `known_hosts` file under `/run/keys`

The MCP container will discover these per-file mounts from `/proc/self/mounts` automatically.

### 3. Target workload must be runnable

Expand Down Expand Up @@ -138,6 +140,6 @@ flowchart LR
If `apx_recipe_run` fails:

- Verify SSH key permissions (`chmod 600 /path/to/key`).
- Verify `SSH_KEY_PATH` and `KNOWN_HOSTS_PATH` in MCP config.
- Verify the SSH key and `known_hosts` files are mounted into `/run/keys`.
- Verify target IP, username, and command path.
- Verify selected recipe is supported in your target environment.
- Verify selected recipe is supported in your target environment.
25 changes: 17 additions & 8 deletions mcp-local/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@
from utils.config import METADATA_PATH, USEARCH_INDEX_PATH, MODEL_NAME, SUPPORTED_SCANNERS, DEFAULT_ARCH
from utils.search_utils import build_bm25_index, deduplicate_urls, hybrid_search, load_metadata, load_usearch_index
from utils.docker_utils import check_docker_image_architectures
from utils.apx import prepare_target, run_workload, get_results
from utils.apx import (
prepare_target,
run_workload,
get_results,
resolve_apx_ssh_mount_env,
build_apx_ssh_mount_help,
)
from utils.migrate_ease_utils import run_migrate_ease_scan
from utils.skopeo_tool import skopeo_help, skopeo_inspect
from utils.llvm_mca_tool import mca_help, llvm_mca_analyze
Expand Down Expand Up @@ -292,20 +298,23 @@ def apx_recipe_run(cmd:str, remote_ip_addr:str, remote_usr:str, recipe:str="code
},
)
apx_dir = os.environ.get("APX_HOME", "/opt/apx")
key_path = os.getenv("SSH_KEY_PATH")
known_hosts_path = os.getenv("KNOWN_HOSTS_PATH")
ssh_mount_env = resolve_apx_ssh_mount_env()
key_path = ssh_mount_env["key_path"]
known_hosts_path = ssh_mount_env["known_hosts_path"]

if not key_path or not known_hosts_path:
mount_help = build_apx_ssh_mount_help(
ssh_mount_env["mount_targets"],
known_hosts_reason=ssh_mount_env.get("known_hosts_reason"),
key_reason=ssh_mount_env.get("key_reason"),
)
return {
"status": "error",
"recipe": recipe,
"stage": "config_validation",
"message": "Missing SSH configuration for APX target access.",
"suggestion": "Set SSH_KEY_PATH and KNOWN_HOSTS_PATH in the MCP docker run configuration, then retry.",
"details": (
"SSH_KEY_PATH and KNOWN_HOSTS_PATH environment variables must be set in the docker run "
"command in the MCP config file to mount in the container to use APX."
),
"suggestion": mount_help["suggestion"],
"details": mount_help["details"],
}

target_add_res = prepare_target(remote_ip_addr, remote_usr, key_path, apx_dir)
Expand Down
175 changes: 175 additions & 0 deletions mcp-local/utils/apx.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

QUERY_REGISTRY_PATH = Path(__file__).resolve().parent.parent / "sql" / "queries.sql"
ANSI_ESCAPE_RE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
RUN_KEYS_DIR = Path("/run/keys")
PROC_MOUNTS_PATH = Path("/proc/self/mounts")


def load_recipe_query_map(sql_file_path: Path) -> Dict[str, Dict[str, str]]:
Expand Down Expand Up @@ -248,6 +250,179 @@ def _build_atp_error_response(
response["raw_output"] = _trim_output(raw_output)
return response


def _decode_mount_field(field: str) -> str:
return re.sub(
r"\\([0-7]{3})",
lambda match: chr(int(match.group(1), 8)),
field,
)


def discover_run_keys_mounts(
mounts_path: Optional[Path] = None,
run_keys_dir: Optional[Path] = None,
) -> List[str]:
mounts_path = mounts_path or PROC_MOUNTS_PATH
run_keys_dir = run_keys_dir or RUN_KEYS_DIR

if not mounts_path.exists():
return []

mount_targets: List[str] = []
try:
for line in mounts_path.read_text(encoding="utf-8").splitlines():
parts = line.split()
if len(parts) < 2:
continue
target = _decode_mount_field(parts[1])
if target == str(run_keys_dir) or target.startswith(f"{run_keys_dir}/"):
mount_targets.append(target)
except (OSError, UnicodeDecodeError):
return []

# Preserve input order while removing duplicates.
return list(dict.fromkeys(mount_targets))


def _select_known_hosts_path(mount_targets: List[str]) -> Tuple[Optional[str], Optional[str]]:
matches = [
path for path in mount_targets
if "known_hosts" in Path(path).name.lower().replace("-", "_")
]
if len(matches) == 1:
return matches[0], None
if len(matches) > 1:
return None, f"Multiple known_hosts mount targets were found: {matches}."
return None, "No known_hosts mount target was found."


def _select_ssh_key_path(
mount_targets: List[str],
known_hosts_path: Optional[str],
) -> Tuple[Optional[str], Optional[str]]:
candidates = [path for path in mount_targets if path != known_hosts_path]
if not candidates:
return None, "No SSH key mount target was found."
if len(candidates) == 1:
return candidates[0], None

key_like = [
path for path in candidates
if any(
token in Path(path).name.lower()
for token in ("ssh", "key", ".pem", "id_rsa", "id_ed25519", "id_ecdsa", "id_dsa")
)
]
if len(key_like) == 1:
return key_like[0], None
if len(key_like) > 1:
return None, (
f"Multiple SSH key-like mount targets were found: {key_like}. "
"Mount only one SSH private key file under /run/keys."
)
return None, (
f"Unable to identify the SSH private key from mount target names. "
f"Candidate non-known_hosts mounts: {candidates}."
)


def _list_run_keys_files(run_keys_dir: Optional[Path] = None) -> List[str]:
run_keys_dir = run_keys_dir or RUN_KEYS_DIR
if not run_keys_dir.exists() or not run_keys_dir.is_dir():
return []

return sorted(
str(path)
for path in run_keys_dir.iterdir()
if path.is_file()
)


def build_apx_ssh_mount_help(
mount_targets: List[str],
run_keys_dir: Optional[Path] = None,
known_hosts_reason: Optional[str] = None,
key_reason: Optional[str] = None,
) -> Dict[str, str]:
run_keys_dir = run_keys_dir or RUN_KEYS_DIR
mount_summary = mount_targets if mount_targets else "none"
run_keys_files = _list_run_keys_files(run_keys_dir)
mount_hint = (
"Mount the files individually as described in the README, for example "
"'-v /path/to/your/ssh/private_key:/run/keys/ssh-key.pem:ro' and "
"'-v /path/to/your/ssh/known_hosts:/run/keys/known_hosts:ro'."
)

suggestion = (
"Mount the SSH private key and known_hosts as individual files under /run/keys, then retry."
)
resolution_reasons = [reason for reason in (known_hosts_reason, key_reason) if reason]
reason_text = ""
if resolution_reasons:
reason_text = " Resolution detail: " + " ".join(resolution_reasons)

if mount_targets == [str(run_keys_dir)] and run_keys_files:
file_summary = run_keys_files
details = (
"APX auto-discovers individual file mounts under /run/keys from /proc/self/mounts. "
f"No individual file mounts were discovered. "
f"Discovered mounts: {mount_summary}. Files currently present under {run_keys_dir}: {file_summary}. "
"This usually means a directory was mounted, for example '-v ~/.ssh:/run/keys:ro', so "
"auto-discovery cannot infer which file is the key and which file is known_hosts from mount data alone. "
f"{mount_hint}{reason_text}"
)
else:
details = (
"APX auto-discovers individual file mounts under /run/keys from /proc/self/mounts. "
f"Discovered mounts: {mount_summary}. "
"Make sure both files are mounted individually under /run/keys as described in the README, "
"for example '-v /path/to/your/ssh/private_key:/run/keys/ssh-key.pem:ro' and "
f"'-v /path/to/your/ssh/known_hosts:/run/keys/known_hosts:ro'.{reason_text}"
)

return {
"suggestion": suggestion,
"details": details,
}


def resolve_apx_ssh_mount_env() -> Dict[str, Any]:
key_path = os.getenv("SSH_KEY_PATH")
known_hosts_path = os.getenv("KNOWN_HOSTS_PATH")
mount_targets: List[str] = []
key_reason: Optional[str] = None
known_hosts_reason: Optional[str] = None

if key_path and known_hosts_path:
return {
"key_path": key_path,
"known_hosts_path": known_hosts_path,
"mount_targets": mount_targets,
"key_reason": key_reason,
"known_hosts_reason": known_hosts_reason,
}

mount_targets = discover_run_keys_mounts()

if not known_hosts_path:
known_hosts_path, known_hosts_reason = _select_known_hosts_path(mount_targets)
if known_hosts_path:
os.environ["KNOWN_HOSTS_PATH"] = known_hosts_path

if not key_path:
key_path, key_reason = _select_ssh_key_path(mount_targets, known_hosts_path)
if key_path:
os.environ["SSH_KEY_PATH"] = key_path

return {
"key_path": key_path,
"known_hosts_path": known_hosts_path,
"mount_targets": mount_targets,
"key_reason": key_reason,
"known_hosts_reason": known_hosts_reason,
}

def extract_run_id(output: str) -> str:
if not output:
return ""
Expand Down
Loading