Skip to main content
Glama
mcp-starter-template

mcp-starter-template

by HamzaOuadid

mcp-starter-template

대부분의 공개 MCP 예제가 생략하는 보안 패턴을 구현한 참조용 MCP 서버 스캐폴드입니다: 사용자별 인증 패스스루(공유 서비스 계정 금지), 명시적 쓰기 옵트인이 있는 기본 읽기 전용 도구, 쓰기 도구를 위한 드라이런 모드, 그리고 침묵하는 no-op이나 크래시 대신 구조화된 거부를 제공하는 세션별 지출/호출 한도입니다. 모든 안전장치는 문서 문자열이 아닌 자동화된 테스트로 뒷받침됩니다.

이러한 안전장치가 전혀 없던 프로덕션 MCP 포크를 감사하고 죽은 도구 핸들러를 제거한 후 포트폴리오용으로 제작했습니다.

형제 프로젝트인 mcp-issue-tracker 는 이 동일한 보안 아키텍처(인증 패스스루, 허용 목록 기반 쓰기, 드라이런, 속도 제한, 감사 추적)를 실제 로컬 이슈 트래커 도메인에 적용합니다 — 같은 패턴이 한 번이 아니라 두 번 검증되었습니다.

왜 존재하는가

대부분의 공개 MCP 서버 예제는 어시스턴트를 전체 권한과 안전장치 없이 서비스 계정에 직접 연결합니다. 그렇게 해서 합리적인 질문에 답하던 어시스턴트가 질문자가 보면 안 되는 데이터를 유출하거나, 아무도 승인하지 않은 쓰기를 조용히 실행하게 됩니다. 이 저장소는 더 안전한 기본값이 어떤 모습인지 보여 주며, 한 자리에서 끝까지 읽을 수 있을 만큼 작습니다.

Related MCP server: Kroki MCP

안전장치와 각각이 방지하는 것

안전장치

위치

방지하는 것

인증 패스스루

auth.py, identity.py

도구 호출이 공유/포괄 자격 증명으로 실행되는 일. 모든 호출은 해당 특정 호출자의 신원을 확인하고 모든 다운스트림 검사는 관리자/서비스 계정이 아닌 그 신원을 사용합니다. “어떤 사람이 요청했는지와 관계없이 어시스턴트가 서비스 계정이 볼 수 있는 모든 것을 본다”는 상황을 방지합니다.

기본 읽기 전용 + 명시적 쓰기 허용 목록

registry.py, server.yaml

새로 추가되었거나 잘못 구성된 쓰기 도구가 누군가 명시적으로 검토하고 활성화하기 전에 실행되는 상황. 도구는 이름이 allowed_write_tools에 있을 때만 쓰기로 호출할 수 있으며, 나머지는 모두 비활성 상태입니다. “도구를 추가했는데 삭제할 수 있다는 것을 잊었다”는 상황을 방지합니다.

드라이런 모드

tools/tickets.py, dryrun.py, server.py

운영자가 동작을 검증하는 동안 쓰기 도구의 실제 다운스트림 부작용이 실행되는 상황. 드라이런에서는 실제 API 클라이언트가 전혀 호출되지 않습니다 — 응답만 검사하는 것이 아니라 클라이언트 메서드 자체를 스파이하는 방식으로 테스트에서 검증합니다. “드라이런이 몰래 여전히 쓰기를 수행해서 프로덕션에서 테스트했다”는 상황을 방지합니다.

세션별 비율/지출 한도

limiter.py

무제한 또는 통제 불능 클라이언트가 비용을 태우거나 다운스트림 API를 마구 때리는 상황. 세션의 창 예산(호출 수 또는 비용 단위)이 소진되면, 그 창의 이후 모든 호출은 트리거한 호출 하나만이 아니라 구조화된 오류와 retry_after로 거부됩니다. “클라이언트의 버그가 무제한 API 청구서가 되었다”는 상황을 방지합니다.

구조화된 감사 로그

audit.py

보안 사고가 사후에 재구성 불가능해지는 상황. 모든 호출 — 허용 또는 거부, 읽기 또는 쓰기, 드라이런 또는 실제 — 은 JSON-lines 레코드 하나와 SQLite 행 하나로 기록됩니다: 타임스탬프, 세션, 사용자, 도구, 읽기/쓰기, 드라이런 플래그, 허용 플래그, 지연 시간. “우리는 실제로 무슨 일이 있었는지 모른다”는 상황을 방지합니다.

아키텍처

                         ┌─────────────────────────────┐
   MCP client  ───────▶  │      transport adapter      │
 (stdio / HTTP)          │  mcp_app.py  /  http_app.py  │
                         └──────────────┬───────────────┘
                                        │ token, session_id, tool_name, args
                                        ▼
                         ┌─────────────────────────────┐
                         │      MCPStarterServer        │   server.py — single
                         │        .call_tool()          │   choke point every
                         └──────────────┬───────────────┘   call passes through
                    1) resolve tool ────┤
                    2) authenticate ────┤──▶ AuthMiddleware ──▶ MockIdentityProvider
                    3) allowlist check ─┤──▶ ToolRegistry
                    4) rate/spend check ┤──▶ SessionLimiter
                    5) execute ─────────┤──▶ tool handler (search_docs / create_ticket)
                    6) audit log ───────┴──▶ AuditLogger ──▶ audit.jsonl + SQLite
  • 인증 미들웨어(auth.py)는 MockIdentityProvider(identity.py)를 통해 베어러 토큰을 User로 해석합니다 — 명확히 개발 전용으로 표시되어 있고, 두 명의 서로 다른 테스트 사용자(alice/엔지니어링, bob/영업)와 관리자로 시드됩니다. 누락되거나 인식되지 않은 토큰은 거부되며, 대체 신원은 없습니다.

  • 도구 레지스트리(registry.py)는 모든 도구의 읽기/쓰기 분류가 존재하는 단일 장소로, 등록 시점에 server.yaml의 tools: 섹션과 대조 확인됩니다 — 코드가 선언한 것과 구성이 말하는 것 사이의 불일치는 시작을 거부합니다. 쓰기 도구는 이름이 allowed_write_tools에 들어간 후에만 호출 가능하며, 어느 쪽이든 list_tools()에는 여전히 표시되므로 검토자는 현재 활성화된 것만이 아니라 전체 표면을 볼 수 있습니다.

  • 드라이런 래퍼: 각 쓰기 도구의 핸들러는 dry_run: bool을 받으며, create_ticket의 경우 값이 true이면 TicketSystemClient.create(대리 다운스트림 API)를 절대 건드리지 않습니다 — 대신 합성 DRYRUN-... id를 반환합니다. dryrun.py가 [DRY RUN] 감사 줄을 포맷합니다.

  • 비율/지출 제한기(limiter.py)는 session_id별 고정 창 카운터입니다: calls_per_min과 cost_per_session(도구 비용은 레지스트리에서 가져옴)은 window_seconds마다 함께 재설정됩니다. 거부된 호출은 그 자체로 예산을 소비하지 않습니다.

  • 감사 로그(audit.py)는 JSON-lines를 파일에 쓰고 모든 레코드를 스펙의 데이터 모델과 일치하는 SQLite audit_log 테이블에 미러링하므로, 텍스트로 tail하거나 SQL로 질의할 수 있습니다.

두 가지 전송 방식이 동일한 MCPStarterServer 코어를 감쌉니다:

  • mcp_app.py — 공식 MCP Python SDK(FastMCP)로 구축된 실제 MCP stdio 서버. stdio는 요청별 헤더가 없는 단일 로컬 프로세스이므로 token과 session_id는 명시적 도구 인자입니다 — 로컬/개발 MCP 서버에서 흔하고 문서화된 단순화입니다. 실제 MCP 클라이언트(Claude Desktop, mcp CLI 등)가 연결하게 될 대상입니다.

  • http_app.py — 토큰은 실제 Authorization: Bearer <token> 헤더에서, 세션은 X-Session-Id에서 가져오는 FastAPI HTTP 전송으로, 실제 멀티 테넌트 배포에서 사용할 형태입니다.

예제 도구

  • search_docs(query) -> list[DocResult] — 읽기 전용. 작은 정적 인메모리 코퍼스를 검색하며 호출 사용자의 팀에 보이는 문서(또는 회사 전체 문서)로 필터링합니다. 이것이 인증 패스스루를 증명 가능하게 만드는 부분입니다: alice(엔지니어링)와 bob(영업)이 같은 질의를 하면 서로 다른 결과가 반환됩니다.

  • create_ticket(title, body) -> TicketId — 쓰기, 허용 목록으로 제한됨. 실제 티케팅 API(TicketSystemClient)를 대신하며, 드라이런은 해당 클라이언트에 닿기 전에 가로챕니다.

데이터 모델

  • audit_log: timestamp, session_id, user_id, tool_name, read_or_write, dry_run, allowed, latency_ms, error_code, detail — 모든 호출에 기록되는 SQLite 테이블 + JSON-lines 파일.

  • tool_registry 구성(server.yaml tools: 섹션): 도구 이름별 read_only, cost_units, description.

  • session_limits: 인메모리 세션별 창(call_count, cost_used, window_seconds에 재설정)으로, server.yaml의 rate_limit:에 의해 구동됩니다.

오류 계약

모든 거부는 구조화된 MCPError — {code, message, retry_after?, details?} — 이며, 맨몸 예외나 조용한 no-op이 절대 아닙니다:

코드

발생 시기

UNAUTHENTICATED

토큰이 없거나 인식되지 않음

WRITE_NOT_ALLOWED

쓰기 도구가 호출되었지만 allowed_write_tools에 없음

RATE_LIMIT_EXCEEDED

세션이 calls_per_min 또는 cost_per_session을 초과함

TOOL_NOT_FOUND

알 수 없는 도구 이름

INVALID_ARGUMENTS

핸들러가 주어진 인자에 대해 TypeError를 발생시킴

HTTP에서는 각각 401 / 403 / 429 / 404 / 400으로 매핑되며, 응답의 detail에 동일한 {code, message, ...} 본문이 포함됩니다.

설치

Python 3.10+ 필요(3.10에서 개발 및 테스트함; 스펙은 3.11+를 요구했지만 — 3.10을 대신 사용한 이유는 아래 편차 참조).

git clone https://github.com/HamzaOuadid/mcp-starter-template.git
cd mcp-starter-template
pip install -e ".[dev]"

사용법

도구 레지스트리 나열(보안 검토)

mcp-starter tools

이 저장소에서 실제 출력:

  create_ticket    WRITE [DISABLED (not allowlisted)] cost=5   Create a ticket in the downstream ticket system (write, allowlist-gated).
  search_docs      read-only                        cost=1   Search internal docs visible to the calling user's team (read-only).

“무엇을 방지하는지” 데모 실행

이것은 마일스톤 4 산출물입니다: 이 저장소에 포함된 실제 server.yaml(dry_run: true, 빈 allowed_write_tools)을 사용하여 두 테스트 사용자 시나리오를 종단 간 시뮬레이션하고 권한 경계가 유지됨을 보여 줍니다.

mcp-starter demo

이 저장소의 server.yaml(rate_limit.calls_per_min: 5)로 실제 실행한 출력:

=== 1. Per-user auth passthrough: same tool, same query, different results ===
  alice (engineering): sees docs ['eng-001', 'eng-002', 'all-001']
  bob (sales): sees docs ['sales-001', 'sales-002', 'all-001']

=== 2. Missing/invalid identity is rejected, not defaulted ===
  token=None -> ok=False error={'code': 'UNAUTHENTICATED', 'message': 'Missing or invalid identity token; call rejected.'}

=== 3. Write tool default posture ===
  create_ticket denied: {'code': 'WRITE_NOT_ALLOWED', 'message': "Tool 'create_ticket' is a write tool and is not in allowed_write_tools. Add it to server.yaml's allowlist to enable it."}

=== 4. Rate limit: burst of calls past the cap ===
  call 1/6: allowed
  call 2/6: allowed
  call 3/6: allowed
  call 4/6: allowed
  call 5/6: allowed
  call 6/6: DENIED (RATE_LIMIT_EXCEEDED)

=== Audit log written to <repo>\demo_audit.jsonl ===
  {"allowed": true, "detail": "", "dry_run": false, "error_code": null, "latency_ms": 0.0, "read_or_write": "read", "session_id": "demo-burst-session", ...}
  {"allowed": true, ...}
  {"allowed": false, "error_code": "RATE_LIMIT_EXCEEDED", "detail": "Session 'demo-burst-session' exceeded its rate/spend cap (5 calls or 10 cost units per 60s window).", ...}

alice(엔지니어링)와 bob(영업)은 공유 회사 전체 핸드북(all-001)과 함께 서로 겹치지 않는 문서 집합을 봅니다 — 동일한 도구와 질의에서 권한 경계가 유지됩니다. None 토큰은 즉시 거부됩니다. create_ticket은 허용 목록이 기본적으로 비어 있으므로 거부됩니다. 분당 5회 호출 세션의 6번째 호출은 구조화된 오류로 거부됩니다.

쓰기 도구가 허용 목록에 포함된 상태로 실행하면(구성 기본값이므로 여전히 드라이런) 드라이런 응답 형태를 볼 수 있습니다:

mcp-starter demo --allow-writes
=== 3. Write tool default posture ===
  create_ticket allowed (allowlisted): TicketId(ticket_id='DRYRUN-8ffc09d5', dry_run=True)

실제 티켓은 생성되지 않았습니다 — 드라이런 모드에서 TicketSystemClient.created는 비어 있으며, 이는 tests/test_dry_run.py에서 클라이언트 메서드 자체를 스파이하여 직접 검증합니다.

HTTP 전송 실행

mcp-starter serve-http --port 8000
curl http://127.0.0.1:8000/tools

curl -X POST http://127.0.0.1:8000/tools/search_docs/call \
  -H "Authorization: Bearer token-alice" \
  -H "X-Session-Id: demo-1" \
  -H "Content-Type: application/json" \
  -d '{"arguments": {"query": ""}}'

# Write tool, denied by default (empty allowlist):
curl -i -X POST http://127.0.0.1:8000/tools/create_ticket/call \
  -H "Authorization: Bearer token-alice" \
  -H "X-Session-Id: demo-1" \
  -H "Content-Type: application/json" \
  -d '{"arguments": {"title": "Broken build", "body": "CI red on main"}}'
# -> HTTP 403, {"detail":{"code":"WRITE_NOT_ALLOWED", ...}}

개발 토큰: token-alice(엔지니어링), token-bob(영업), token-admin(엔지니어링, 관리자 플래그 설정).

실제 MCP stdio 서버 실행

mcp-starter serve-stdio

실제 FastMCP stdio 서버를 시작합니다 — MCP 클라이언트(예: mcp CLI의 mcp dev, 또는 Claude Desktop의 구성)를 python -m mcp_starter.mcp_app으로 지정하세요. 도구: search_docs(query, token, session_id), create_ticket(title, body, token, session_id), list_tools().

구성

server.yaml을 편집하세요:

dry_run: true                 # write tools log-and-simulate instead of executing
allowed_write_tools: []       # empty = no write tool is callable, by design
rate_limit:
  calls_per_min: 5
  cost_per_session: 10
  window_seconds: 60
tools:
  search_docs:
    read_only: true
    cost_units: 1
  create_ticket:
    read_only: false
    cost_units: 5

티켓 생성을 실제로 활성화하려면: create_ticket을 allowed_write_tools에 추가하고 그리고 dry_run: false로 설정하세요. 둘 중 하나만 적용하면 쓰기에는 보이지 않거나 시뮬레이션 상태로 유지됩니다.

테스트

pytest tests/ -v

이 저장소에서 실제 출력(40개 테스트, 모두 통과):

tests/test_audit_log.py::test_audit_jsonl_reconstructs_a_session PASSED
tests/test_audit_log.py::test_audit_sqlite_table_matches_data_model PASSED
tests/test_audit_log.py::test_query_filters_by_session PASSED
tests/test_audit_log.py::test_rate_limit_denial_is_also_audited PASSED
tests/test_auth_passthrough.py::test_two_users_see_different_results_from_same_tool PASSED
tests/test_auth_passthrough.py::test_missing_token_is_rejected_not_defaulted PASSED
tests/test_auth_passthrough.py::test_invalid_token_is_rejected PASSED
tests/test_auth_passthrough.py::test_empty_string_token_is_rejected PASSED
tests/test_auth_passthrough.py::test_unknown_tool_name_does_not_crash PASSED
tests/test_cli.py::test_tools_command_lists_both_example_tools PASSED
tests/test_cli.py::test_demo_command_runs_full_scenario PASSED
tests/test_cli.py::test_demo_command_with_allow_writes_flag PASSED
tests/test_dry_run.py::test_dry_run_never_invokes_the_real_downstream_client PASSED
tests/test_dry_run.py::test_dry_run_logs_the_would_be_action_with_marker PASSED
tests/test_dry_run.py::test_dry_run_off_with_allowlist_actually_calls_downstream PASSED
tests/test_dry_run.py::test_dry_run_plus_write_tool_never_executes_even_when_allowlisted_repeatedly PASSED
tests/test_dry_run.py::test_read_only_tool_is_unaffected_by_dry_run_flag PASSED
tests/test_http_transport.py::test_list_tools_endpoint PASSED
tests/test_http_transport.py::test_auth_header_passthrough_two_users_differ PASSED
tests/test_http_transport.py::test_missing_auth_header_returns_401 PASSED
tests/test_http_transport.py::test_write_not_allowed_returns_403 PASSED
tests/test_http_transport.py::test_rate_limit_returns_429 PASSED
tests/test_http_transport.py::test_unknown_tool_returns_404 PASSED
tests/test_mcp_stdio.py::test_stdio_server_lists_all_three_tools PASSED
tests/test_mcp_stdio.py::test_stdio_server_two_users_differ PASSED
tests/test_mcp_stdio.py::test_stdio_server_write_tool_denied_by_default PASSED
tests/test_mcp_stdio.py::test_stdio_server_missing_token_rejected PASSED
tests/test_rate_limit.py::test_burst_of_n_plus_one_rejects_the_last_call PASSED
tests/test_rate_limit.py::test_calls_keep_being_rejected_until_window_resets PASSED
tests/test_rate_limit.py::test_cost_cap_is_enforced_independent_of_call_count PASSED
tests/test_rate_limit.py::test_sessions_are_isolated_from_each_other PASSED
tests/test_rate_limit.py::test_rate_limit_via_server_returns_structured_error PASSED
tests/test_rate_limit.py::test_denied_write_does_not_consume_rate_budget PASSED
tests/test_registry_allowlist.py::test_registry_describes_every_tool_classification PASSED
tests/test_registry_allowlist.py::test_all_write_tools_default_to_disabled PASSED
tests/test_registry_allowlist.py::test_write_tool_not_in_allowlist_is_denied PASSED
tests/test_registry_allowlist.py::test_write_tool_in_allowlist_becomes_enabled PASSED
tests/test_registry_allowlist.py::test_registration_refuses_undeclared_tool PASSED
tests/test_registry_allowlist.py::test_registration_refuses_classification_mismatch PASSED
tests/test_registry_allowlist.py::test_unknown_tool_call_is_tool_not_found PASSED

======================== 40 passed, 1 warning in 6.91s ========================

관심사별 커버리지:

  • 인증 패스스루 (test_auth_passthrough.py) — 두 명의 목업 사용자, 같은 도구, 다른 결과; 누락/유효하지 않은/빈 토큰은 거부되며 기본값으로 대체되지 않음; 알 수 없는 도구 이름은 크래시 없이 깔끔하게 실패.

  • 레지스트리 / 허용 목록 (test_registry_allowlist.py) — 모든 도구의 분류를 검사할 수 있음; 모든 쓰기 도구는 기본적으로 비활성화됨; 등록은 config에 없는 도구나 코드/설정 분류가 일치하지 않는 도구를 거부함; 알 수 없는 도구 이름은 깔끔한 TOOL_NOT_FOUND.

  • 드라이런 (test_dry_run.py) — TicketSystemClient.create를 직접 스파이하여 드라이런 중에는 실제로 절대 호출되지 않음을 검증한다. 단지 응답이 합성처럼 보이는 것만 확인하는 것이 아니다. caplog를 사용해 [DRY RUN] 마커가 실제로 로깅되는지 확인한다. 드라이런이 꺼지고 도구가 허용 목록에 등록되면 실제 클라이언트가 호출됨을 확인한다. 드라이런 + 허용 목록 쓰기 조합을 여러 번 반복해 회귀를 방지한다. 읽기 전용 도구는 플래그의 영향을 받지 않음을 확인한다.

  • 속도 제한 (test_rate_limit.py) — N+1 개의 버스트에서 정확히 (N+1)번째를 거부한다. 가짜 시계를 통해 해당 요청을 유발한 것뿐만 아니라 나머지 윈도우 동안 계속 호출을 거부한다. 비용 상한은 호출 횟수와 별개로 적용된다. 세션은 서로 격리된다. 거부된 쓰기는 그 자체로 속도 예산을 소비하지 않는다.

  • 감사 로그 (test_audit_log.py) — JSONL과 SQLite 모두 전체 세션을 who/what/allowed/dry-run을 재구성할 수 있을 만큼 상세하게 캡처한다; SQLite 행은 세션별로 필터링할 수 있다; 속도 제한 거부도 성공뿐만 아니라 트레일에 캡처된다.

  • 두 트랜스포트 (test_http_transport.py, test_mcp_stdio.py) — 동일한 안전장치가 FastAPI의 TestClient와 실제 FastMCP 서버의 비동기 call_tool/list_tools를 통해 구동될 때도 유지되며, 트랜스포트에 구애받지 않는 코어를 통해서만이 아니다.

  • CLI (test_cli.py) — tools와 demo (--allow-writes 유무에 관계없이) typer.testing. CliRunner를 통해 오류 없이 엔드 투 엔드로 실행된다.

스펙과의 차이 및 이유

  • Python 3.10이지 3.11+가 아님. 개발/CI 환경은 3.10을 사용한다. 이 코드베이스에는 3.11 전용 기능이 전혀 없으므로 인터프리터 업그레이드를 막는 대신 requires-python 최소 버전을 낮췄다. CI는 실제 테스트 환경에 맞춰 3.10을 고정한다.

  • SQLite이지 PostgreSQL이 아님 — 감사 로그용. 스펙은 둘 다 허용한다. Docker/Postgres는 이 환경에서 사용할 수 없다. 감사 스키마(audit.py의 audit_log 테이블)는 SQLite 전용 구문이 없는 순수 SQL이므로 나중에 Postgres로 마이그레이션하는 것은 드라이버 교체(sqlite3.connect → psycopg2/asyncpg)와 AUTOINCREMENT → SERIAL/IDENTITY뿐이며 재설계가 아니다.

  • stdio를 통한 인증 패스스루는 전송 헤더가 아닌 명시적 token 인수를 사용한다. MCP의 stdio 전송은 단일 로컬 프로세스이며 요청별 헤더가 없으므로 HTTP의 Authorization 헤더가 HTTP 전송(http_app.py)에 실제 요청별 자격 증명을 제공하는 것처럼 가로챌 대상이 없다. 토큰을 명시적으로 전달하면 효과(모든 호출을 게이팅하는 기본값이 아닌 해석된 신원)가 두 전송에서 동일하고 테스트 가능하게 유지된다. 이는 문서화된 단순화이며 stdio가 "실제" 다중 사용자 인증을 가진다는 주장이 아니다. 프로덕션 다중 사용자 배포는 HTTP 전송을 실행하거나, 이 코드 앞단에서 실제 자격 증명을 주입하는 인증 프록시로 감싼 stdio 전송을 실행해야 한다.

  • MockIdentityProvider에는 OAuth/JWT/mTLS가 없다. 이는 정적 token→user 사전으로, 스펙의 위험 노트에 따라 명백히 개발 전용이다. 실제 검증으로 교체한다는 것은 AuthMiddleware.authenticate의 토큰 조회를 실제 IdP를 대상으로 구현하는 것을 의미한다. 나머지 파이프라인(registry, limiter, dry-run, audit)은 User를 돌려받는 것에만 의존하므로 영향을 받지 않는다.

  • 제외: v0.1 git 태그. 마일스톤 4는 v0.1 릴리스를 태깅하도록 요구한다. 이 저장소는 마일스톤별 PR이 아니라 사용자 스토리별 커밋으로 진행되므로, 기본 브랜치에 합쳐지고 CI가 통과한 뒤 메인테이너가 태깅 (git tag v0.1.0 && git push --tags)하도록 남겨 둔다. 어디에도 푸시된 적 없는 저장소에 스스로 태깅하지 않는다.

  • 제외: 영구적인 session_limits/tool_registry 테이블 없음. 스펙의 데이터 모델은 audit_log와 함께 session_limits와 tool_registry를 테이블로 나열한다. tool_registry 분류는 server.yaml에 있으며, (리뷰어가 쿼리해야 하는 DB 테이블보다 단일 진실 공급원으로 더 나을 수 있다.) session_limits는 limiter.py에서 메모리에만 존재한다. 이는 단일 프로세스 스타터에는 올바르지만 재시작 후 유지되지 않거나 프로세스 간에 확장되지 않는다. 둘 이상의 서버 프로세스 뒤에서 실행하기 전에 가장 먼저 고쳐야 할 점(예: Redis 기반 카운터)으로 지적한다.

  • 예제 도구는 세 개 이상이 아니라 두 개. 스펙은 "2-3"개를 요구하므로 정확히 두 개(읽기 하나, 쓰기 하나)를 제공했다. 세 번째 읽기 전용 도구는 앞의 두 도구가 이미 다루는 안전장치를 추가로 검증하지 않기 때문이다.

프로젝트 구조

src/mcp_starter/
  identity.py    mock identity provider (dev-only) + User model
  auth.py        auth passthrough middleware
  config.py      server.yaml loading/validation (pydantic)
  registry.py    tool registry: classification + allowlist enforcement
  limiter.py     per-session fixed-window rate/spend limiter
  audit.py       JSONL + SQLite structured audit logging
  dryrun.py      "[DRY RUN]" audit-line formatting
  errors.py      structured MCPError + error codes
  server.py      MCPStarterServer.call_tool — the orchestration core
  mcp_app.py     real MCP stdio server (official MCP Python SDK)
  http_app.py    FastAPI HTTP transport (Authorization header passthrough)
  cli.py         `mcp-starter` CLI: tools / demo / serve-http / serve-stdio
  tools/
    docs.py      search_docs (read-only example tool)
    tickets.py   create_ticket (write example tool) + TicketSystemClient
tests/           37 tests across every guardrail and both transports
server.yaml      tool classification, allowlist, dry-run, rate limits

라이선스

MIT — LICENSE 참조.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    A secure MCP server example that demonstrates how to deploy to Google Cloud Run with authentication and identity token protection. Serves as a tutorial template for building production-ready MCP servers in the cloud.
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    A production-ready MCP server scaffold that features built-in authentication, Docker support, and a comprehensive CI/CD release pipeline. It provides a standardized template for deploying servers with multi-transport support and configurable read-only modes.
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    A production-ready foundation for building secure, observable MCP servers with built-in authentication, rate limiting, and reference tools like database-query and semantic-search.
    15
    17 npm
    MIT