Skip to main content
Glama
HamzaOuadid

mcp-starter-template

by HamzaOuadid

mcp-starter-template

Эталонный каркас MCP-сервера, реализующий паттерны безопасности, которые большинство публичных примеров MCP пропускают: сквозная аутентификация пользователя (никогда не общий служебный аккаунт), инструменты, доступные только для чтения по умолчанию с явным согласием на запись, режим пробного запуска (dry-run) для пишущих инструментов и лимит расходов/частоты на сессию со структурированными отказами вместо молчаливого бездействия или падений. Каждое защитное ограничение подкреплено автоматизированным тестом, а не только docstring.

Создан как элемент портфолио после аудита и удаления мёртвых обработчиков инструментов из продакшен-форка MCP, в котором не было ни одного из этих защитных механизмов.

Родственный проект — mcp-issue-tracker — использует ту же самую архитектуру безопасности (сквозная аутентификация, записи через разрешённый список, dry-run, ограничение частоты, журнал аудита), применённую к реальному локальному домену трекера задач — тот же паттерн, доказанный дважды, а не один раз.

Почему это существует

Большинство публичных примеров MCP-серверов подключают ассистента напрямую к сервисному аккаунту с полными правами и без каких-либо ограждений. Именно так ассистент, отвечающий на разумный вопрос, в итоге раскрывает данные, которые спрашивающий не должен видеть, или молча выполняет запись, которую никто не одобрял. Этот репозиторий — пример более безопасного подхода по умолчанию, достаточно компактного, чтобы прочитать его целиком за один присест.

Related MCP server: Kroki MCP

Защитные механизмы и что каждый из них предотвращает

Защитный механизм

Где

Что предотвращает

Сквозная аутентификация

auth.py, identity.py

Выполнение вызова инструмента под общим/универсальным учётным данным. Каждый вызов определяет личность конкретного вызывающего, и все последующие проверки используют именно эту личность, а не учётную запись администратора или сервисного аккаунта. Предотвращает ситуацию, когда «ассистент видит всё, что видит сервисный аккаунт, независимо от того, кто спросил».

Только чтение по умолчанию + явный список разрешённых для записи

registry.py, server.yaml

Выполнение недавно добавленного или неправильно сконфигурированного пишущего инструмента до того, как кто-то явно проверил и включил его. Инструмент может быть вызван как записывающий только если его имя есть в allowed_write_tools; всё остальное инертно. Предотвращает ситуацию: «мы добавили инструмент и забыли, что он может удалять данные».

Режим dry-run

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

Срабатывание реального побочного эффекта пишущего инструмента на нижестоящем сервисе, пока оператор ещё проверяет поведение. В режиме dry-run реальный API-клиент не вызывается вообще — это проверяется в тестах через шпионаж за самим методом клиента, а не только через изучение ответа. Предотвращает ситуацию: «мы тестировали в проде, потому что dry-run втайне всё равно писал».

Лимит частоты/расходов на сессию

limiter.py

Неограниченный или вышедший из-под контроля клиент, сжигающий бюджет или долбящий нижестоящий API. Как только оконный бюджет сессии (вызовы или единицы стоимости) исчерпан, каждый последующий вызов в этом окне отклоняется со структурированной ошибкой и retry_after, а не только тот вызов, который превысил лимит. Предотвращает ситуацию: «баг в клиенте превратился в безграничный счёт за API».

Структурированный журнал аудита

audit.py

Невозможность восстановить инцидент безопасности после факта. Каждый вызов — разрешённый или отклонённый, чтение или запись, dry-run или реальный — записывается как одна запись в формате JSON-lines и одна строка в SQLite: временная метка, сессия, пользователь, инструмент, чтение/запись, флаг dry-run, флаг разрешения, задержка. Предотвращает ситуацию: «мы на самом деле не знаем, что произошло».

Архитектура

                         ┌─────────────────────────────┐
   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) преобразует bearer-токен в объект User с помощью MockIdentityProvider (identity.py) — с явной пометкой «только для разработки», с двумя разными тестовыми пользователями (alice/engineering, bob/sales) и администратором. Отсутствующие или нераспознанные токены отклоняются; запасной личности нет.

  • Реестр инструментов (registry.py) — единственное место, где хранится классификация каждого инструмента по чтению/записи; при регистрации она сверяется с секцией tools: в server.yaml — расхождение между тем, что объявляет код, и тем, что говорит конфиг, не даст серверу запуститься. Пишущий инструмент становится вызываемым только после того, как его имя попадает в allowed_write_tools; при этом он остаётся видимым в list_tools() в любом случае, чтобы ревьюер мог видеть всю поверхность, а не только то, что сейчас включено.

  • Обёртка dry-run: обработчик каждого пишущего инструмента принимает dry_run: bool, и для create_ticket при значении true никогда не обращается к TicketSystemClient.create (заглушке нижестоящего API) — вместо этого возвращает синтетический идентификатор DRYRUN-.... dryrun.py форматирует строку аудита [DRY RUN].

  • Лимитер частоты/расходов (limiter.py) — счётчик фиксированного окна для каждого session_id: calls_per_min и cost_per_session (стоимость инструмента берётся из реестра) сбрасываются вместе каждые window_seconds. Отклонённые вызовы сами по себе не потребляют бюджет.

  • Журнал аудита (audit.py) пишет JSON-lines в файл и дублирует каждую запись в таблицу SQLite audit_log, соответствующую модели данных из спецификации, так что его можно читать как текст или запрашивать через SQL.

Два транспорта оборачивают одно и то же ядро MCPStarterServer:

  • mcp_app.py — настоящий MCP stdio-сервер на официальном MCP Python SDK (FastMCP). Поскольку stdio — это единый локальный процесс без заголовков запроса, token и session_id передаются как явные аргументы инструментов — распространённое документированное упрощение для локальных/dev MCP-серверов. Именно с ним будет общаться реальный MCP-клиент (Claude Desktop, CLI mcp и т.п.).

  • http_app.py — HTTP-транспорт на FastAPI, где токен берётся из настоящего заголовка Authorization: Bearer <token>, а сессия — из X-Session-Id, то есть в форме, которую использовал бы настоящий мультитенантный деплой.

Примеры инструментов

  • search_docs(query) -> list[DocResult] — только чтение. Выполняет поиск по небольшому статическому корпусу в памяти, отфильтрованному по документам, видимым команде вызывающего пользователя (или общедоступным документам компании). Именно это делает сквозную аутентификацию доказуемой: один и тот же запрос от alice (engineering) и bob (sales) возвращает разные результаты.

  • create_ticket(title, body) -> TicketId — запись, ограничено списком разрешённых. Заменяет реальный API тикетов (TicketSystemClient); dry-run перехватывает вызов до того, как этот клиент будет затронут.

Модель данных

  • 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 (секция tools: в server.yaml): read_only, cost_units, description для каждого имени инструмента.

  • session_limits: оконный лимит сессии в памяти (call_count, cost_used, сброс через window_seconds), управляется параметром rate_limit: в server.yaml.

Контракт ошибок

Каждый отказ — это структурированная ошибка MCPError — {code, message, retry_after?, details?} — никогда не голое исключение и не молчаливое бездействие:

код

когда

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 соответственно, с тем же телом {code, message, ...} в поле detail ответа.

Установка

Требуется 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 (engineering) и bob (sales) видят непересекающиеся наборы документов плюс общий общедоступный справочник (all-001) — граница прав сохраняется при использовании одного и того же инструмента и запроса. Токен None отклоняется сразу. create_ticket отклоняется, потому что список разрешённых по умолчанию пуст. 6-й вызов в сессии с лимитом 5 вызовов в минуту отклоняется со структурированной ошибкой.

Запустите его с добавлением пишущего инструмента в список разрешённых (всё ещё dry-run, так как это значение по умолчанию в конфиге), чтобы увидеть форму ответа dry-run:

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

Ни один реальный тикет не был создан — TicketSystemClient.created остаётся пустым в режиме dry-run; это напрямую проверяется в 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", ...}}

Dev-токены: token-alice (engineering), token-bob (sales), token-admin (engineering, установлен флаг администратора).

Запуск настоящего MCP stdio-сервера

mcp-starter serve-stdio

Это запускает настоящий FastMCP stdio-сервер — укажите MCP-клиенту (например, mcp dev из CLI mcp или конфигу 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) — классификация каждого инструмента доступна для проверки; все инструменты записи по умолчанию отключены; регистрация отклоняет инструменты, отсутствующие в конфигурации или чьи классификации в коде и конфигурации расходятся; неизвестное имя инструмента даёт корректный TOOL_NOT_FOUND.

  • Пробный запуск (dry-run) (test_dry_run.py) — напрямую шпионит за TicketSystemClient.create, чтобы убедиться, что он действительно никогда не вызывается в режиме dry-run, а не только в том, что ответ выглядит синтетическим; использует caplog, чтобы подтвердить, что маркер [DRY RUN] действительно логируется; подтверждает, что реальный клиент вызывается, когда dry-run выключен и инструмент находится в списке разрешённых; повторяет комбинацию dry-run + разрешённая запись несколько раз для защиты от регрессий; подтверждает, что флаг не влияет на инструменты только для чтения.

  • Ограничение частоты запросов (test_rate_limit.py) — всплеск из N+1 отвергает ровно (N+1)-й вызов; вызовы продолжают отклоняться в течение остатка окна (не только тот, что его вызвал) с помощью фиктивных часов; лимит стоимости соблюдается независимо от количества вызовов; сеансы изолированы друг от друга; отклонённая запись сама не расходует бюджет лимита частоты.

  • Журнал аудита (test_audit_log.py) — JSONL и SQLite обе фиксируют полную сессию с достаточной детализацией, чтобы восстановить кто/что/разрешено/dry-run; строки SQLite фильтруются по сессии; отказы из-за ограничения частоты также попадают в журнал, а не только успешные операции.

  • Оба транспорта (test_http_transport.py, test_mcp_stdio.py) — те же защитные механизмы действуют при работе через TestClient из FastAPI и через асинхронные call_tool/list_tools реального сервера FastMCP, а не только через независимое от транспорта ядро.

  • 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_log в audit.py) написана на чистом SQL без синтаксиса, специфичного для SQLite, поэтому переход на Postgres позже — это замена драйвера (sqlite3.connect → psycopg2/asyncpg) плюс AUTOINCREMENT → SERIAL/IDENTITY, а не перепроектирование.

  • Сквозная аутентификация через stdio использует явный аргумент token, а не заголовок транспорта. Транспорт stdio в MCP — это единый локальный процесс без заголовков на каждый запрос, так что перехватывать нечего, в отличие от HTTP, где заголовок Authorization даёт HTTP-транспорту (http_app.py) реальные учётные данные для каждого запроса. Явная передача токена сохраняет тот же эффект (определённая, не используемая по умолчанию идентичность, управляющая каждым вызовом) и тестируемость на обоих транспортах; это документированное упрощение, а не утверждение, что у stdio есть «настоящая» многопользовательская аутентификация. В производственном многопользовательском развёртывании следует использовать HTTP-транспорт либо stdio-транспорт, обёрнутый аутентифицирующим прокси, который внедряет реальные учётные данные выше по потоку от этого кода.

  • Никаких OAuth/JWT/mTLS в MockIdentityProvider. Это статический словарь токен→пользователь, явно предназначенный только для разработки, согласно примечанию о рисках в самой спецификации. Замена на реальную проверку означает реализацию поиска токена в AuthMiddleware.authenticate через реальный IdP; остальная часть конвейера (реестр, ограничитель, dry-run, аудит) не затрагивается, поскольку она зависит только от получения объекта User.

  • Исключено: git-тег v0.1. Веха 4 предусматривает создание тега для релиза v0.1. Этот репозиторий ведётся по коммитам на пользовательскую историю, а не по PR на веху, поэтому создание тега оставлено мейнтейнеру: он сделает это, когда ветка попадёт в основную ветку с зелёным CI (git tag v0.1.0 && git push --tags), а не самостоятельное тегирование репозитория, который никуда не пушился.

  • Исключено: отсутствуют постоянные таблицы session_limits/tool_registry. В модели данных спецификации session_limits и tool_registry перечислены как таблицы наряду с audit_log. Классификация tool_registry хранится в server.yaml (возможно, это лучший единый источник истины, чем таблица БД, к которой рецензенту пришлось бы обращаться), а 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
    C
    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
    C
    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
    10 npm
    MIT