Files
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

193 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Архитектура 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/<server>.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` — чисто.