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
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.
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The note implies auto-discovery will always work for anything mounted under /run/keys, but the implementation only sets SSH_KEY_PATH/KNOWN_HOSTS_PATH when it can uniquely identify a known_hosts mount and a single key-like mount. Consider clarifying the constraints (expected filenames/uniqueness) and documenting that users can still set SSH_KEY_PATH and KNOWN_HOSTS_PATH explicitly when auto-discovery is ambiguous.

Suggested change
**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.
**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 can auto-discover SSH files mounted under `/run/keys` when it can uniquely identify a `known_hosts` file and a single key-like file, as shown in the configs above (for example, `/run/keys/known_hosts` and `/run/keys/ssh-key.pem`). If you mount multiple candidate key files, use different filenames, or otherwise make auto-discovery ambiguous, set `SSH_KEY_PATH` and `KNOWN_HOSTS_PATH` explicitly in your MCP client configuration.

Copilot uses AI. Check for mistakes.

### 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 mounts from `/proc/self/mounts` and set the internal `SSH_KEY_PATH` and `KNOWN_HOSTS_PATH` values automatically.

Comment on lines +44 to 45
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This states the container will discover mounts and set SSH_KEY_PATH/KNOWN_HOSTS_PATH automatically, but discovery can fail if /run/keys is mounted as a directory or if multiple candidate mounts exist. Consider softening the wording (e.g., “attempts to auto-discover”) and adding a fallback instruction to set SSH_KEY_PATH/KNOWN_HOSTS_PATH explicitly when discovery is ambiguous.

Suggested change
The MCP container will discover these mounts from `/proc/self/mounts` and set the internal `SSH_KEY_PATH` and `KNOWN_HOSTS_PATH` values automatically.
The MCP container attempts to discover these mounts from `/proc/self/mounts` and set the internal `SSH_KEY_PATH` and `KNOWN_HOSTS_PATH` values automatically.
If discovery is ambiguous or does not succeed (for example, if `/run/keys` is mounted as a directory or multiple candidate mounts exist), set `SSH_KEY_PATH` and `KNOWN_HOSTS_PATH` explicitly in the MCP server/container configuration.

Copilot uses AI. Check for mistakes.
### 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.
13 changes: 7 additions & 6 deletions mcp-local/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
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
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,19 +292,20 @@ 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:
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.",
"suggestion": "Mount both the SSH private key and known_hosts file into /run/keys, 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."
"APX looks for SSH_KEY_PATH and KNOWN_HOSTS_PATH first, then auto-discovers mounted files "
f"under /run/keys from /proc/self/mounts. Discovered mounts: {ssh_mount_env['mount_targets']}"
),
}
Comment on lines 294 to 310
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apx_recipe_run now has new configuration behavior (auto-discovering SSH mounts and returning a structured config_validation error with discovered mounts). The integration test suite covers several other tools but does not currently exercise this tool’s config-validation path; adding a test that calls apx_recipe_run without SSH env/mounts and asserts the error shape would help prevent regressions in the new auto-discovery logic.

Copilot uses AI. Check for mistakes.

Expand Down
87 changes: 87 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,91 @@ 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] = []
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)
Comment on lines +273 to +279
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discover_run_keys_mounts() reads /proc/self/mounts via read_text() without handling OSError/UnicodeDecodeError. If the mounts file exists but is unreadable or contains unexpected encoding, this will raise and can crash apx_recipe_run instead of returning the existing structured error response. Consider wrapping the read/parse loop in a try/except and returning an empty list (or an explicit error indicator) on failure.

Suggested change
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)
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 []

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JoeStech This seems like a worthwhile addition


Comment on lines +272 to +280
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-discovery currently depends on mount targets under /run/keys. If users mount a directory (e.g. -v ~/.ssh:/run/keys:ro), /proc/self/mounts will typically only contain /run/keys and not per-file mountpoints, so _select_known_hosts_path/_select_ssh_key_path will fail to resolve paths. Either (a) update discovery to also scan the /run/keys directory contents when /run/keys itself is mounted, or (b) clarify in docs that individual files must be bind-mounted.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is true. I saw that separately and was wondering the same thing. Commented on the exact line below.

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


def _select_known_hosts_path(mount_targets: List[str]) -> Optional[str]:
matches = [
path for path in mount_targets
if "known_hosts" in Path(path).name.lower().replace("-", "_")
]
return matches[0] if len(matches) == 1 else None


def _select_ssh_key_path(mount_targets: List[str], known_hosts_path: Optional[str]) -> Optional[str]:
candidates = [path for path in mount_targets if path != known_hosts_path]
if len(candidates) == 1:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this ends up being a directory, then we would pass that through and continue the mcp tool, as mentioned above

return candidates[0]

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")
)
]
return key_like[0] if len(key_like) == 1 else None
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this line, we end up checking in server.py if ssh path is set. If there is more than one key, then we return None, however in the error message returned to the agent, we don't mention that there can only be one key in the mount targets. And then second to that, I wonder about the candidates above if someone doesn't follow those naming conventions (i.e. renames an rsa key to anything else and the name ends up being ec2, for example).

An option could be to check the beginning of the file contents for the ------BEGIN ... ---- style thing. But that is much slower and maybe not necessary.



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] = []

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

mount_targets = discover_run_keys_mounts()

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

if not key_path:
key_path = _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,
}

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