dumbtv
dumbtv가 ADB로 Fire TV에서 광고·추적·업데이트 알림을 제거, 루트·앱 변경 없이, 무료 오픈소스
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Record every host an MCP server contacts, per tool, and fail CI on new ones. Runs a stdio MCP server behind an embedded CONNECT proxy, writes egress.json, prints Claude Code WebFetch rules, a sandbox allowlist and a Markdown report for pull requests, and warns on proxy bypass and hidden characters in tool metadata.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗What: run a stdio MCP server behind a small recording proxy and get the exact set of hosts it contacts while you run initialize, tools/list and sample tool calls. Every host is attributed to the phase and the tool that reached it, written to egress.json, printed as a table, and printed again as two ready-to-paste blocks: Claude Code WebFetch(domain:host) permission rules and a sandbox allowed-domain list. Keep the file as a baseline and --check fails CI the day a tool starts talking to a host nobody declared.
Who it is for: anyone who installs third-party MCP servers or maintains one, and wants a reviewed list of outbound hosts instead of trusting the README; security reviewers who need the allowlist for a Claude Code sandbox.
pipx install git+https://github.com/basitalisandhu/mcp-egress
mcp-egress run --calls calls.json --baseline egress.baseline.json -- uvx some-mcp-server # record once
mcp-egress run --calls calls.json --baseline egress.baseline.json --check -- uvx some-mcp-server # in CI
A real run against the test fixture in this repository (a local web server on port 39211 stands in for an API):
$ mcp-egress run --calls calls.json -- python fixtures/proxied_server.py
mcp-egress: python fixtures/proxied_server.py
server: proxied-fixture 0.1.0 (protocol 2025-11-25), 6 tool(s), 3 call(s), 1 host(s)
HOST PORT SCHEME FIRST SEEN TOOLS CONNS OUT IN
127.0.0.1 39211 http call:fetch fetch, ping 2 248 B 406 B
Claude Code permission rules (settings.json, permissions.allow):
"WebFetch(domain:127.0.0.1)",
Sandbox allowed domains (settings.json, sandbox.network.allowedDomains):
[
"127.0.0.1"
]
Warnings:
open-world-hint-false ping: declares openWorldHint false but reached 127.0.0.1:39211
Wrote egress.json
Project site: https://basitalisandhu.github.io/mcp-egress/. Related: mcp-tools-lint checks the same server's tools/list schemas and annotations.
Tool calls reach hosts nobody declared. An MCP server's README says it talks to one API; the server is free to talk to anything, and a tool whose definition changed after you installed it can start sending data somewhere new without any visible change in behaviour. People are fighting this by hand: there is an open request to auto-allowlist MCP server domains for code execution network egress in Claude Code, and eight open reports that the sandbox egress allowlist is ignored or collapses to a built-in list. One security checklist for MCP puts it plainly: keep a per-server allowlist of outbound hosts, because a new outbound host is the most reliable signal of a rug pull. An enterprise scorer reported in July 2026 that 184 popular MCP servers changed their tool definitions after publication.
Static scanners read the code and cannot see what a server actually connects to. Runtime agent firewalls wrap the whole agent, not one server, and give you a policy to maintain rather than a list to start from. mcp-egress is the per-server audit in between: run the server once under a proxy, read the list, commit it, and let CI tell you when it grows.
Why the two paste blocks: in Claude Code, only WebFetch(domain:host) permission rules add a host to the sandbox allowed-domain list; a bare rule does not. mcp-egress prints exactly those rules, and the same hosts as a plain JSON array for a sandbox configuration.
127.0.0.1 on an ephemeral port. It accepts CONNECT host:port (the HTTPS path; bytes are tunnelled, never read) and plain absolute-form GET http://host/... requests.HTTP_PROXY, HTTPS_PROXY, ALL_PROXY (and their lower-case forms) pointed at the proxy, NO_PROXY and no_proxy set to an empty string so loopback and private hosts are proxied too, and NODE_USE_ENV_PROXY=1 so the built-in fetch of Node 24 and newer reads those variables. The rest of your environment is inherited unchanged.mcp client runs initialize and tools/list (phase list), then every {tool, arguments} entry of --calls (phase call, tool name recorded).egress.json is written.Hosts are recorded as the server named them (DNS is resolved by the proxy), so the list is a list of names, not IP addresses.
Requires Python 3.11 or newer on Linux or macOS. PyPI publication is pending, so install from the repository:
pipx install git+https://github.com/basitalisandhu/mcp-egress # isolated CLI install
uvx --from git+https://github.com/basitalisandhu/mcp-egress mcp-egress --help # run without installing
pip install git+https://github.com/basitalisandhu/mcp-egress # into the current environment
git clone https://github.com/basitalisandhu/mcp-egress && cd mcp-egress && uv venv && uv pip install -e ".[dev]" # development
Once the package is on PyPI the short forms work too: pipx install mcp-egress, uvx mcp-egress --help, pip install mcp-egress. The only runtime dependency is mcp (2.0 or newer).
Each release tag publishes ghcr.io/basitalisandhu/mcp-egress for linux/amd64 and linux/arm64, tagged with the version and latest. The image runs as uid 1000 with /work as the working directory:
docker run --rm -v "$PWD:/work" ghcr.io/basitalisandhu/mcp-egress:0.3.1 \
run --calls calls.json --baseline egress.baseline.json --check --out egress.json -- python server.py
mcp-egress run starts the MCP server inside the container, so the server command has to work there. The image contains Python 3.12 and the mcp package and nothing else: a Python server in the mounted directory that needs only mcp and the standard library runs as is. Servers started with npx, uvx or another runtime need an image of your own that adds that runtime (FROM ghcr.io/basitalisandhu/mcp-egress:0.3.1), or a local install of mcp-egress. The baseline, --check and the egress.json document behave the same in the container; the mounted directory must be writable by uid 1000.
The image is signed with a keyless cosign signature and has a build provenance attestation and an SPDX SBOM (attached to the GitHub Release). To verify:
cosign verify ghcr.io/basitalisandhu/mcp-egress:0.3.1 \
--certificate-identity-regexp '^https://github.com/basitalisandhu/mcp-egress/\.github/workflows/publish-github-packages\.yml@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
gh attestation verify oci://ghcr.io/basitalisandhu/mcp-egress:0.3.1 --repo basitalisandhu/mcp-egress
Once published to PyPI:
pip install mcp-egress
mcp-egress diff old-egress.json new-egress.json
mcp-egress diff --format json old-egress.json new-egress.json
diff runs no MCP server and changes neither file. It shows globally added and
removed host:port keys, the tools that reached each in the appropriate document,
and tools whose host sets changed. Hosts without tool attribution are labelled
explicitly. Host case, duplicate host rows and host-list ordering do not create
changes; repeated tool names are aggregated.
With --format json (default is text) the same content is printed as a JSON
object (keys sorted) so CI scripts can read it without parsing the text form.
For an old.example to new.example move of the fetch tool:
{
"added_hosts": {
"new.example:443": [
"fetch"
]
},
"changed_tools": {
"fetch": {
"added": [
"new.example:443"
],
"removed": [
"old.example:443"
]
}
},
"ok": false,
"removed_hosts": {
"old.example:443": [
"fetch"
]
}
}
Exit codes are 1 when a globally new host appears, 0 otherwise (including removal-only
or tool-only changes), and 2 for unreadable/malformed documents. Both files need a
hosts array; malformed tool host lists are reported rather than causing a traceback.
This makes baseline-update reviews possible without rerunning an agent or server.
mcp-egress run [options] -- <command> [args...]
Options go before --; everything after -- is the server command, exactly as you would put it in an MCP client configuration.
| Option | What it does |
|---|---|
--calls FILE | JSON file of calls to make after tools/list: a list of {"tool": name, "arguments": {...}}, or an object with a calls list. Without it only initialize and tools/list run. |
--out FILE | Where to write the run document. Default egress.json. |
--baseline FILE | Without --check: also save this run as the baseline. With --check: the baseline to compare against (never modified). |
--check | Exit 1 when a host:port seen in this run is not in the baseline. Hosts in the baseline that were not seen are reported, not failed. |
--timeout SECONDS | Per-request timeout for initialize, tools/list and each call. Default 30. A call that times out is recorded as failed; the run continues. |
--format text|markdown | Report format on stdout. Default text. markdown prints the same content as GitHub-flavoured Markdown (see below) and moves the Wrote ... status lines to stderr, so stdout can be redirected straight into a pull request comment or $GITHUB_STEP_SUMMARY. |
--server-stderr FILE | Write the server's stderr to a file instead of passing it through. |
Exit codes: 0 ok, 1 new hosts with --check, 2 usage error (bad options, unreadable calls file, missing baseline), 3 the server could not be started or did not get through initialize and tools/list.
A first run saves the baseline:
mcp-egress run --calls calls.json --baseline egress.baseline.json -- uvx some-mcp-server --api-key "$KEY"
Every later run in CI checks against it:
mcp-egress run --calls calls.json --baseline egress.baseline.json --check -- uvx some-mcp-server --api-key "$KEY"
calls.json holds the tool calls that exercise the server's network paths. Tools that are never called cannot reach anything, so call each tool at least once with representative arguments:
[
{"tool": "fetch", "arguments": {"url": "http://127.0.0.1:39211/items"}},
{"tool": "tunnel", "arguments": {"url": "http://127.0.0.1:39211/health"}},
{"tool": "ping", "arguments": {"url": "http://127.0.0.1:39211/"}},
{"tool": "echo", "arguments": {"text": "no network"}}
]
The run below is real output against the test fixture in tests/fixtures/proxied_server.py, a small server whose tools fetch URLs with urllib (which honours proxy variables), with a local HTTP server standing in for an API. The ping tool declares openWorldHint: false and fetches anyway, which is the contradiction the audit flags.
$ mcp-egress run --calls calls.json --baseline baseline.json -- python fixtures/proxied_server.py
mcp-egress: python fixtures/proxied_server.py
server: proxied-fixture 0.1.0 (protocol 2025-11-25), 6 tool(s), 4 call(s), 2 host(s)
HOST PORT SCHEME FIRST SEEN TOOLS CONNS OUT IN
127.0.0.1 39211 http call:fetch fetch, ping 2 243 B 278 B
127.0.0.1 39211 https call:tunnel tunnel 1 66 B 139 B
Claude Code permission rules (settings.json, permissions.allow):
"WebFetch(domain:127.0.0.1)",
Sandbox allowed domains (settings.json, sandbox.network.allowedDomains):
[
"127.0.0.1"
]
Warnings:
open-world-hint-false ping: declares openWorldHint false but reached 127.0.0.1:39211
Wrote egress.json
Baseline saved to baseline.json
SCHEME is https for connections that arrived as CONNECT (the proxy tunnels them without reading) and http for absolute-form requests forwarded in the clear. FIRST SEEN is the phase and tool of the first connection; TOOLS lists every tool that reached the host.
The same run checked against a baseline that only knows api.example.com:443:
$ mcp-egress run --calls calls.json --baseline old-baseline.json --check --out egress.json -- python fixtures/proxied_server.py
...
Baseline old-baseline.json: 1 new host(s):
127.0.0.1:39211 (tools: fetch, ping, tunnel)
Wrote egress.json
Exit code 1. The second fixture, tests/fixtures/socket_server.py, fetches with a raw http.client connection that ignores proxy variables:
$ mcp-egress run --calls calls.json --out socket.json -- python fixtures/socket_server.py
mcp-egress: python fixtures/socket_server.py
server: socket-fixture 0.1.0 (protocol 2025-11-25), 2 tool(s), 4 call(s), 0 host(s)
No proxied connections were seen.
Claude Code permission rules (settings.json, permissions.allow):
# no hosts seen, nothing to allow
Sandbox allowed domains (settings.json, sandbox.network.allowedDomains):
[]
Warnings:
possible-proxy-bypass fetch: result contains '200 OK' but no proxied connection was seen during the call; the server may ignore proxy variables
Wrote socket.json
The first block goes into the permissions.allow array of a Claude Code settings file (.claude/settings.json in the project or ~/.claude/settings.json):
{
"permissions": {
"allow": [
"WebFetch(domain:127.0.0.1)"
]
}
}
The second block is the same hosts as a JSON array, for a sandbox allowed-domain list such as sandbox.network.allowedDomains in Claude Code settings or the allowlist of whatever sandbox runs your agent. Hosts are printed without ports and without wildcards; add *. prefixes yourself if a service uses many subdomains.
| Code | Meaning |
|---|---|
open-world-hint-false | A tool annotated openWorldHint: false reached a host. Annotations are the server's own claims; this one was false. |
outside-declared-base-url | The server command line contains a URL (for example --base-url https://api.example.com), and a tool reached a host other than the ones declared there. Only printed when the command line declares at least one URL. |
hidden-characters | A tool's name, title or description from tools/list contains characters that render as nothing: zero-width spaces and joiners, bidi embeddings, overrides and isolates, the byte order mark, the soft hyphen, variation selectors from the supplement block, or Unicode tag characters (U+E0000 to U+E007F). The message names each code point and, for tag characters, quotes the ASCII text they spell out, because that is how instructions are hidden from a human reviewer. Checked for every listed tool, called or not, and needs no network. Parameter descriptions inside inputSchema are not checked yet. |
possible-proxy-bypass | A tool call succeeded and its result contains an http:// or https:// URL, a status line such as 200 OK or HTTP/1.1 404 Not Found, or a status field such as status: 503, while no proxied connection was seen during that call. The server probably reached the network without honouring the proxy variables. See the limits below. |
The document is the baseline format and the input for your own scripts. Its JSON Schema is schema/egress.schema.json and the test suite validates every run against it.
| Key | Content |
|---|---|
format_version, generator | 1, and the tool name and version that wrote the file. |
command, started, finished, proxy | The server command, UTC timestamps, and the proxy URL the server was given. |
server | name, version and negotiated protocol_version from initialize, or null. |
hosts | One entry per host, port and scheme: host, port, scheme, phase and tool of the first connection, first_seen, bytes_out, bytes_in, connections, the phases and tools that reached it, and addresses: distinct connected IP strings in first-seen order (empty when no connection succeeded). The field is optional in the schema and is present in files written by 0.1.1 and later; older files without it are still valid and --check accepts them as baselines. Addresses are observations, not a baseline check; --check compares only host:port. |
tools | Every tool from tools/list with name, title, description, annotations (as the server sent them) and the host:port list it reached. |
calls | One entry per --calls entry: tool, arguments, ok, is_error, error, hosts, network_evidence (the fragment that looked like network output, if any) and duration_ms. |
warnings | {code, tool, message} entries, the same ones printed. |
check | null, or {baseline, ok, new_hosts, missing_hosts} when --check ran. |
--check compares host:port keys only; byte counts, tools and phases may change freely. A baseline needs nothing more than a hosts list, so you can also write one by hand.
name: mcp-egress
on: [pull_request]
permissions:
contents: read
jobs:
egress:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pipx install git+https://github.com/basitalisandhu/mcp-egress
- run: mcp-egress run --calls calls.json --baseline egress.baseline.json --check --out egress.json -- uvx some-mcp-server
env:
SOME_API_KEY: ${{ secrets.SOME_API_KEY }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: egress
path: egress.json
Or use the composite action in this repository, which installs the tool, runs the check, sets out and new-hosts outputs and still fails the step on a new host (Linux and macOS runners):
- uses: actions/checkout@v7
- id: egress
uses: basitalisandhu/mcp-egress/[email protected]
with:
command: uvx some-mcp-server
calls: calls.json
baseline: egress.baseline.json # the default
check: true # the default; false records the baseline instead
env:
SOME_API_KEY: ${{ secrets.SOME_API_KEY }}
- if: always()
run: echo "new hosts: ${{ steps.egress.outputs.new-hosts }}"
Inputs: command (required, split like a shell would), calls, baseline, check, out (default egress.json), python-version (default 3.12) and version (a PyPI version to install; leave it empty to install from the action's own checkout, which is the only option until the package is on PyPI). The action installs into its own virtual environment and never sets a proxy variable; the tool does that for the server it starts.
To show the result on the run page or in a pull request comment, add --format markdown and send stdout to the step summary. The exit code is the same, so the job still fails on a new host:
- run: mcp-egress run --format markdown --calls calls.json --baseline egress.baseline.json --check --out egress.json -- uvx some-mcp-server >> "$GITHUB_STEP_SUMMARY"
A real Markdown report, from the fixture run above checked against a baseline that only lists api.example.com:443 (exit code 1):
## mcp-egress report
Command: `python fixtures/proxied_server.py`
Server: proxied-fixture 0.1.0 (protocol 2025-11-25), 6 tool(s), 3 call(s), 1 host(s)
### Hosts
| host | port | scheme | first seen | tools | connections | bytes out | bytes in |
|---|---:|---|---|---|---:|---:|---:|
| 127.0.0.1 | 39211 | http | call:fetch | fetch, ping | 2 | 248 B | 406 B |
...
### Baseline check
Baseline `old.json`: 1 new host(s):
- `127.0.0.1:39211` (tools: fetch, ping)
When a tool legitimately needs a new host, re-run without --check and commit the new baseline in the same pull request as the change that needed it, so the review sees the host and the reason together.
mcp-egress sees only connections that go through the proxy, and a server goes through the proxy only if its HTTP stack honours the proxy environment variables. Most do: Python's urllib, requests and httpx, Go's net/http, curl, and the built-in fetch of Node 24 and newer when NODE_USE_ENV_PROXY=1 is set (mcp-egress sets it). Some do not: raw sockets, custom HTTP stacks, older Node runtimes, and any client created with proxy support switched off. A server like that reaches the network directly and mcp-egress records nothing for it.
The possible-proxy-bypass warning is the only signal you get in that case, and it is a heuristic: it fires when a call succeeded with network-shaped output and no connection was seen. It cannot fire for a tool that reaches the network and returns nothing that looks like it, and it can fire for a tool that merely echoes a URL you passed in. Treat an empty host list for a tool that clearly talks to an API as a finding in itself.
Other things this tool does not observe:
CONNECT tunnel. Hosts and ports, yes; paths, headers and payloads, no, by design.calls.json.A Linux network-namespace mode that captures every connection regardless of proxy support is the planned answer to the first point of the list; see the roadmap.
Real messages and what to do about them.
mcp-egress: command not found after pipx install. Run pipx ensurepath and open a new shell.
error: cannot start 'some-server': [Errno 2] No such file or directory: 'some-server'. The command after -- is not on PATH. Use the full path, or uvx / npx -y with the package name.
error: server failed during list: Connection closed. The server started and then exited, usually because of a missing environment variable or argument. Add --server-stderr server.log and read the log; run the same command by hand to see the error.
mcp-egress: error: --check needs --baseline FILE and error: baseline egress.baseline.json does not exist. --check compares with a baseline, so record one first: the same command without --check writes it. Commit the file so CI can read it.
error: baseline calls.json is UTF-16 text; save it as UTF-8, is not UTF-8 text (byte 22) or is nested too deeply to read. Baselines and calls files are UTF-8 JSON. PowerShell's > writes UTF-16; use Out-File -Encoding utf8. A UTF-8 byte order mark is fine. Before 0.3.0 these crashed with a traceback (deep nesting) or printed a decoder error without the file name (calls files).
No proxied connections were seen for a server that obviously calls an API. The server's HTTP client ignores HTTPS_PROXY: raw sockets, a custom stack, or Node older than 24. See The proxy-bypass warning and its limits.
How do I find out which hosts an MCP server connects to?
Run it once under mcp-egress run with a calls.json that exercises each tool: mcp-egress run --calls calls.json -- uvx the-server. The table lists every host and port the server contacted, which tool reached it, and how many bytes went each way; egress.json holds the same data for scripts. The method is a recording proxy plus the standard proxy environment variables, so it works for any server whose HTTP client honours HTTPS_PROXY, in any language, with no code changes and no root privileges.
How do I allowlist an MCP server's domains in Claude Code?
Run the server through mcp-egress and paste the printed WebFetch(domain:host) rules into the permissions.allow array of your settings file. The Claude Code permissions documentation says those rules are what adds a host to the sandbox allowed-domain list. mcp-egress also prints the hosts as a JSON array for sandbox.network.allowedDomains or any other sandbox configuration. Re-run with --baseline so the next change to the list is a reviewed diff rather than a surprise.
Can a server hide its traffic from mcp-egress?
Yes, if it ignores proxy variables: raw sockets, a custom HTTP stack, or a Node runtime older than 24 all connect directly and leave no record. mcp-egress prints possible-proxy-bypass when a tool's result looks like network output and no connection was seen, and prints No proxied connections were seen when the whole run was silent, but both are hints, not proof. Treat an empty host list for a tool that obviously talks to an API as a finding. A network-namespace mode that captures every connection on Linux is on the roadmap.
Does mcp-egress see the content of requests, or my credentials?
No. HTTPS goes through the proxy as a CONNECT tunnel, and the proxy copies bytes without reading them; only the host, the port and the byte counts are recorded. Plain http:// requests are forwarded with their headers but nothing from them is stored either. egress.json contains the server command line as you typed it, so if you pass secrets as arguments rather than through the environment, they will be in the file; prefer the environment, which is inherited by the server and never written anywhere.
Issues and pull requests are welcome; see CONTRIBUTING.md and the written-out starter issues in docs/good-first-issues.md. Run make check (ruff and pytest) before opening a pull request. Security problems: see SECURITY.md.
More tools by the same author: https://github.com/basitalisandhu
tools/list surface of an MCP server for schema dialect, annotation and naming problems.MIT, see LICENSE. Copyright 2026 Muhammad Basit Ali.