Skip to main content
Glama

aiMCPGate

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

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

상태: 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: mcpproxy-go

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에 없습니다. DockerfileCOPY mcp-gate /mcp-gateENTRYPOINT ["/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은 자체 레지스트리를 구축하고, 업스트림에 자체 연결을 열고, 보고하고 종료합니다. 따라서 그 출력은 라이브 게이트웨이의 상태가 아니라 업스트림 연결 가능성 현재입니다: 실행 중인 프로세스가 업스트림을 잃고 카탈로그에서 제거했다면, 이 명령들은 그것을 표시하지 않습니다. 또한 호출 저널을 깨끗하게 유지합니다 — 저널링이 비활성화된 상태로 실행되므로, 이렇게 만든 calllogs에 나타나지 않습니다.

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단계 — 2개 이상의 stdio 업스트림을 하나의 stdio 엔드포인트(Claude Code가 사용하는 것과 동일한 전송) 뒤에서 멀티플렉싱하고 기본 로깅.

  • 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:
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, completionskill(내장 가이드로 대체됨)을 제외한 모든 명령은 설정을 로드합니다: --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단계 저장(잘라내기, 그다음 채우기)을 실제로 안전하게 만드는 것이다: 작성자가 완전한 폴링 간격보다 오래 파일을 반쯤 쓰인 상태로 유지해야만 검사를 속일 수 있다. 대가는 지연이다 — 리로드는 최대 두 번의 폴링 간격(기본 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 베어러 검사를 조용히 비활성화하므로, 이는 절대 조용히 일어나도록 방치되지 않습니다. 인증 없이 실행하려면 auth_token 키를 완전히 제거하세요.

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

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

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 }
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에는 {"limitBytes": N, "resultBytes": M}과 함께 게이트웨이 키 io.github.akomyagin.aimcpgate/result-over-limit 가 추가되어 에이전트가 제한이 우회되었음을 알 수 있습니다. 키를 모르는 클라이언트는 이를 무시하기만 하면 됩니다. 운영자 result_truncation_skipped 저널 이벤트는 이전과 동일하게 계속 발생합니다.

라이선스

MIT — LICENSE를 참조하세요.

Install Server
A
license - permissive license
A
quality
A
maintenance

Maintenance

Maintainers
Response time
6dRelease cycle
7Releases (12mo)
Commit activity

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables centralized management and unified interface for multiple child MCP servers (filesystem, sqlite, etc.), allowing users to discover, launch, and execute tools across different MCP servers through a single gateway.
  • 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.
    45
    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

View all related MCP servers

Related MCP Connectors

  • Hosted MCP server for LLM cost estimation, model comparison, and budget-aware routing.

  • Hosted AgentLux MCP server for marketplace, identity, creator, services, and social flows.

  • Operator-as-agent MCP hub. 6 tools. First $5 free, then $0.001/call.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/akomyagin/aiMCPGate'

If you have feedback or need assistance with the MCP directory API, please join our Discord server