Files
forge-toolkit/docs/ARCHITECTURE.md
T
Maksim Totmin c398045664
ci / test (push) Failing after 5s
ci / lint (push) Failing after 4s
chore: initial release v0.1.0
forge-toolkit: хелперы для разработки MCP-серверов на Go.

- toolkit: args/schema/result/env/probe/server
- toolkit/validate: валидаторы синтаксиса
- configreload: live-reload конфига по контент-хэшу
- docs: ARCHITECTURE, quickstart, mcp-contract
- template/ и examples/hello-tool/
- CI (Gitea Actions), лицензия Apache-2.0
2026-09-14 00:23:08 +07:00

8.9 KiB
Raw Blame History

Архитектура MCP-серверов на forge-toolkit

Кому: разработчику, который пишет MCP-сервер на Go с этим тулкитом. После прочтения ты должен уметь написать сервер любого домена: файлы, БД, HTTP, DevOps и т.д.

Эталон рядом: template/ (каркас) и 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

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): хост добавит свой префикс при регистрации.

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/<server>.json. Для перечитывания на лету — configreload:

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-вызов позволяет узнать паттерны без выполнения действия.

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).


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 — чисто.