Skip to main content
Glama

aiMCPGate

Русская версия — README_RU.md.

Go로 작성된 MCP 서버(Model Context Protocol)용 게이트웨이/프록시입니다. MCP 클라이언트(Claude Code, Cursor 등)에게는 하나의 MCP 서버로 보이지만, 내부적으로는 여러 업스트림 MCP 서버에 대한 호출을 멀티플렉싱하고, 해당 서버들의 도구, 프롬프트, 리소스를 하나의 카탈로그로 통합하며, 모든 호출을 기록합니다.

상태: MVP 완료(06단계) + 포스트-MVP 718단계 배포 완료, 최신 릴리스 v0.5.0. 1단계 — 호출 로그가 있는 stdio 엔드포인트 뒤에서 stdio 업스트림 멀티플렉싱; 2단계 — HTTP/SSE 클라이언트 측 전송, HTTP 업스트림, CLI 로그 뷰어(mcp-gate logs); 릴리스 파이프라인(goreleaser, linux/darwin/windows × amd64/arm64 크로스 컴파일, CGO 없음). 포스트-MVP에서 업스트림 자동 재시작, 설정 핫 리로드, 도구 필터링/이름 변경, doctor가 추가되었고, v0.3.0에서는 전체 prompts/resources/ resources/templates/completion 통합, ping, 진행 상황 전달 및 실제 취소, logging/setLevel 팬아웃, 업스트림별 호출 제한(속도 제한 / 동시성 / 결과 잘림 / 타임아웃), 지연 카탈로그 및 tools/list 페이지네이션, 클라이언트와 업스트림 양쪽의 SSE 서버→클라이언트 스트림이 추가되었습니다. v0.4.0은 서버→클라이언트 방향을 완성했습니다: 서버 시작 방식의 세 가지 메서드 — elicitation/create, sampling/createMessage, roots/list — 가 네 가지 전송 조합 모두(클라이언트 측 stdio 또는 HTTP × 업스트림 측 stdio 또는 HTTP)에서 프록시됩니다. 이제 게이트웨이는 자체 클라이언트가 선언한 기능을 그대로 업스트림에 선언하며, 빈 {}를 보내지 않습니다. HTTP 전송에는 서버 측 Mcp-Session-Id 세션과 DELETE /mcp 종료가 추가되었습니다. v0.5.0은 운영자 관찰 가능성(18단계)을 추가합니다: 여덟 가지 이벤트 유형 — 업스트림 시작 실패 및 슈퍼바이저 포기, 드롭된 알림 및 서버→클라이언트 요청, GET SSE가 없는 HTTP 업스트림, 카탈로그 충돌 및 잘못된 URI 템플릿, max_result_bytes를 조용히 우회한 결과 — 이제 MCP 클라이언트가 일반적으로 소유하는 stderr 대신 호출 저널(mcp-gate logs)에 기록됩니다. 설정 파싱이 엄격해졌습니다(알 수 없거나 오타가 있는 키는 치명적 오류). 또한 클라이언트 측 가드/잘림 스토리의 절반을 마무리합니다: 속도 제한 또는 동시성 가드가 거부한 tools/call은 이제 구분할 수 없는 -32603 대신 자체 JSON-RPC 오류 코드 -32029와 기계 판독 가능한 data: {"retryable":true,"reason":...}를 반환하며, max_result_bytes를 우회한 비텍스트 결과는 result._meta 마커를 포함합니다(content는 바이트 단위로 그대로 유지됨). 마지막으로, 설정되지 않은 환경 변수를 참조하는 auth_token은 HTTP 인증을 조용히 비활성화하는 대신 게이트웨이 시작을 거부합니다.

v0.5.0으로 업그레이드 — 세 가지 동작 변경, 설정 파일 형식 자체에는 영향 없음:

  • 설정 파싱이 이제 엄격합니다. 이전에는 조용히 무시되던 알 수 없거나 오타가 있는 최상위 또는 업스트림별 키가 있는 설정은 이제 로드에 실패합니다. 키 이름을 수정하거나 (오류가 이름을 알려줌) 제거하세요.

  • 설정되지 않은 VAR이 있는 auth_token: ${VAR}는 이제 시작을 거부하며 변수 이름을 알려줍니다. 이전에는 조용히 빈 토큰이 되어 — HTTP 게이트웨이에서는 경고 없이 베어러 검사를 완전히 비활성화했습니다. 변수를 설정하거나(--env-file 전달) 의도적으로 인증 없이 실행하려면 auth_token을 제거하세요.

  • 호출 저널(log_file / calls.jsonl)에 두 번째 레코드 유형인 "kind":"event"가 기존 호출 레코드와 함께 추가되었습니다. v0.4.0 이하 바이너리가 v0.5.0 저널을 읽으면 실패하는 대신 이벤트 라인을 희소한 ERR 항목으로 렌더링합니다 — 저널을 쓴 바이너리와 같거나 더 새로운 바이너리로 읽으세요.

v0.4.0으로 업그레이드: 설정 파일 변경은 없지만 두 가지 관찰 가능한 HTTP 모드 동작 변경이 있습니다 — initialize 이후 POST /mcp에는 이제 세션 ID가 필수이며 (헤더는 initialize 응답에서 반환됨), 업스트림 레지스트리는 프로세스 시작 시가 아니라 첫 번째 실제 MCP 요청에서 지연 시작됩니다.

구현되지 않음: 클라이언트별 액세스 정책.

릴리스

크로스 플랫폼 바이너리는 goreleaser(.goreleaser.yaml)를 통해 빌드됩니다: linux/darwin/windows × amd64/arm64, CGO 없음, 버전은 -ldflags -X main.version=...로 내장되며, 체크섬은 SHA256SUMS에 포함됩니다. 로컬 드라이 런: goreleaser release --snapshot --clean.

Related MCP server: mcpstead

MCP 레지스트리에서 설치

원시 릴리스 바이너리 외에도 게이트웨이는 GitHub Container Registry의 OCI 이미지와 npm 래퍼 패키지로 배포됩니다 — MCP 레지스트리가 설치하는 두 가지 형식입니다.

Docker:

docker run --rm -i -v $(pwd)/config.yaml:/config.yaml ghcr.io/akomyagin/aimcpgate serve

-i는 필수입니다: 게이트웨이는 stdio를 통해 MCP를 통신하므로 클라이언트는 stdin을 열어 두어야 합니다(없으면 컨테이너는 EOF를 보고 즉시 종료됩니다). 이미지에는 자체 설정이 없으므로 직접 마운트하세요 — 위 예시는 기본 경로 /config.yaml에 마운트합니다; 다른 경로는 serve -c로 사용할 수 있습니다.

실제 업스트림 없이 레지스트리 샌드박스 검사(Glama.ai 등)를 재현하려면 이미지에 내장된 데모 설정을 사용하세요 — 샌드박스가 실행해야 하는 정확한 명령은 다음과 같습니다:

docker run --rm -i ghcr.io/akomyagin/aimcpgate serve -c /demo.config.yaml

npx (첫 설치 시 플랫폼용 사전 빌드 바이너리를 다운로드하고 SHA256 체크섬을 검증합니다):

npx aimcpgate serve -c ./config.yaml

이미지 정책: OCI 이미지에는 mcp-gate 바이너리만 포함됩니다 — stdio 업스트림용 런타임(node/npx, python, 셸)은 없습니다. 설정이 stdio 업스트림 서버를 실행하는 경우 이미지를 직접 확장하여 필요한 것을 설치하세요; HTTP 업스트림은 기본적으로 작동합니다 (CA 인증서 포함).

데모 설정: demo.config.yaml과 숨겨진 __demo-echo 하위 명령은 레지스트리 샌드박스(Glama.ai)가 실제 업스트림 없이 게이트웨이를 검사할 수 있도록만 존재합니다 — 실제 배포에서는 절대 사용하지 마세요.

컨테이너 내부에서 CLI 명령 실행

doctor, catalog, call, logs는 운영자가 배포를 검사하는 방법입니다. 컨테이너 내부에서 어떻게 호출해야 하는지를 결정하는 세 가지 사실이 있습니다:

  1. 바이너리는 /mcp-gate이며 $PATH에 없습니다. Dockerfile은 COPY mcp-gate /mcp-gate와 ENTRYPOINT ["/mcp-gate"]를 수행합니다 — 검색 경로에 넣는 것은 없습니다(이상해 보이면 Dockerfile을 확인하세요). 따라서 일반적인 형식은 실패합니다:

    $ docker exec mcp-gate mcp-gate catalog -c /config.yaml
    OCI runtime exec failed: exec failed: unable to start container process: exec: "mcp-gate": executable file not found in $PATH

    대신 절대 경로를 사용하세요 — 그것이 유일한 차이점입니다.

  2. 이미지는 distroless이므로 셸이 전혀 없습니다. 기본은 gcr.io/distroless/static-debian12:nonroot이며, 바이너리와 CA 인증서만 포함하고 다른 것은 없습니다. docker exec mcp-gate sh -c '…'는 sh가 그냥 없기 때문에 같은 방식으로 실패하며, 주변을 살펴볼 ls/cat도 없습니다. 파이프, 글로빙, 리다이렉션은 명령의 HOST 쪽에 유지하세요.

  3. docker exec는 새 프로세스를 시작합니다. 실행 중인 serve를 조회하지 않습니다. doctor, catalog, call은 자체 레지스트리를 구축하고, 업스트림에 자체 연결을 열고, 보고하고 종료합니다. 따라서 그 출력은 라이브 게이트웨이의 상태가 아니라 현재 업스트림 연결 가능성입니다: 실행 중인 프로세스가 업스트림을 잃고 카탈로그에서 제거했다면, 이 명령들은 그것을 표시하지 않습니다. 또한 호출 저널을 깨끗하게 유지합니다 — 저널링이 비활성화된 상태로 실행되므로, 이렇게 수행한 call은 logs에 나타나지 않습니다.

docker exec mcp-gate /mcp-gate version
docker exec mcp-gate /mcp-gate doctor  -c /config.yaml
docker exec mcp-gate /mcp-gate catalog -c /config.yaml
docker exec mcp-gate /mcp-gate call demo__echo '{"text":"hi"}' -c /config.yaml
docker exec mcp-gate /mcp-gate logs    -c /config.yaml --tail 50

이 명령들은 분리되어 이름이 지정된 컨테이너가 시작되었다고 가정합니다(예: docker run -d --name mcp-gate …) — 위의 포그라운드 docker run --rm -i … 예시와 달리, stdio 클라이언트가 연결을 끊는 즉시 종료되어 docker exec가 도달할 대상이 없습니다. 설정은 같은 예시처럼 기본 경로 /config.yaml에 마운트된 것으로 가정합니다; demo__echo는 자체 카탈로그의 도구를 대신합니다. 몇 가지 주의 사항:

  • logs는 사실 3의 예외입니다: 실행 중인 게이트웨이가 쓰는 저널 파일을 읽으므로 라이브 프로세스를 반영합니다. 이를 위해서는 마운트된 설정의 log_file이 컨테이너 내부에서 보이는 경로를 가리켜야 하고, 해당 위치에 볼륨이 마운트되어야 합니다 — 그렇지 않으면 저널은 컨테이너의 stderr(즉, docker logs)로 가고 mcp-gate logs는 읽을 것이 없습니다. -c는 저널 위치를 알려주는 옵션이고, --file이 이를 재정의합니다.

  • 이것은 실제로 HTTP 모드에 관한 것입니다. stdio 모드에서는 MCP 클라이언트가 컨테이너를 생성하고 소유하므로, 일반적으로 exec할 수 있는 장기 실행 컨테이너가 없습니다. 검사할 수 있는 게이트웨이는 transport: http로 별도로 시작된 (docker run -d --name mcp-gate …) 것입니다.

  • HTTP 모드에는 기본이 아닌 listen_addr이 필요합니다. 기본값은 127.0.0.1:28080입니다 — 컨테이너 내부의 루프백으로, -p가 있어도 호스트에서 도달할 수 없습니다. 설정에서 listen_addr: 0.0.0.0:<port>를 설정하세요; 그러면 게이트웨이는 의도적으로 auth_token 없이는 시작을 거부합니다("HTTP 엔드포인트가 인증 없이 네트워크에서 도달 가능할 것입니다").

왜

활발한 MCP 사용자는 일반적으로 여러 서버(파일시스템, GitHub, 검색, 사용자 정의)를 구성하며, 각각이 모든 클라이언트의 자체 설정에 중복됩니다. aiMCPGate는 다음을 제공합니다:

  • 하나의 진입점 — 클라이언트 설정의 N개 항목 대신 단일 MCP 엔드포인트.

  • 하나의 카탈로그 — 모든 업스트림 서버의 도구와 프롬프트가 병합됩니다 (이름 충돌이 없도록 <upstream>__<tool>로 네임스페이스됨), 리소스와 리소스 템플릿도 포함합니다(URI로 주소가 지정되므로 이름이 변경되지 않음).

  • 호출 로그 — 어떤 업스트림, 어떤 도구, 언제, 성공/실패. 이것이 "단순 프록시" 위에 추가된 가치입니다.

솔로 펫 프로젝트: 우선순위는 Go 학습(동시성, os/exec, JSON-RPC 2.0, stdio 및 HTTP/SSE 전송)입니다. 비용 — 기본적으로 월 $0(로컬 프로세스), 텔레메트리 없음.

작동 방식 (요약)

MCP client ──stdio/HTTP──▶ aiMCPGate ──JSON-RPC──▶ upstream A (stdio)
                              │        ├─────────▶ upstream B (stdio)
                          call log     └─────────▶ upstream C (http, Phase 2)

MVP (두 단계)

  • 1단계 — 하나의 stdio 엔드포인트(Claude Code가 사용하는 것과 동일한 전송) 뒤에서 2개 이상의 stdio 업스트림 멀티플렉싱 + 기본 로깅.

  • 2단계 — HTTP/SSE 전송, HTTP 업스트림 서버, 로그 뷰어(CLI는 구축됨; 웹 뷰는 의도적으로 제외), 선택적으로 액세스 정책 — 그것은 검토 후 거부됨.

빌드

export PATH="$HOME/sdk/go/bin:$PATH"   # if go isn't already on PATH
go build ./...
go vet ./...
go test -race ./...

go run ./cmd version

사용법

# stdio mode (the client launches the gateway as a subprocess):
mcp-gate serve --config ./config.yaml

# http mode (transport: http in the config) — endpoint at http://<listen_addr>/mcp;
# every request after initialize carries the issued Mcp-Session-Id (see below):
mcp-gate serve --config ./config-http.yaml

# check every enabled upstream once (launch → handshake → tools/list) and print
# a per-upstream OK/FAIL table; exit code is non-zero if any upstream failed
# (scriptable for CI/cron), no auto-restart, no call logging — one pass then exit.
# It also WARNs (POSIX only) if config.yaml or --env-file is readable by
# group/others — either may hold a secret literal, and nothing else checks:
mcp-gate doctor --config ./config.yaml

# call one aggregated tool once from the shell (single bring-up, no supervisor —
# the fastest way to debug a config, a filter or a rename without a live client):
mcp-gate call github__search_repositories '{"query":"mcp"}' --config ./config.yaml

# report the aggregated catalog size per upstream (tools / bytes / ~tokens) plus
# the heaviest individual tools — the data behind allow-list / strip decisions:
mcp-gate catalog --config ./config.yaml --top 20

# view the journal — tool calls AND operator events (last 50 lines; filter by
# upstream/tool/status):
mcp-gate logs --file ./logs/calls.jsonl --tail 50
mcp-gate logs --config ./config.yaml --upstream github --status err
# show ONLY the operator events (see "Operator events" below):
mcp-gate logs --config ./config.yaml --events
# keep watching the log as it grows, or aggregate it instead of listing records
# (--follow and --stats are mutually exclusive):
mcp-gate logs --config ./config.yaml --follow
mcp-gate logs --config ./config.yaml --stats

# generate a random auth token (for the HTTP transport) and see how to wire it in:
mcp-gate token --generate
# print the auth token currently set in the config:
mcp-gate token --config ./config-http.yaml

# print ready-to-paste MCP client config snippets (Claude Code / Cursor / Claude
# Desktop) for whichever transport the config uses: a launch command for stdio, or
# the endpoint URL plus the Bearer header (when auth_token is set) for http:
mcp-gate client-config --config ./config.yaml

# print a SKILL.md teaching an agent how to use the aggregated catalog
# (built-in text by default; overridable via skill_file in the config):
mcp-gate skill > .claude/skills/mcp-gate/SKILL.md

# shell completions (cobra's built-in command; the release archives also ship
# pre-generated ones):
mcp-gate completion bash > /etc/bash_completion.d/mcp-gate

token --generate, completion, skill(내장 가이드로 대체됨)을 제외한 모든 명령은 설정을 로드합니다: --config를 전달하거나 바이너리 옆에 config.yaml을 두세요(아래 설정 참조).

serve, doctor, call, catalog는 또한 --env-file ./.env를 허용합니다 — 설정이 로드되기 전에 적용되는 최소한의 KEY=VALUE 파서로, 설정 내부의 ${VAR} 참조가 해당 파일에서 해석됩니다. 실제 프로세스 환경이 항상 파일보다 우선합니다.

HTTP 세션 (Mcp-Session-Id)

http 모드에서 게이트웨이는 Streamable HTTP 세션을 실행합니다: initialize에 대한 응답은 Mcp-Session-Id 헤더를 포함하며, 이후의 모든 요청 — POST, GET SSE 스트림, DELETE — 은 해당 헤더를 다시 보내야 합니다. 없으면 응답은 400입니다; 알 수 없거나 만료된 ID는 404이며, 클라이언트에게 다시 initialize하라고 알려줍니다. 세션은 DELETE /mcp(204)로 해제되거나, 30분 동안 요청이 없으면 해제됩니다 — 열린 SSE 스트림은 활동으로 간주되어 세션을 유지합니다.

MCP 클라이언트는 이 모든 것을 자동으로 처리합니다. 수동 curl 호출의 경우, initialize 응답에서 헤더를 가져와 다시 보내세요:

SID=$(curl -sD - -o /dev/null -X POST http://127.0.0.1:28080/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' \
  | tr -d '\r' | awk -F': ' '/^[Mm]cp-[Ss]ession-[Ii]d/{print $2}')

curl -s -X POST http://127.0.0.1:28080/mcp \
  -H 'Content-Type: application/json' -H "Mcp-Session-Id: $SID" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

curl -s -X DELETE http://127.0.0.1:28080/mcp -H "Mcp-Session-Id: $SID"

세션은 또한 호출 로그를 정직하게 만듭니다: 모든 호출은 해당 세션의 clientInfo 아래에서 감사되므로, 여러 HTTP 클라이언트가 하나의 빈 client 필드를 공유하는 대신 calls.jsonl에서 구분됩니다.

HTTP를 통한 서버→클라이언트 요청 (elicitation, sampling, roots)

업스트림이 통화 중에 무언가를 요청할 때 — elicitation/create, sampling/createMessage, roots/list — 그 질문은 한 세션의 GET /mcp 스트림에 SSE 이벤트로 전달되며, 클라이언트는 동일한 id와 동일한 Mcp-Session-Id를 가진 JSON-RPC 응답을 담은 일반 POST로 답한다. 질문을 받은 세션만이 답할 수 있으며, 다른 세션의 답은 무시된다. 스트림이 열린 상태로 해당 기능을 선언한 클라이언트가 없으면, 업스트림 요청은 타임아웃에 맡겨지는 대신 사양이 규정한 형태(elicitation은 {"action":"decline"}, 나머지 둘은 -32601)로 즉시 거절된다. 질문이 미해결인 동안 세션이 종료되어도 마찬가지다.

알아둘 만한 세 가지 결과:

  • 업스트림은 첫 번째로 초기화하는 클라이언트의 기능을 전달받으며, 그 집합은 프로세스 수명 동안 고정된다. MCP 2025-06-18에는 재협상이 없으므로, 더 많은 기능을 선언하는 두 번째 클라이언트가 이미 이루어진 핸드셰이크를 바꿀 수 없다 — 업스트림은 자신이 알지 못하는 클라이언트를 대신해 어떤 기능도 약속받지 않는다.

  • 업스트림은 게이트웨이가 포트를 바인딩할 때가 아니라, 이를 필요로 하는 첫 번째 요청이 올 때 시작된다. 위의 선언이 가능한 이유가 바로 이것이다: 핸드셰이크는 클라이언트가 자신이 지원하는 것을 말한 후에 이루어져야 한다. 업스트림이 시작할 수 없으면 클라이언트는 JSON-RPC -32603을 받고 게이트웨이는 이전에 업스트림을 즉시 시작했을 때와 마찬가지로 오류와 함께 종료된다.

  • 질문은 해당 기능을 선언한 클라이언트에게 전달된다 — 반드시 그 질문을 유발한 호출의 주인에게 가는 것은 아니다. 라우팅은 선언된 기능을 기준으로 이루어지며, 일치하는 세션 중 가장 최근에 활성화된 세션이 선택된다. 업스트림 요청에는 어떤 호출자가 그것을 요청했는지 알려주는 정보가 없다. 단일 클라이언트(일반적인 경우)에서는 보이지 않지만, 두 개를 실행하면 한 클라이언트의 tools/call로 발생한 양식이 다른 클라이언트의 UI에 표시될 수 있다.

동일한 교환의 업스트림 측도 HTTP로 동작한다: url:로 연결된 원격 MCP 서버는 자신의 질문을 SSE 프레임으로 보낼 수 있다 — 장기 연결된 GET 스트림을 통해서든, 게이트웨이 자신의 POST 중 하나에 응답하는 스트림에 끼워 넣어서든. SDK 서버는 tools/call 내부에서 발생한 elicitation/create를 후자의 방식으로 보낸다. 게이트웨이는 이를 동일한 파이프라인을 통해 프록시하고 클라이언트의 답변을 서버 자신의 요청 id 아래에 JSON-RPC 응답을 담은 일반 POST 하나로 보낸다. 이러한 업스트림은 stdio 업스트림과 동일한 정직한 정책으로 게이트웨이의 클라이언트 기능을 전달받는다 — 게이트웨이 자신의 클라이언트가 선언한 경우에만 기능이 제공되며, 클라이언트가 전혀 없는 doctor/call/catalog는 정확히 {}만 계속 선언한다. 답변 POST는 재시도되지 않는다: 이를 받지 못한 업스트림은 자체 타임아웃에 의존한다.

저널의 운영자 이벤트

log_file의 저널에는 두 종류의 줄이 있다: 도구 호출당 하나, 그리고 운영자 이벤트당 하나 — 그렇지 않으면 결코 알 수 없었을 게이트웨이 상태다. stdio 모드에서는 MCP 클라이언트가 터미널을 소유하므로 게이트웨이의 stderr는 사용자에게 보이지 않으며, 이러한 조건 중 여러 가지는 이전에 디버그 수준에서만 기록되었다. 이제 mcp-gate logs가 읽는 동일한 파일에 기록된다:

이벤트

의미

upstream_start_failed

업스트림이 끝내 기동되지 않았다. 해당 도구는 카탈로그에 없다.

upstream_gave_up

슈퍼바이저가 업스트림 재시작을 중단했다(시도 소진, 리로드로 재시작 비활성화, 또는 활성 채널 없음) 카탈로그에서 제거했다.

notification_dropped

구독자의 버퍼가 가득 차서 전달된 알림이 버려졌다 — 전달은 설계상 비차단이다.

server_request_dropped

업스트림이 클라이언트만 답할 수 있는 것(elicitation/sampling/roots)을 요청했고 어떤 전송도 질문을 받지 않아, 도구 호출이 그를 대신해 거절되었다.

sse_stream_unavailable

HTTP 업스트림이 GET SSE 스트림을 제공하지 않아, 게이트웨이가 재시작될 때까지 해당 업스트림의 tools/list_changed가 도착하지 않는다.

catalog_collision

두 항목이 동일한 클라이언트 대상 도구/프롬프트 이름 또는 리소스 URI를 주장했다. 먼저 온 것이 이기고 진 쪽은 클라이언트에게 숨겨진다.

catalog_bad_template

리소스 URI 템플릿이 컴파일되지 않는다: 클라이언트에게 나열되지만 읽기와 일치할 수 없다.

result_truncation_skipped

결과가 max_result_bytes를 초과했지만 잘라낼 수 있는 텍스트가 없어(예: 이미지만) 전체가 통과했다.

이벤트는 호출과 함께 인라인으로 EVT 표시로 나타난다. mcp-gate logs --events는 이벤트만 표시하고, --stats는 이벤트별 테이블을 추가한다. --tool과 --status는 호출 전용 필터이므로 둘 중 하나가 설정된 동안에는 이벤트가 제외된다(--upstream은 둘 다에 적용된다). 알아둘 만한 결과 하나: notification_dropped는 업스트림을 지명하지 않는다 — 드롭은 알림을 보낸 사람이 아니라 버퍼가 가득 찬 구독자의 속성이므로 — --upstream X는 이를 절대 표시하지 않는다. 해당 필터 없이 찾아야 한다. 반복된 드롭은 병합된다 — 첫 번째는 즉시 기록되고, 1분 이내의 추가 드롭은 해당 키의 다음 줄 count=에 집계되며, 나머지는 종료 시 플러시된다. 이러한 백로그를 담은 줄은 detail=에 이를 접은 가장 오래된 발생 시각을 명시한다 — 줄 자체의 타임스탬프는 가장 최신이므로, 둘을 합치면 버스트가 실제로 발생한 시간 범위가 정해진다.

실용적인 메모 두 가지:

  • log_file을 설정하라. 비어 있으면 저널이 stderr로 가는데, stdio 모드에서는 stderr가 MCP 클라이언트에 속한다 — 이벤트는 볼 수 없는 곳에 기록될 것이다.

  • 저널을 기록한 것과 동일한(또는 더 새로운) 바이너리로 읽어라. 이벤트에는 이전 버전이 모르는 "kind" 필드가 있으므로, ≤ v0.4.0의 mcp-gate logs는 이를 희소하고 대부분 빈 레코드로 렌더링한다.

이 중 어떤 것도 MCP 클라이언트에는 보이지 않는다: 오류 코드, 결과 본문 또는 기능이 변경되지 않았다 — 이벤트는 저널에만 기록된다.

게이트웨이가 라우팅할 수 없었던 호출은 이벤트가 아니라 — 일반적인 실패한 CALL 줄이다. 어떤 업스트림도 제공하지 않는 도구 이름을 요청한 클라이언트는 다른 호출과 마찬가지로 CallRecord를 받으며, upstream 열은 센티널 (unrouted)로 설정된다. mcp-gate logs --upstream '(unrouted)'는 정확히 그 줄들만 선택하고 다른 것은 선택하지 않는다. 두 번째로 구별되는 경우는 거의 같아 보이지만 실제 업스트림 이름을 지명한다: 경로는 존재하지만(도구가 카탈로그에 있음) 업스트림의 연결이 사라진 경우(재시작 중이거나 드롭됨) — 그 줄은 실제 업스트림 이름을 담으므로 센티널 대신 평소처럼 --upstream <name>으로 필터링한다.

구성 리로드 (SIGHUP)

게이트웨이는 SIGHUP 시 구성을 실시간으로 리로드한다 — 재시작도, 클라이언트 연결 끊김도 없다. config.yaml을 편집하고 시그널을 보내라:

kill -HUP $(pgrep -f 'mcp-gate serve')

리로드 시 게이트웨이는 새 구성을 실행 중인 업스트림과 비교하여 최소 변경을 적용한다: 새로 추가된 업스트림은 시작되고, 제거된(또는 enabled: false인) 업스트림은 종료되며, 시작 필드(command/args/url/env/headers)가 변경된 업스트림은 재시작되고, 도구 필터만 변경된(allow/deny/rename, 또는 카탈로그 프로젝션 규칙 strip_annotations/strip_output_schema/max_description/ describe) 업스트림은 재시작 없이 다시 프로젝션된다. 호출 제한(rate_limit, max_concurrent, max_result_bytes, call_timeout — 전역 또는 업스트림별)도 실시간으로 적용된다: 재시작이 필요 없으며 다음 호출이 새 값을 사용할 뿐이다. 변경되지 않은 업스트림은 그대로 계속 실행된다. 잘못된 편집(잘못된 YAML, 검증 실패)은 기록되고 무시된다 — 현재 실행 중인 구성이 유지되므로 오타가 게이트웨이를 죽이지 않는다.

동작 참고: 게이트웨이는 SIGHUP 핸들러를 설치하므로 SIGHUP은 더 이상 OS 기본값처럼 프로세스를 종료하지 않는다. 게이트웨이를 중지하려면 Ctrl-C, SIGINT 또는 SIGTERM을 사용하라.

SIGHUP은 Unix 전용이다. Windows에서 — 또는 시그널을 보내고 싶지 않은 어디에서든 — 옵트인 폴링 대안을 사용하라:

mcp-gate serve --config ./config.yaml --watch-config        # bare flag = poll every 2s
mcp-gate serve --config ./config.yaml --watch-config=10s    # note the "=", not a space

해당 간격으로 구성 파일의 지문을 확인하고 SIGHUP이 사용하는 것과 동일한 리로드 경로를 적용한다. SIGHUP 핸들러와 함께 실행해도 안전하다.

감시자는 파일의 mtime과 크기를 비교하고, 다음 틱에서 해당 지문이 반복될 때까지 기다린 후 파일을 읽는다. 이것이 두 단계 저장(잘라내기, 그 다음 채우기)을 실제로 안전하게 만드는 이유다: 작성자가 전체 폴링 간격보다 오래 파일을 반쯤 쓰인 상태로 유지해야 검사를 속일 수 있다. 대가는 지연이다 — 리로드는 최대 두 번의 폴링 간격(기본 2초에서 최대 4초) 내에 적용된다.

stdio에서는 업스트림이 클라이언트의 첫 번째 요청 시 시작되므로, 클라이언트가 연결되기 전에 이루어진 편집은 아직 적용될 수 없다. 감시자는 그 편집을 유지하고 게이트웨이가 기동될 때까지 매 폴링마다 재시도한 후 적용한다 — 두 번째로 파일을 저장할 필요가 없다. 영구히 거절된 편집(파싱 불가능한 YAML, 또는 아래의 upstreams 없음 가드)은 한 번 보고되고 재시도되지 않는다.

두 트리거 모두에 대한 백스톱으로, 새 구성에 upstreams가 전혀 선언되지 않은 리로드는 거절되고 기록된다: 그것은 반쯤 쓰인 파일의 특징이며, 적용하면 실행 중인 모든 업스트림이 내려갈 것이다. 모든 업스트림을 의도적으로 제거하려면 게이트웨이를 재시작하라. 명시적인 enabled: false는 영향을 받지 않는다 — 마지막 업스트림을 비활성화하는 것은 여전히 적용된다.

구성

--config가 없으면 게이트웨이는 자체 바이너리 옆에서 config.yaml을 찾는다(예: mcp-gate가 /etc/gate/에 설치된 경우, 실행된 작업 디렉터리와 관계없이 /etc/gate/config.yaml을 찾는다). 해당 파일이 없고 --config도 전달되지 않았으면 빈 게이트웨이로 시작하는 대신 명시적으로 오류를 낸다. 구성 내부의 상대 경로(log_file, skill_file, debug_payload_log)는 현재 작업 디렉터리가 아닌 구성 파일 자체의 디렉터리를 기준으로 해석된다.

알 수 없는 키는 시작 오류다. 구성은 엄격하게 파싱된다: 철자가 틀리거나 인식할 수 없는 키는 이전처럼 조용히 무시되는 대신 키 이름과 줄 번호와 함께 게이트웨이를 중지시킨다. 구체적인 이점: enabled의 오타가 더 이상 업스트림을 조용히 실행 상태로 둘 수 없다. 사용자 정의 x- 스크래치 키도 거부된다 — 블록을 공유하려면 첫 번째 실제 업스트림에 YAML 앵커를 두고 다른 업스트림에 병합(<<: *anchor)하라. 앵커와 병합 키는 평소처럼 작동한다.

업스트림은 기본적으로 활성화된다: enabled:를 생략하면 다른 업스트림과 마찬가지로 시작된다. 구성을 삭제하지 않고 게이트웨이에서 제외하려면 **enabled: false**로 명시적으로 비활성화하라 — 그러면 tools/list에도 mcp-gate doctor의 테이블에도 나타나지 않는다. 주의: 값이 없는 enabled: (또는 enabled: null)는 생략으로 읽히므로, 값을 주석 처리하면 업스트림이 계속 실행된다 — 오직 리터럴 false만 비활성화한다.

참고: "바이너리 옆" 조회는 실행 중인 실행 파일의 경로를 사용한다. go run ./cmd ...에서는 그 실행 파일이 임시 디렉터리의 일회용 빌드이므로 기본 조회가 config.yaml을 찾지 못한다 — go run을 사용할 때는 --config를 명시적으로 전달하거나, 빌드된 바이너리를 실행하라.

모든 필드를 포함한 전체 예제 — config.example.yaml. 업스트림 서버 집합은 YAML로 선언됩니다. 비밀값(토큰)은 env/.env를 통해 전달되며(로드 시 ${VAR} 확장), 설정 파일에 커밋되지 않습니다. 각 업스트림은 command(stdio 하위 프로세스) 또는 url(HTTP 서버, Streamable HTTP) 중 정확히 하나를 설정합니다 — 연결 종류는 자동으로 추론됩니다.

설정되지 않은 ${VAR} 참조는 필드별로 다르게 동작합니다:

  • **auth_token**이 설정되지 않은 변수를 참조하면 시작이 실패하며 변수 이름이 표시됩니다 — 빈 auth_token은 HTTP bearer 검사를 조용히 비활성화하므로, 이런 일이 조용히 발생하는 것은 절대 허용되지 않습니다. 인증 없이 실행하려면 auth_token 키를 완전히 제거하세요.

  • 업스트림의 **env/headers**에 설정되지 않은 변수가 있으면 오류가 아닙니다: 값이 비어 있게 되고 누락된 비밀값은 나중에 해당 업스트림의 401로 표면화됩니다. 게이트웨이는 이를 사전에 보고합니다 — 저널(mcp-gate logs)의 unresolved_secret_var 이벤트와 mcp-gate doctor의 WARN 줄입니다.

  • stdio 모드에서 mcp-gate client-config는 운영자의 환경 변수가 MCP 클라이언트에 의해 상속되지 않는다고 (stderr로) 경고합니다. MCP 클라이언트는 자체 환경에서 게이트웨이를 실행하므로 — 클라이언트가 실행하는 곳에 변수를 설정하세요.

기본적으로 모든 stdio 업스트림은 게이트웨이의 전체 프로세스 환경을 상속합니다 — --env-file이 다른 업스트림을 위해 로드한 비밀값과 auth_token도 포함됩니다. env_isolation: strict(전역)를 설정하면 각 stdio 업스트림은 대신 최소한의 기본 환경(PATH, HOME, TMPDIR 및 Windows 동등 항목)과 자체 env: 블록만 받으며, 이는 항상 하위 프로세스에 전달됩니다. 신뢰할 수 없는 공개 레지스트리(npx로 실행되는 완전히 신뢰하지 않는 패키지)에서 업스트림을 실행할 때 권장됩니다. strict 업스트림이 보기를 원하는 게이트웨이 변수는 해당 env:에 명시적으로 나열하세요(예: PYTHONPATH: ${PYTHONPATH}). 값은 실행 시점에 읽힙니다: 실행 중인 업스트림은 (재)시작될 때까지 시작 시의 환경을 유지합니다.

별도로, 항상 활성화되는 기능: 게이트웨이가 ${VAR} 비밀값을 확장하면 정확한 값을 알게 되며, 운영자 대상 자유 텍스트 — 저널(mcp-gate logs) 및 크래시 로그 — 에서 해당 값을 *****로 편집(redact)**합니다. 비밀값이 구성되지 않은 경우 비용이 들지 않습니다.

일반, 암호화되지 않은 HTTP를 통해 루프백이 아닌 호스트로 전송되는 비밀값은 시작 시 WARN과 mcp-gate doctor의 한 줄을 받습니다(절대 하드 실패가 아닙니다 — 신뢰할 수 있는 인프라(예: VPN 또는 격리된 LAN)에서의 평문은 게이트웨이가 설정만으로 판단할 수 없는 의도적인 선택일 수 있습니다): 루프백을 넘어 바인딩된 비어 있지 않은 auth_token, 또는 https://가 아닌 루프백이 아닌 url로 전송되는 업스트림의 headers. TLS 자체는 게이트웨이에 내장되어 있지 않습니다 — 필요하다면 TLS 종료 리버스 프록시를 앞에 두세요, 상호 TLS에 이미 권장된 것과 동일한 패턴입니다.

transport: stdio            # stdio (Phase 1) | http (Phase 2)
listen_addr: "127.0.0.1:28080"  # only used for transport: http; loopback by default
# auth_token: ${AIMCPGATE_TOKEN}  # required if you widen listen_addr past loopback;
#                                 # the variable must be set or startup fails
log_file: ./logs/calls.jsonl
# debug_payload_log: ./logs/payloads.jsonl  # OPT-IN, off by default: logs raw
#                                   # arguments AND results — can contain secrets
# Optional global call limits (each can be overridden per upstream):
# rate_limit: { rps: 5, burst: 2 }  # token bucket per upstream for tools/call
#                                   # (refusal → client error -32029, retryable)
# max_result_bytes: 65536           # truncate oversized textual results (0 = off;
#                                   # non-text over-limit results get a _meta marker)
# call_timeout: 30s                 # bounds one upstream request
# How the catalog is presented to the client (both hot-reloadable):
# catalog_mode: lazy                # normal (default) | lazy: the client sees only
#                                   # gate_search_tools / gate_describe / gate_call
# page_size: 50                     # paginate tools/list (0/omitted = whole catalog;
#                                   # ignored in lazy mode)
# Auto-restart policy for crashed stdio upstreams (defaults: on, 1s→30s, 5 tries):
# restart: { enabled: true, initial_backoff: 1s, max_backoff: 30s, max_attempts: 5 }
# env_isolation: strict             # opt-in ("" default = inherit the gateway's full
#                                   # env): a stdio upstream then gets only a minimal
#                                   # base (PATH/HOME/...) plus its own env: block —
#                                   # NOT other upstreams' secrets or the auth token
upstreams:
  - name: filesystem        # stdio upstream
    command: npx
    args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user"]
    enabled: true
  - name: github
    command: github-mcp-server
    env:
      GITHUB_TOKEN: ${GITHUB_TOKEN}   # from the environment, not hardcoded
    enabled: true
    # Optional per-upstream tool filter / catalog projection (keys are ORIGINAL
    # tool names; all editable live via SIGHUP with no upstream restart):
    # tools:
    #   allow: ["search_repositories"]  # if non-empty, only these survive
    #   deny: ["delete_repository"]     # always subtracted, even from allow
    #   rename: { search_repositories: "gh_search" }
    #   strip_annotations: true         # drop heavyweight catalog fields
    #   strip_output_schema: true
    #   max_description: 200            # truncate descriptions to N runes
    #   describe: { get_issue: "Fetch one issue." }   # replace wholesale
    # Optional per-upstream call limits (override the globals for this upstream):
    # rate_limit: { rps: 1, burst: 1 }  # rps: 0 disables the global limit here
    #                                   # (refusal → client error -32029, retryable)
    # max_concurrent: 4                 # cap on simultaneous in-flight calls
    #                                   # (refusal → client error -32029, retryable)
    # max_result_bytes: 32768           # 0 disables the global cap here
    # call_timeout: 120s                # this upstream is slow — give it longer
  - name: remote            # http upstream (Phase 2)
    url: https://mcp.example.com/mcp
    headers:
      Authorization: "Bearer ${REMOTE_MCP_TOKEN}"   # secret, never logged
    enabled: true

호출 제한이 적용될 때 클라이언트가 보는 것

위의 호출 제한 중 두 가지는 운영자 저널뿐만 아니라 MCP 클라이언트(에이전트)에도 표면화됩니다:

  • 가드 거부(rate_limit / max_concurrent). 업스트림별 속도 제한기 또는 동시성 상한이 수용할 수 없어 게이트웨이가 tools/call을 거부하면, 클라이언트는 게이트웨이 자체 코드 **-32029**와 기계 판독 가능한 data: {"retryable": true, "reason": "rate_limit" | "concurrency_limit"}를 포함한 JSON-RPC 오류를 받습니다. 호출은 업스트림에 도달하지 않았으므로 에이전트는 이중 실행 위험 없이 대기 후 재시도할 수 있습니다. 일반적인 전송/라우팅 실패는 기존의 -32603을 유지하며, 업스트림 자체가 반환하는 오류는 코드와 데이터가 변경되지 않은 채 그대로 전달됩니다 — 업스트림의 -32029는 게이트웨이 신호가 아닙니다.

  • 잘릴 수 없는 초과 크기 결과(max_result_bytes). 텍스트 결과는 콘텐츠 내 [truncated by mcp-gate: …] 마커로 축소됩니다. 제한을 초과하지만 잘릴 수 있는 텍스트가 없는 비텍스트/비표준 결과(예: 이미지만 있는 경우)는 전체가 바이트 단위 그대로 전달됩니다 — 해당 content[]는 절대 변경되지 않습니다 — 그러나 결과의 _meta에는 게이트웨이 키 **io.github.akomyagin.aimcpgate/result-over-limit**가 {"limitBytes": N, "resultBytes": M}과 함께 추가되어 에이전트가 제한이 우회되었음을 알 수 있습니다. 키를 모르는 클라이언트는 단순히 무시합니다. 운영자 result_truncation_skipped 저널 이벤트는 이전과 동일하게 발생합니다.

라이선스

MIT — LICENSE 참조.

Available Tools

2 tools
demo-echo__echoB

Echo the given text back verbatim.

ParametersJSON Schema
NameRequiredDescriptionDefault
textYesText to echo back verbatim.

TDQS

B3.4/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations exist, so the description bears full responsibility. It merely repeats the parameter description ('Echo the given text back verbatim') without adding behavioral details such as side effects, error handling, or output format.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence with no extraneous information. Every word serves the purpose.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simplicity (one required parameter, no output schema), the description is largely complete. It could explicitly mention the return format, but the behavior is trivially inferable.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3. The description adds no new meaning beyond the parameter's description, which already states 'Text to echo back verbatim.'

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description clearly states the action ('Echo') and the resource ('the given text back verbatim'), making the purpose unmistakable. The sibling tool 'ping' likely serves a different function (connectivity test), so this description distinguishes well.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance is provided on when to use this tool versus the sibling 'ping' or any alternatives. The description simply states what it does without contextual usage advice.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

demo-echo__pingA

Health check: always returns "pong".

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided, but the description fully discloses the output ('always returns pong'), leaving no ambiguity about its behavior for a simple ping tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

One concise sentence that is front-loaded with purpose and output, no unnecessary words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple health check tool with no parameters and no output schema, the description is complete and sufficient for an agent to use correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

No parameters in the schema; description adds no parameter info, but baseline for 0-param tools is 4.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it is a health check that always returns 'pong', which is specific and distinguishes it from the sibling tool 'echo'.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage for health checks, and with only one sibling ('echo'), the purpose is clear without explicit alternatives. Slightly lacking explicit when-not-to-use guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 2 tool updatesv0.3.0
    • First observeddemo-echo__echo
    • First observeddemo-echo__ping

TDQS

A4.1/5.0

Scored across 2 tools

Disambiguation5/5

The two tools serve completely different purposes: echo returns input text, ping returns a fixed health response. No ambiguity.

Naming Consistency5/5

Both tools follow the same pattern: prefix 'demo-echo__' followed by a verb. The naming is uniform and predictable.

Tool Count4/5

With only 2 tools, the set is minimal but appropriate for a simple demonstration server. It covers the core functionality without being overly sparse.

Completeness5/5

For the stated purpose of a demo echo server, the tools provide exactly what is needed: echo text and health check. No obvious gaps.

Maintenance

ActivitySlowing
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    MCP Gateway that aggregates multiple upstream MCP servers into a single endpoint with persistent connections, tool registry, and authentication.
    31 npm
    2
    MIT
  • F
    license
    A
    quality
    D
    maintenance
    Universal MCP proxy server that discovers, searches, and executes tools across all configured MCP servers from a single entry point.
    7
    -
  • A
    license
    A
    quality
    B
    maintenance
    A zero-dependency MCP gateway: host your own tools, forward and curate tools from other MCP servers, expose them leanly to cut agent token cost, and gate every call through your own policy hooks before it runs.
    4
    34 npm
    38
    MIT