Check MCP tools in CI
The pre-deploy check is for teams that build their own MCP server. It keeps the approved tool list in the server’s repository as stackduke.lock.json, and fails a pull request when the tools change without approval or when a tool’s text trips one of the scanner’s checks. It is part of the open-source MCP scanner, and runs in your CI, on your runners. Nothing is sent to Stackduke, and it does not need a Stackduke account.
This is a different thing from Review tool changes, which covers servers connected to the Stackduke dashboard. The pre-deploy check catches a change before your server ships; the dashboard review catches one in a server your agents already use.
Before you begin
Section titled “Before you begin”- Node 22.18 or later, wherever the check runs. The GitHub Action sets it up if the runner has nothing recent enough.
- A command that starts your server as a local program speaking MCP over standard input and output, such as
node dist/server.js, or a web address for it: https, or http onlocalhostfor a server started earlier in the same job.
Set it up
Section titled “Set it up”-
Write the first approved list. In the server’s repository, run the check with
--update. Read thestackduke.lock.jsonit writes before you commit it: it is the list every later change is compared with.Terminal window npx @stackduke/mcp-scanner check --update -- node dist/server.jsEverything after
--is the command that starts your server, passed as it is. For a server at an address, give the address instead:npx @stackduke/mcp-scanner check --update http://localhost:3000/mcp. If it lists findings and writes nothing, see Fix or accept a finding. -
Add the check to pull requests. For GitHub, add
.github/workflows/mcp-tools.yml. Change the build step andcommandto match your server.name: MCP toolson: pull_requestpermissions:contents: readpull-requests: write # only for comment: "true"jobs:check:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v7with:persist-credentials: false # keeps the job's token off the disk the server can read- run: npm ci && npm run build- uses: Stackduke/mcp-scanner@v0 # or a fixed release such as @v0.3.0with:command: node dist/server.jscomment: "true"With
comment: "true", the summary is posted on the pull request and updated in place on later runs. It is always written to the job summary. -
Make it required. In the repository’s branch protection, make the
checkjob a required status check, so a pull request that changes the tools cannot merge until the change is approved.
Approve a change
Section titled “Approve a change”When a pull request changes the tools, the check fails and its summary lists what changed and what failed. Once a person has reviewed the change and it is expected:
-
Run the same check with
--update, on the pull request’s branch:Terminal window npx @stackduke/mcp-scanner check --update -- node dist/server.js -
Commit the updated
stackduke.lock.jsonin the same pull request. Reviewers see each changed definition, as the server gives it, in the lockfile’s diff, which is the record of what was approved.
--update writes nothing while any finding is open and no one has accepted it: a high or medium text check, or a tool that can change things with no risk label. Approving a tool change therefore cannot also approve, in passing, a description that tells the model to ignore its instructions. The same holds for the first --update in step 1 of the set-up.
Fix or accept a finding
Section titled “Fix or accept a finding”Fix a finding in the server where you can: for a missing risk label, set readOnlyHint (the tool only reads) or destructiveHint on the tool; for a text check, change the tool’s text. Text checks are patterns, and a label guessed from a name can be wrong, so some findings are false alarms. If a person has checked a finding and it is one, approve it explicitly:
npx @stackduke/mcp-scanner check --update --accept-findings -- node dist/server.jsThe finding is recorded under that tool’s accepted in the lockfile, where reviewers see it in the diff. It stays accepted only while the tool’s definition stays exactly the same.
What fails the check
Section titled “What fails the check”- A tool was added, removed or changed and the lockfile was not updated. “Changed” covers the name, title, description, input schema, output schema and annotations. A renamed tool counts as one removed and one added.
- A tool name is listed more than once.
- A tool’s text trips a high or medium text check: instructions aimed at the model, invisible characters, pointers to credential stores, requests to send data elsewhere, annotations that contradict the name, and similar. Low findings are listed as notes and never fail the check.
- A tool counted as able to change things, or one whose name says nothing the scanner understands, carries neither
readOnlyHintnordestructiveHint. A tool counted as only reading needs neither.
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
| 0 | The tools match the lockfile and nothing fired. |
| 1 | The check failed, or there is no lockfile yet. |
| 2 | The arguments are wrong. |
| 3 | The server at the address asked for a sign-in. The check sends no credential. |
| 4 | The server could not be listed. |
Options
Section titled “Options”| Option | What it does |
|---|---|
--lockfile=<path> |
Where the approved list is. Default stackduke.lock.json. |
--summary=<file> |
Also write the Markdown summary to a file. |
--timeout=<seconds> |
How long to wait for each reply. Default 60 for a program, 15 for an address. |
--update |
Approve the current tools. |
--accept-findings |
With --update, approve findings a person judged false alarms. |
--command="<line>" |
The server’s command as one line, instead of after --. |
The GitHub Action takes url or command, lockfile, working-directory, timeout, comment and github-token, and sets the output result to passed, failed or error.
GitLab CI
Section titled “GitLab CI”mcp-tools: image: node:22 rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" script: - npm ci && npm run build - npx --yes @stackduke/mcp-scanner@0.3 check --summary=mcp-tools.md -- node dist/server.js artifacts: when: always paths: [mcp-tools.md]The job fails when the check does, and blocks the merge request when the project requires pipelines to pass. The check does not post its summary as a merge-request comment.
Limits
Section titled “Limits”- The check runs the server you give it, with the job’s environment less GitHub’s and GitLab’s own tokens and workflow files. That narrows what the server can reach; it is not a sandbox. Give it only a server you would run anyway.
- Do not run it on
pull_request_targetto get comments on pull requests from forks. That event gives a pull request’s code a token that can write. - It makes changes visible to reviewers. It does not stop someone who means harm, because the author of a pull request controls both the server’s code and the lockfile. Both show in the diff.
- It sees the tools a server lists at start-up with the settings the job gives it. Tools a server adds later, or only for some accounts, are not seen.
- A pass means nothing changed and nothing matched, not that the server is safe.
- One lockfile holds one server. A repository with several servers runs the check once for each, with
--lockfile.