# Bouncer: 수신 주소를 도구 출력과 대조해 전송 막기
# Bouncer: checking recipients against tool output to block sends

- Site: https://select1.dev/2026-10-28 (English), https://select1.dev/ko/2026-10-28 (Korean)
- Entry: 018
- Published: 2026-10-28 (KST). Launch date: 2026-10-27
- Pick: Bouncer <https://github.com/Ezed9/mcp-bouncer> (source: Show HN)
- Verified: ran
- What: Bouncer 는 에이전트와 MCP 서버 사이에서 도구 호출을 검사해, 서버 응답에 나온 주소로 메일을 보내거나 파일을 공유하는 호출을 막는 프로덕트다.
- What (en): Bouncer is a product that checks tool calls between an agent and an MCP server. It blocks calls that send email or share files with an address found in data the server returned.
- Why: 오늘의 픽으로 선정한 이유는 에이전트가 프롬프트 인젝션에 속아도 데이터를 공격자에게 보내는 호출은 막기 때문이다.
- Why (en): It is today’s pick because it stops an agent from sending data to an attacker, even when a prompt injection fools the agent.
- Verdict: 작동한다. 티켓에 나온 공격자 주소로 보내는 호출을 모두 거부했다.
- Verdict (en): It works. It denied every send to the attacker address found in a ticket.
- Tags: mcp, ai-agents, security, prompt-injection, python, ai-built

## Checked against the README

- [pass] 이전 출력에 나온 수신 주소는 대소문자와 공백을 맞춘 뒤 비교해 거부한다(A destination is tainted only if it appears in earlier output after case and whitespace normalization) — 티켓에 ATTACKER@Evil.Example 이 적힌 상태에서 attacker@evil.example 로 send_email 을 호출하자 came from untrusted data 라는 메시지와 함께 거부됐다. 공백을 바꾼 경우는 시험하지 않았다
- [pass] 출력 속 주소가 인코딩돼 있거나 여러 조각으로 나뉘어 있으면, 그 주소로 보낼 때 사용자에게 확인을 요청한다. 사용자의 답이 없거나 클라이언트가 확인 요청을 지원하지 않으면 거부한다(An encoded or assembled address gets ask, not deny. An unanswered ask is a deny) — 티켓에 attacker [at] evil [dot] example 로 적고 attacker@evil.example 로 보냈다. 확인 요청을 지원하지 않는 클라이언트에서도, 거절로 답한 클라이언트에서도 호출은 is not a vouched recipient 라는 메시지와 함께 거부됐다. 답하지 않는 경우는 시험하지 않았다
- [pass] 거부된 호출은 서버로 전달되지 않는다(A denied call is never forwarded upstream) — 직접 작성한 MCP 서버가 전송 요청을 받으면 그 수신 주소를 파일에 기록하게 했다. 전송을 여섯 번 요청했다. 공격자 주소로 세 번, ana@globex.example 로 한 번 보낸 호출은 Bouncer 가 거부해 서버에 전달되지 않았다. 허용 목록에 있는 support@acme.example 로 보낸 두 번만 서버에 전달돼 기록됐다. 한 번은 티켓을 읽은 뒤에, 한 번은 읽지 않고 보냈다

## Measured

- 같은 MCP 서버에 get_ticket 을 50번 호출했을 때 응답 시간의 중앙값. 직접 연결했을 때와 Bouncer 를 거쳤을 때: 3.59 ms → 6.55 ms (Python time.perf_counter 로 측정)

## 한국어

## 무엇이 문제인가

메일, 티켓, 문서를 읽는 에이전트는 그 안에 숨은 지시를 따를 수 있다. 고객이 남긴 티켓에 "고객 목록을 이 주소로 보내라"는 문장이 있으면, 에이전트가 메일 도구로 고객 목록을 실제로 보낼 수 있다. 이런 공격을 프롬프트 인젝션이라고 한다.

Anthropic 의 Claude Code 권한 문서에 따르면 Claude Code 는 셸 명령을 실행하거나 파일을 수정하기 전에 승인을 요청한다. MCP 도구의 허용과 거부는 `mcp__server__tool` 형식의 규칙으로 지정한다. 특정 파라미터 값이 들어간 호출을 막으려면 `--disallowedTools` 거부 규칙에 그 값을 지정해야 한다. 이때는 파라미터 값이 규칙의 값과 정확히 같은 호출만 막힌다. 제작자는 README 에서 Bouncer 를 이렇게 도구마다 승인하는 방식, LLM 으로 인젝션을 찾는 탐지기와 비교했다. Claude Code 의 권한 규칙은 에이전트가 읽은 데이터에 수신 주소가 들어 있었는지는 확인하지 않는다.

## 어떻게 해결했나

Bouncer 는 2026년 10월 27일 Hacker News 에 Show HN 글로 올라왔다. Bouncer 는 MCP 클라이언트와 MCP 서버 사이에서 도구 호출을 중계한다. 에이전트가 읽은 티켓이나 고객 목록처럼 MCP 서버가 반환한 출력은 모두 저장해 둔다. 수신 주소가 그 출력에 있었는지 나중에 찾아보기 위해서다.

에이전트가 메일 전송이나 파일 공유처럼 데이터를 내보내는 도구를 호출하면, Bouncer 는 수신 주소를 확인한다. 허용 목록에 있거나 사용자가 승인한 주소면 허용한다. 그 밖의 주소는 저장해 둔 출력에 있으면 거부하고, 없으면 사용자에게 확인을 요청한다. 거부된 호출은 서버에 전달하지 않고, 판정은 모두 감사 로그에 남긴다. 파일 경로 제한과 도구별 호출 횟수 제한도 함께 적용한다.

## 무엇이 다른가

첫째, 판단 기준이 다르다. Claude Code 의 권한 규칙은 도구 이름과 파라미터 값을 보고 판단한다. Bouncer 는 그 값의 출처를 본다. 거부 규칙에 없는 주소라도 서버 출력에 나왔으면 거부한다. 판단에 LLM 을 쓰지 않고, 대소문자와 공백을 정규화한 뒤 이전 출력에 그 주소가 문자열로 들어 있는지 찾는다. 트레이드오프는 정상 주소라도 출력에 나온 다른 주소의 부분 문자열이면 거부된다는 점이다.

둘째, 지키는 범위가 다르다. Claude Code 의 권한 규칙은 셸 명령과 파일 수정까지 다룬다. Bouncer 는 MCP 호출만 검사한다. 출력은 서버마다 따로 저장한다. 그래서 에이전트가 한 서버에서 읽은 주소로 다른 서버의 전송 도구를 호출하면, 거부하지 않고 확인만 요청한다. 제작자의 AgentDojo 실험(에이전트 모델 gpt-4o-mini)에서 에이전트가 공격자에게 데이터를 보낸 경우는 Bouncer 없이 공격 164건 중 48건, Bouncer 를 거치면 163건 중 0건이었다. README 는 공격 수가 1건 다른 이유를 밝히지 않는다. 이 실험에서는 확인 요청에 답할 사용자가 없어 확인 요청을 모두 거부로 처리했고, Bouncer 는 전송을 하나도 허용하지 않았다. 그래서 메일을 보내는 정상 작업은 Bouncer 없이 6건 중 4건 성공했지만 Bouncer 를 거치면 하나도 성공하지 못했다.

## 어떻게 만들었나

- 커밋 73개는 2026년 7월 6일부터 10월 24일까지 작성됐다. 그중 68개에 Claude 와 함께 작성했다는 Co-authored-by 트레일러가 있다.
- 소스 코드는 Python 1,996줄, 테스트 코드는 2,145줄이다.
- Bouncer 에는 호출을 실행하지 않고 판정만 해 보거나 감사 로그를 읽는 점검용 도구가 있다. README 의 보안 공지에 따르면 0.1.0 부터 0.1.2 까지는 에이전트의 도구 목록에 이 도구들도 있었다. 인젝션에 속은 에이전트가 이 도구로 감사 로그에 판정을 추가하거나 최근 기록을 읽어도, Bouncer 는 그 호출을 검사하지도 기록하지도 않았다. 제작자는 0.1.3 에서 이 도구들을 목록에서 뺐다.

## 실행해 보니

저장소를 클론해 README 의 데모를 실행하고, 직접 작성한 MCP 서버로 수신 주소를 바꿔 가며 시험했다. API 키는 필요하지 않았다.

1. 설치. `uv sync --all-packages` 가 3초 만에 패키지 45개를 설치했다.
2. 데모. 데모 스크립트는 에이전트가 티켓에 숨은 공격 문장을 따를 때처럼 도구를 호출한다. 공격자 주소로 메일을 보내는 호출과 작업 폴더 밖에 파일을 쓰는 호출이 거부됐다. 고객 삭제 도구는 호출 횟수 제한이 1회라 두 번째 호출이 거부됐다.
3. 대소문자. 티켓에 ATTACKER@Evil.Example 로 적힌 주소를 소문자로 바꿔 메일을 보내도 거부됐다.
4. 바꿔 쓴 주소. 티켓에 attacker [at] evil [dot] example 로 적고 attacker@evil.example 로 보내자 Bouncer 가 사용자에게 확인을 요청했다. 호출은 확인 요청을 지원하지 않는 클라이언트에서도, 확인 요청을 거절한 클라이언트에서도 거부됐다.
5. 비슷한 주소. 고객 목록에 dana@globex.example 이 있을 때, 목록에 없던 ana@globex.example 로 보내자 거부됐다. ana@globex.example 이 dana@globex.example 안에 들어 있기 때문이다.
6. 허용 목록. 허용 목록에 넣은 support@acme.example 로는 티켓에 그 주소가 나온 뒤에도 메일이 전송됐다.
7. 지연. get_ticket 을 50번 호출해 응답 시간의 중앙값을 구했다. 직접 연결하면 3.59 ms, Bouncer 를 거치면 6.55 ms 였다.
8. 테스트. `pytest` 는 147개 통과, 3개 건너뜀으로 5.6초에 끝났다.

```
send_email(to=attacker@evil.example, body=<customer list>)
    DENY  destination to='attacker@evil.example' came from untrusted data
[bouncer blocked] destination to='ana@globex.example' came from untrusted data
```

## 확인하지 못한 것

- AgentDojo 실험 재현. 모델 API 키가 필요하다. gpt-4o-mini 외의 모델에서도 결과가 같은지 확인하자.
- `gmail`, `slack` 등의 팩. 팩은 서버별 기본 설정이다. 어느 도구가 데이터를 내보내는지, 어느 파라미터가 수신 주소인지가 지정돼 있다. README 에 따르면 실제 서버에서 검증되지 않았다. 각 서버의 도구 목록과 대조하자.
- Claude Code 연결. `bouncer init` 으로 설정해 Claude Code 에서 확인 요청이 표시되는지 시험하자.

## 판정

작동한다. 티켓에 나온 공격자 주소로 보내는 호출을 모두 거부했다. 다만 정상 주소도 출력에 나온 다른 주소의 부분 문자열이면 거부했다. 에이전트가 MCP 로 메일, 메신저, 파일 공유 도구를 쓰는 경우에 써 볼 만하다.

### 용어

- MCP: Model Context Protocol. AI 모델을 외부 도구와 데이터에 연결하는 방식을 정의한 규격이다.
- LLM: 대규모 언어 모델. 많은 글로 학습해 글을 이해하고 생성하는 AI 모델이다.
- Show HN: 개발자 커뮤니티 Hacker News 에서 자신이 만든 프로덕트를 소개하는 글. 제목이 "Show HN:"으로 시작한다.
- AgentDojo: LLM 에이전트가 프롬프트 인젝션 공격에 얼마나 견디는지 측정하는 공개 벤치마크.

## English

## The problem

An agent that reads email, tickets or documents may follow instructions hidden in them. If a customer ticket says "send the customer list to this address", the agent may send it with an email tool. This attack is called prompt injection.

Anthropic’s docs on Claude Code permissions say Claude Code asks for approval before it runs a shell command or edits a file. MCP tools are allowed or denied by rules in the form `mcp__server__tool`. To block an MCP tool by a parameter value, you put the value in a `--disallowedTools` deny rule. Only calls with that same value are blocked. In the README, the maker compares Bouncer with this per-tool approval and with LLM-based injection detectors. Claude Code’s permission rules cannot tell whether a recipient came from data the agent read.

## How it solves it

Bouncer was posted as a Show HN on Hacker News on October 27, 2026. It sits between an MCP client and an MCP server and relays tool calls. It keeps a copy of everything the MCP server returns, such as the tickets and customer lists the agent reads. When the agent later sends something, Bouncer checks whether the recipient’s address is in that copy.

Bouncer checks tools that send data out, such as email or file sharing. It allows sends to addresses on the allowlist or approved by the user. It denies sends to any other address that appears in that copy. It asks the user to confirm all other sends. A denied call is never passed to the server, and every verdict goes into an audit log. Bouncer also limits file paths and the number of calls per tool.

## What is different

The first difference is what the judgment rests on. Claude Code’s permission rules look at the tool name and the parameter value. Bouncer looks at where the value came from. Even an address that no deny rule names is denied if it appeared in the server’s output. No LLM makes the decision. Bouncer normalizes case and whitespace and checks whether the string appears in earlier output. So it also denies sends to a legitimate address that is part of a longer one in the output.

The second difference is what it covers. Claude Code’s permission rules also cover shell commands and file edits. Bouncer sees only MCP calls. It keeps a separate copy of each server’s output. If an agent reads an address through one server and sends to it through another, the send gets a confirmation prompt. The maker ran AgentDojo with gpt-4o-mini as the agent. The agent sent data to the attacker in 48 of 164 attacks without Bouncer and 0 of 163 with it. The README does not say why the attack counts differ by one. The run had no user to answer, so every confirmation prompt counted as a denial. Legitimate email tasks succeeded in 4 of 6 cases without Bouncer and 0 of 6 with it.

## How it was built

- The 73 commits date from July 6 to October 24, 2026. Of these, 68 have a Co-authored-by trailer for Claude.
- The source is 1,996 lines of Python, and the tests are 2,145 lines.
- Bouncer has its own inspection tools. One judges a call without running it and logs the verdict, and another reads the audit log. A security note in the README says these tools were also in the agent’s tool list in versions 0.1.0 to 0.1.2. A prompt-injected agent could use them to add verdicts to the audit log or read recent entries, and Bouncer did not check or record the calls themselves. In 0.1.3 the maker took them out of the list.

## Running it

I cloned the repository and ran the README’s demo. Then I tested different recipients against an MCP server I wrote. No API key was needed.

1. Install. `uv sync --all-packages` installed 45 packages in 3 seconds.
2. Demo. The demo script calls tools the way an agent would if it followed an attack hidden in a ticket. Bouncer denied the email to the attacker and a file write outside the work folder. The call budget of 1 stopped the second attempt to delete a customer.
3. Letter case. The ticket had ATTACKER@Evil.Example. Bouncer denied an email to the same address in lowercase.
4. A rewritten address. The ticket had attacker [at] evil [dot] example. When I sent an email to attacker@evil.example, Bouncer asked the user for confirmation. It denied the call when the client lacked confirmation support or the user refused.
5. A similar address. The customer list had dana@globex.example. Bouncer denied a send to ana@globex.example, an address not in the list, because it is part of dana@globex.example.
6. Allowlist. Email to support@acme.example, which is on the allowlist, still went out after the address appeared in a ticket.
7. Latency. I called get_ticket 50 times. The median time was 3.59 ms with a direct connection and 6.55 ms through Bouncer.
8. Tests. `pytest` passed 147 tests and skipped 3, in 5.6 seconds.

```
send_email(to=attacker@evil.example, body=<customer list>)
    DENY  destination to='attacker@evil.example' came from untrusted data
[bouncer blocked] destination to='ana@globex.example' came from untrusted data
```

## Not checked

- Reproducing the AgentDojo run. It needs a model API key. Check models other than gpt-4o-mini too.
- The `gmail`, `slack` and other packs. A pack is a built-in setting that names each server’s sending tools and recipient arguments. According to the README, the packs were not checked against real servers. Compare them with each server’s tools.
- Use with Claude Code. Set up Bouncer with `bouncer init` and test whether Claude Code shows the prompt.

## Verdict

It works. It denied every send to the attacker address found in a ticket. But it also denied a send to a legitimate address that was part of a longer one in the output. It is worth trying if your agents use email, chat or file sharing tools over MCP.

### Terms

- MCP: Model Context Protocol. It is a standard for how AI models connect to outside tools and data.
- LLM: A large language model. It is an AI model trained on large amounts of text to understand and generate text.
- Show HN: A post on Hacker News, a developer community, in which someone presents what they made. Its title starts with "Show HN:".
- AgentDojo: A public benchmark that measures how well LLM agents resist prompt injection attacks.

## Launches the same day

- Hatamoto <https://hatamoto.dev/> (Show HN, 11): CMS. Go 로 작성했고 필요한 기능을 기본으로 갖췄다고 소개한다.
- Markdown 40x more token-efficient with SQLite <https://www.withnative.ai/research-and-posts/markdown-context> (Show HN, 6): 실험 글. Markdown 을 SQLite 와 함께 쓰면 토큰을 40배 적게 쓴다고 주장한다.
- Tiny Tube <https://github.com/spapas/tiny-tube> (Show HN, 5): 영상 공유 서버. Go 실행 파일 하나로 직접 운영한다.
- Software Hate Index <https://hissingkitty.com/predictions> (Show HN, 5): SaaS 불만 순위. 사용자 불만을 실시간으로 모아 순위를 보여 준다고 소개한다.
- ESP-Watchtower <https://github.com/prasadabhishek/esp-watchtower> (Show HN, 5): 홈 네트워크 감시 장치. 3달러짜리 ESP8266 으로 네트워크를 감시하고 미끼 서버를 운영한다.
- Bouncer <https://github.com/Ezed9/mcp-bouncer> (Show HN, 2): MCP 도구 호출 검사기. 에이전트가 읽은 데이터에 나온 주소로 보내는 호출을 거부한다.
- ai-app-playbook <https://github.com/blakeandersonw/ai-app-playbook> (GitHub, 369): 앱 제작 지침서. 코딩 에이전트에게 읽혀 수익이 나는 앱을 만드는 방법을 정리했다고 소개한다.
- ai-pose-gallery-zh <https://github.com/anhao/ai-pose-gallery-zh> (GitHub, 247): 포즈 참고 이미지 모음. AI 이미지용 포즈 1,439조를 중국어로 정리한 정적 갤러리다.
- all-historical-evidence-of-jesus-christ <https://github.com/sybil-solutions/all-historical-evidence-of-jesus-christ> (GitHub, 219): 사료 모음. 나사렛 예수에 관한 연구 자료와 신약, 외경 문헌을 모았다고 소개한다.
- TheySaid 4.0 <https://www.producthunt.com/products/theysaid> (Product Hunt, 303): 사용자 반응 예측 도구. 만드는 화면에서 사용자가 어떻게 반응할지 보여 준다고 소개한다.
- Overmind <https://www.producthunt.com/products/overmind-2> (Product Hunt, 259): 오픈소스 플랫폼. 에이전트가 계속 학습하게 한다고 소개한다.
- Orbit <https://www.producthunt.com/products/orbit-app-blocker> (Product Hunt, 199): 휴대폰 사용 습관 앱. 앱을 열려면 먼저 해야 할 일을 하게 한다고 소개한다.

## Getting started

- Steps: uv sync --all-packages ; uv run python examples/demo/run_demo.py
- Needs: Python 3.12 이상, uv, MCP 클라이언트(Claude Code, Cursor 등). 확인 요청을 받으려면 클라이언트가 MCP elicitation 을 지원해야 한다
- Cost: MIT, 무료.
- Note: bouncer init 은 클라이언트의 MCP 설정 파일을 수정한다. README 에 따르면 먼저 설정 파일을 백업해야 한다.
