Skip to main content
Glama
HamzaOuadid

mcp-starter-template

by HamzaOuadid

mcp-starter-template

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

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

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

왜 존재하는가

대부분의 공개 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.yamltools: 섹션과 대조 확인됩니다 — 코드가 선언한 것과 구성이 말하는 것 사이의 불일치는 시작을 거부합니다. 쓰기 도구는 이름이 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_mincost_per_session(도구 비용은 레지스트리에서 가져옴)은 window_seconds마다 함께 재설정됩니다. 거부된 호출은 그 자체로 예산을 소비하지 않습니다.

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

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

  • mcp_app.py — 공식 MCP Python SDK(FastMCP)로 구축된 실제 MCP stdio 서버. stdio는 요청별 헤더가 없는 단일 로컬 프로세스이므로 tokensession_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.yamlrate_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_ticketallowed_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) — toolsdemo (--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.pyaudit_log 테이블)는 SQLite 전용 구문이 없는 순수 SQL이므로 나중에 Postgres로 마이그레이션하는 것은 드라이버 교체(sqlite3.connectpsycopg2/asyncpg)와 AUTOINCREMENTSERIAL/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_limitstool_registry를 테이블로 나열한다. tool_registry 분류는 server.yaml에 있으며, (리뷰어가 쿼리해야 하는 DB 테이블보다 단일 진실 공급원으로 더 나을 수 있다.) session_limitslimiter.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 참조.

-
license - not tested
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

  • A paid remote MCP for CLI tool MCP, built to return verdicts, receipts, usage logs, and audit-ready

  • Hosted MCP server for agent governance: MCP config audits, injection scans, scope-policy checks.

  • The official MCP Server from Mia-Platform to interact with Mia-Platform Console

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/HamzaOuadid/mcp-starter-template'

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