Cloud Security in your IDE (MCP)¶
The LimaCharlie MCP server exposes Cloud Security to any Model Context Protocol client — Claude Code, Cursor, and others — so an AI assistant can read your cloud posture, triage findings, and, for the AppSec code lane, scan the working copy on your own machine before anything is pushed.
This page covers the setup and the four code-lane tools. The rest of the Cloud Security tool surface mirrors the command line interface one-for-one.
Setup — Claude Code¶
git clone https://github.com/refractionPOINT/lc-mcp-server
cd lc-mcp-server
go build -o lc-mcp-server ./cmd/server
claude mcp add limacharlie-cloudsec \
--env LC_OID=<your-organization-id> \
--env LC_API_KEY=<your-api-key> \
--env MCP_MODE=stdio \
--env MCP_PROFILE=cloud_security \
-- /absolute/path/to/lc-mcp-server
/mcp in a session lists the server and its tools.
Setup — Cursor¶
Add the server to ~/.cursor/mcp.json, or to .cursor/mcp.json inside a project to scope the
credential to one repository:
{
"mcpServers": {
"limacharlie-cloudsec": {
"command": "/absolute/path/to/lc-mcp-server",
"args": [],
"env": {
"LC_OID": "<your-organization-id>",
"LC_API_KEY": "<your-api-key>",
"MCP_MODE": "stdio",
"MCP_PROFILE": "cloud_security",
"LOG_LEVEL": "warn"
}
}
}
}
LOG_LEVEL=warn matters more than it looks: the server logs to stderr, and in stdio mode a chatty
stderr is noise in the client's transport log.
Profiles¶
MCP_PROFILE decides which tools the client is offered. For Cloud Security work:
| Profile | What it exposes |
|---|---|
cloud_security |
Every Cloud Security tool, including the triage writes |
cloud_security_readonly |
The reads only — for a session that must not be able to change anything |
all |
The whole platform |
A narrow profile is not just tidiness. An assistant chooses from what it is shown, so a session that
only needs to read posture is both cheaper and safer with cloud_security_readonly.
Permissions¶
Reads need cloudsec.get; the triage writes and the code ingest need cloudsec.set. The whole
surface also requires the organization to be subscribed to the ext-cloud-security extension — a
403 saying cloud security is not enabled means exactly that, and the tools say so in their errors.
The code-lane tools¶
| Tool | What it does |
|---|---|
cloudsec_code_repos |
The repositories the code lane sees, with scan state and the open-finding rollup |
cloudsec_code_findings |
Findings for one or more repositories, or the cross-filtered facet counts |
cloudsec_code_scan_local |
Scans a working copy on your machine with the same scanner the hosted lane runs |
cloudsec_code_autofix |
Opens the dependency fix pull request for an SCA finding |
Before they can return anything¶
Code scanning is opt-in per organization, and two things must be true:
- A source-control provider is connected (a
cloudsec_providerrecord). - A
code_scanningrecord exists in thecloudsec_policyhive, naming the repositories in scope and the engines that run.
Both are hive records — see Code Scanning for the policy's fields. An empty answer from a code tool usually means one of those two is missing, not that your code is clean, and the tools say so rather than implying an all-clear.
Reading findings¶
cloudsec_code_findings will not list without at least one repo. That is deliberate: the findings
backend has no "any repository" selector, so dropping the constraint would return your whole
findings worklist — cloud findings included — under a tool named for the code lane. Filtering by
class does not scope it either, because vulnerability, misconfig and malware are shared with
the cloud lane.
The unscoped mode is facets: true, which is honest because the repo facet counts only findings
that have a repository. So the natural order is:
cloudsec_code_findings { "facets": true } → which repositories carry what
cloudsec_code_repos { "has_findings": true } → the same per repository, with scan state
cloudsec_code_findings { "repo": ["owner/name"], "severity": ["CRITICAL", "HIGH"] }
source narrows the same read by producer — hosted for the scan LimaCharlie ran, ingest for a
document your pipeline pushed, other for the source control's own detectors, none for a finding
with no code provenance, or both. It is applied inside the query, so the facet counts describe the
estate rather than the page you happen to be holding.
repo is matched exactly against a key whose owner and name segments are both ASCII
lower-cased, while a finding's code.repo_name is the source-control platform's display casing.
The filter folds what you pass, so a key read straight off a finding now scopes the read rather
than silently matching nothing. A key that is genuinely wrong still returns an empty page, and an
empty page under a single repo filter carries a note saying which case you are in: the key is
right and nothing matched, the key is wrong and here is the real one, or no such repository is in
the inventory.
Scanning your working copy¶
This runs on your machine, not in LimaCharlie: it needs Docker and the
limacharlie CLI on PATH, and it takes minutes rather than seconds. Nothing about the
checkout leaves the machine, and without ingest nothing leaves it at all — the result says so
explicitly, so a scan that found plenty is not misread as a clean estate.
Because it runs a container locally, it is available only when the server is running in stdio mode. A hosted MCP deployment refuses it.
scanners defaults to sca,iac,licenses; sast and images also run locally. Secret scanning
does not, and asking for it is an error rather than a silent omission: a credential's identity in
this pipeline is a digest keyed by a value only the hosted lane holds, so locally-found secrets
would neither deduplicate against a hosted scan's nor be accepted by the ingest. Use the hosted lane
for secrets.
With ingest: true and a repo, the report is pushed to your organization, where it deduplicates
against the hosted scan by identity — the report format is loss-free, so it lands on exactly the
rows a hosted scan of the same repository would write, and re-pushing an identical report writes
nothing. A pushed report can only close findings it previously reported, never one the hosted
scanner found.
Pass output_path alongside ingest. The report otherwise exists only for the duration of the
scan, so a push that fails — not subscribed, no enabled code_scanning policy selecting the
repository, a quota, a transient error — costs the whole scan again.
cloudsec_code_autofix¶
Opens the pull request that raises a vulnerable dependency to its fixed version, for one finding:
finding_id is required and is the only input that decides anything: the
backend resolves it against the dependency rows its own scan produced and raises
that package to that advisory's fixed version, so there is no way to name a
package or a version. repo and provider are optional search hints.
It needs the separate, opt-in Code Actions App on the connection — the read-only collection App never gains write access.
The tool answers as soon as the request is accepted; the clone, the edit and the pull request happen minutes later in a sandbox, so the pull request is the result. Read it in the repository rather than in the tool's reply.
A refusal does not come back on this call
Because the call has already answered, every reason a fix does not happen is
a quiet no-op here: no Code Actions App (write_app_not_configured) or one
lacking Contents: Read and write (write_app_lacks_contents), a package
flagged malicious, no published fixed version, an unsupported ecosystem, a
repository outside the policy scope or over the free-tier quota, a pull
request already open for that package, or the daily limit.
None of these appear in the reply. They surface as the
cloudsec.code_autofix_refused operational event — that is where to look
when no pull request appears.
See Code Scanning for the setup, the npm registry-metadata policy, and the Go lockfile caveat that decide whether the pull request is complete on its own.
See also¶
- Code Scanning — the lane these tools read
- Command Line Interface — the same surface, without an assistant