# Архитектура MCP-серверов на forge-toolkit > **Кому:** разработчику, который пишет MCP-сервер на Go с этим тулкитом. > После прочтения ты должен уметь написать сервер любого домена: файлы, БД, > HTTP, DevOps и т.д. Эталон рядом: [`template/`](../template) (каркас) и [`examples/hello-tool/`](../examples/hello-tool) (минимальный рабочий пример). --- ## 1. Что такое сервер на forge-toolkit MCP-сервер — **самостоятельная консольная программа**: читает JSON-RPC из stdin, пишет в stdout. Один сервер решает **одну задачу** и даёт один или несколько инструментов. Никакого «заодно могу и…». Сервер собирается из: - **каркаса** — `toolkit` / `toolkit/validate` / `configreload` (домен-агностичные хелперы); - **домена** — код конкретной задачи (политики, клиенты, маппинг аргументов). Домен живёт отдельно от каркаса; каркас не расширяется «на будущее». --- ## 2. Три столпа 1. **stdio-only.** Транспорт — stdin/stdout (`mcp.StdioTransport`). Сервер не поднимает HTTP/SSE/TCP и не заводит session-pool: хост сам решает, как запускать процесс (один на сервер, один на агента и т.д.). 2. **Один потокобезопасный Manager.** Состояние процесса — один объект- диспетчер. Если он изменяемый (пулы соединений, кэши) — под мутексом (`sync.RWMutex`): инструменты вызываются конкурентно. 3. **Ручные схемы.** Инструмент объявляется явно: `&mcp.Tool{Name, Description, InputSchema}` + обработчик. `InputSchema` — JSON Schema 2020-12, собирается хелперами `toolkit.Schema/StrProps/...`. --- ## 3. Жизненный цикл `main.go` ```go func main() { if toolkit.Health() { // --health → печатает "ok" и выходит (дешёво) return } mgr := mydomain.NewManager() // один Manager на процесс defer mgr.Close() if err := toolkit.Run("my-server", func(s *mcp.Server) { tools.RegisterAll(s, mgr) }); err != nil { fmt.Fprintf(os.Stderr, "my-server: %v\n", err) os.Exit(1) } } ``` `toolkit.Run` поднимает `mcp.NewServer` и stdio-цикл с graceful shutdown по `SIGINT`/`SIGTERM`. --- ## 4. Инструмент: объявление и схема Имя инструмента — **без префикса сервера** (`read_file`, `run_query`): хост добавит свой префикс при регистрации. ```go mcp.AddTool(s, &mcp.Tool{ Name: "read_file", Description: "Читает файл внутри разрешённых корней. Read-only.", InputSchema: toolkit.Schema(map[string]any{ "path": toolkit.StrProps("путь относительно корня", true), }, []string{"path"}), }, func(ctx context.Context, req *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) { args := toolkit.RequestArgs(req) path, err := toolkit.RequireString(args, "path") if err != nil { return toolkit.Error(err.Error()), nil, nil } // ... доменная работа ... return toolkit.Text("..."), nil, nil }) ``` `properties` в схеме — всегда объект (даже пустой): часть провайдеров отклоняет `"properties": null`. --- ## 5. Результат и ошибки `mcp.CallToolResult`: - успех — `toolkit.Text(msg)`; - **доменная** ошибка (модель должна исправиться) — `toolkit.Error(msg)` (`IsError=true`), цикл агента продолжается; - **инфраструктурный** сбой — вернуть Go-`error`: вызов считается неудачным. Правило: всё, что модель может поправить (неверный аргумент, не найдено, недостаточно прав) — `toolkit.Error`; всё, что сломано на стороне сервера (диск, сеть, паника) — Go-`error`. --- ## 6. Хелперы аргументов `toolkit.RequestArgs(req)` → `map[string]any`. Далее: | Хелпер | Назначение | |---|---| | `GetString(args, key, def)` | строка (не-строки приводятся через `%v`) | | `RequireString(args, key)` | непустая строка или ошибка | | `GetInt(args, key, def)` | int (принимает `float64`/строку) | | `GetBool(args, key, def)` | bool | | `GetStringArray` / `GetIntArray` | непустые срезы | | `GetStringMap(args, key)` | объект строк (например, заголовки) | Валидация аргументов — в обработчике, а не в схеме: схема подсказывает модели, обработчик — источник истины. --- ## 7. Конфиг и live-reload Сервер читает свой конфиг из файла (по флагу `-config`) и/или из каталога тенанта `FORGE_TENANT_CONFIG/.json`. Для перечитывания на лету — `configreload`: ```go loader := configreload.New(path, parseConfig) // сверяет контент по sha256 cfg, err := loader.Get() // перечитывает только при изменении ``` `${VAR}` в конфиге разворачивается через `toolkit.Expand`; отсутствующая переменная даёт пустую строку — дальнейшая валидация отвергнет пустое обязательное поле (fail-closed). --- ## 8. Безопасность - **Минимум прав по умолчанию.** Политики задают явные allow/deny; дефолт — запрет, а не разрешение. - **Ограничивай область.** Пути — внутри разрешённых корней (jail); SQL — только параметризованный и read-only там, где это возможно; сеть — только разрешённые схемы/хосты. - **Ограничивай время.** Любая внешняя операция — с `context` и таймаутом; инструмент не должен подвешивать сервер. - **Секреты — только из окружения** (`${VAR}`), не в конфиге и не в коде. --- ## 9. Approval и probe (эмитированные паттерны) Инструменты, выполняющие side-effect, могут объявить **permission-паттерны**: хост использует их для политики одобрения, а probe-вызов позволяет узнать паттерны **без выполнения действия**. ```go toolkit.RegisterPatternTool(s, tool, func(args map[string]any) (patterns, always []string) { return []string{"write:" + toolkit.GetString(args, "path", "")}, nil }, handler) ``` Probe-вызов (`_meta["forge.permission_patterns"]=true`) возвращает паттерны в `_meta` и **не выполняет** действие (см. [`mcp-contract.md`](mcp-contract.md)). --- ## 10. Тестирование - Юнит-тесты домена и парсинга конфига; `go test -race ./...`. - Для обработчиков — прямой вызов с собранным `*mcp.CallToolRequest`. - Проверяй probe: действие не выполняется, паттерны возвращаются. --- ## 11. Definition of Done 1. Отдельный Go-модуль, stdio-only, один потокобезопасный Manager. 2. `main.go` использует `toolkit.Health()` и `toolkit.Run(...)`. 3. Каркас — из `toolkit`/`toolkit/validate`/`configreload`; домен — в своём пакете. 4. Конфиг с `${VAR}` (fail-closed) и live-reload; статический `-config` — фолбэк. 5. Side-effecting инструменты объявляют паттерны через `RegisterPatternTool`; дефолт политики — least-privilege. 6. Юнит-тесты; `go test -race ./...` зелёный. 7. `README.md`: инструменты, безопасность, подключение, схема approval. 8. `go mod tidy`, `golangci-lint run ./...`, `gofmt` — чисто.