Initialize project scaffold and domain model
This commit is contained in:
+11
@@ -0,0 +1,11 @@
|
||||
/target
|
||||
/.idea
|
||||
/.vscode
|
||||
/.DS_Store
|
||||
/node_modules
|
||||
/dist
|
||||
/coverage
|
||||
/.env
|
||||
/var
|
||||
*.log
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# AGENTS
|
||||
|
||||
## Purpose
|
||||
|
||||
This repository is developed through agent-assisted workflow. Follow the repository documents first, then implement code.
|
||||
|
||||
## Source of truth
|
||||
|
||||
Use the documents in this order when there is ambiguity:
|
||||
|
||||
1. `docs/architecture.md`
|
||||
2. `docs/module-decomposition.md`
|
||||
3. `docs/data-model.md`
|
||||
4. `docs/database-schema.md`
|
||||
5. `docs/admin-api.md`
|
||||
6. `docs/mcp-interface.md`
|
||||
7. `docs/rust-design.md`
|
||||
8. `docs/development-rules.md`
|
||||
9. `docs/rust-code-rules.md`
|
||||
10. `docs/implementation-plan.md`
|
||||
|
||||
If code and docs diverge, update docs first or together with code.
|
||||
|
||||
## Workflow
|
||||
|
||||
- Follow `Red -> Green -> Refactor -> Commit`.
|
||||
- Every feature uses its own branch: `feat/<feature-name>`.
|
||||
- Commits must be atomic.
|
||||
- Push periodically after one or more logically complete `RGR + commit` cycles.
|
||||
- Do not wait for the whole feature to be finished before pushing.
|
||||
|
||||
## Language rules
|
||||
|
||||
- Commit messages must be in English.
|
||||
- Code identifiers must be in English.
|
||||
- Code comments are avoided by default.
|
||||
- If a code comment is truly unavoidable, it must be in English.
|
||||
|
||||
## Code rules
|
||||
|
||||
- Prefer self-documenting code.
|
||||
- Keep domain logic separate from storage, transport, and orchestration.
|
||||
- Do not create god-structs or giant services.
|
||||
- Keep `pub` surface minimal.
|
||||
- Avoid `unwrap`, `expect`, `todo`, `dbg`, and `panic` in production code.
|
||||
- `unsafe` is forbidden by default.
|
||||
|
||||
## Commands
|
||||
|
||||
Use the canonical commands from `justfile`:
|
||||
|
||||
- `just fmt`
|
||||
- `just fmt-check`
|
||||
- `just check`
|
||||
- `just clippy`
|
||||
- `just test`
|
||||
- `just verify`
|
||||
|
||||
## Current execution mode
|
||||
|
||||
- Build the Rust workspace first.
|
||||
- Keep the UI as a separate app outside the Cargo workspace.
|
||||
- Implement one vertical slice at a time.
|
||||
|
||||
## Task tracking
|
||||
|
||||
- Check `TASKS.md` before starting a new piece of work.
|
||||
- Update `TASKS.md` when a task starts, finishes, or gets blocked.
|
||||
Generated
+346
@@ -0,0 +1,346 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "admin-api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "mcp-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcpaas-adapter-graphql"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"mcpaas-core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcpaas-adapter-grpc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"mcpaas-core",
|
||||
"mcpaas-proto",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcpaas-adapter-rest"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"mcpaas-core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcpaas-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcpaas-mapping"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"mcpaas-core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcpaas-proto"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"mcpaas-core",
|
||||
"mcpaas-schema",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcpaas-registry"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"mcpaas-core",
|
||||
"mcpaas-mapping",
|
||||
"mcpaas-schema",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcpaas-runtime"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"mcpaas-adapter-graphql",
|
||||
"mcpaas-adapter-grpc",
|
||||
"mcpaas-adapter-rest",
|
||||
"mcpaas-core",
|
||||
"mcpaas-mapping",
|
||||
"mcpaas-schema",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcpaas-schema"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"mcpaas-core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.50.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tokio-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"apps/admin-api",
|
||||
"apps/mcp-server",
|
||||
"crates/mcpaas-core",
|
||||
"crates/mcpaas-schema",
|
||||
"crates/mcpaas-mapping",
|
||||
"crates/mcpaas-proto",
|
||||
"crates/mcpaas-registry",
|
||||
"crates/mcpaas-runtime",
|
||||
"crates/mcpaas-adapter-rest",
|
||||
"crates/mcpaas-adapter-graphql",
|
||||
"crates/mcpaas-adapter-grpc",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
rust-version = "1.85"
|
||||
version = "0.1.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
thiserror = "2"
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
||||
tracing = "0.1"
|
||||
@@ -0,0 +1,74 @@
|
||||
# MCPaaS
|
||||
|
||||
MCPaaS - это low-code платформа для публикации внешних API в виде MCP tools без написания нового backend-обработчика под каждую интеграцию. Система предоставляет единый административный UI, в котором оператор может подключать REST, GraphQL и gRPC операции, настраивать маппинг входных и выходных данных, выполнять тестовый вызов и публиковать результат как MCP tool.
|
||||
|
||||
На текущем этапе репозиторий содержит проектную документацию и архитектурные решения, которые задают границы MVP и подход к реализации.
|
||||
|
||||
## Цели
|
||||
|
||||
- Разработать MCP server на Rust.
|
||||
- Поддержать динамическое добавление интеграций через UI или конфигурацию.
|
||||
- Обеспечить единый сценарий работы оператора для REST, GraphQL и gRPC.
|
||||
- Нормализовать внешние протоколы в единую внутреннюю модель операции.
|
||||
- Избежать генерации и деплоя нового backend-кода при добавлении каждого нового инструмента.
|
||||
|
||||
## Состав MVP
|
||||
|
||||
- Поддержка REST для `GET`, `POST`, `PUT`, `PATCH` и `DELETE`.
|
||||
- Поддержка GraphQL для `query` и `mutation` на основе шаблонов и переменных.
|
||||
- Поддержка только unary-методов gRPC.
|
||||
- Загрузка примеров `JSON` для ускоренного создания схем и чернового маппинга.
|
||||
- Загрузка `.proto` файлов или descriptor set для обнаружения схемы gRPC.
|
||||
- Импорт и экспорт конфигураций операций в `YAML`.
|
||||
- Использование `JSONPath` для точечного маппинга вложенных параметров и ответа.
|
||||
- Настройка маппинга запроса и ответа через UI.
|
||||
- Публикация tools в MCP без пересборки backend.
|
||||
|
||||
## Структура документации
|
||||
|
||||
- `docs/architecture.md` - архитектура системы, модули, потоки данных и стек.
|
||||
- `docs/module-decomposition.md` - детальная декомпозиция crates и внутренних модулей.
|
||||
- `docs/data-model.md` - формальная модель данных и JSON-структуры сущностей.
|
||||
- `docs/database-schema.md` - схема БД, связи и versioning конфигураций.
|
||||
- `docs/admin-api.md` - HTTP-контракты административного API.
|
||||
- `docs/diagrams.md` - структурные диаграммы компонентов, сущностей, БД и потоков.
|
||||
- `docs/mcp-interface.md` - транспорт и контракт публикации MCP tools.
|
||||
- `docs/testing-strategy.md` - стратегия тестирования до и во время разработки.
|
||||
- `docs/runtime-config.md` - конфигурация окружения, storage и секретов.
|
||||
- `docs/rust-design.md` - распределение методов, `impl`, `trait` и service-слоя без god-struct.
|
||||
- `docs/development-rules.md` - правила разработки, TDD-процесс и git workflow.
|
||||
- `docs/rust-code-rules.md` - Rust-specific правила кода, toolchain и linting.
|
||||
- `docs/implementation-plan.md` - последовательность модулей и фич по этапам реализации.
|
||||
- `docs/protocols/rest.md` - функциональные требования и ограничения для REST.
|
||||
- `docs/protocols/graphql.md` - функциональные требования и ограничения для GraphQL.
|
||||
- `docs/protocols/grpc.md` - функциональные требования и ограничения для gRPC.
|
||||
|
||||
## Ключевая идея продукта
|
||||
|
||||
Система строится вокруг унифицированной сущности `Operation`. Каждая операция описывает:
|
||||
|
||||
- внешний протокол,
|
||||
- целевой endpoint или метод,
|
||||
- входную схему,
|
||||
- правила маппинга входных данных,
|
||||
- параметры выполнения,
|
||||
- правила маппинга выходных данных,
|
||||
- метаданные MCP tool.
|
||||
|
||||
За счет этого MCP runtime работает с единой внутренней моделью, а протокольные адаптеры уже выполняют конкретные вызовы REST, GraphQL или gRPC.
|
||||
|
||||
Для GraphQL это означает, что в MCP публикуется не "универсальный GraphQL endpoint", а конкретная операция с фиксированным шаблоном запроса, фиксированным набором входных параметров и предсказуемой структурой ответа.
|
||||
|
||||
Для упрощения настройки оператор может загружать примеры входного и выходного `JSON`, а для gRPC - `.proto` или descriptor set. На основе этих артефактов система строит черновую схему и стартовый маппинг, который затем вручную уточняется через `JSONPath`.
|
||||
|
||||
Конфигурации операций должны импортироваться и экспортироваться в `YAML`, чтобы их можно было переносить между окружениями, хранить в git и редактировать вне UI.
|
||||
|
||||
## Поддерживаемые протоколы
|
||||
|
||||
В MVP платформа ориентируется на три основных протокольных сценария интеграции:
|
||||
|
||||
- REST
|
||||
- GraphQL
|
||||
- gRPC
|
||||
|
||||
`SOAP` сознательно не входит в MVP. Он остается актуальным для части корпоративных и государственных интеграций, но требует отдельного адаптера с поддержкой WSDL, XML Schema, SOAP envelope, namespaces и XML-oriented mapping. Для первой версии это слишком большой отдельный пласт сложности.
|
||||
@@ -0,0 +1,45 @@
|
||||
# TASKS
|
||||
|
||||
## Current
|
||||
|
||||
### `feat/domain-model`
|
||||
|
||||
Status: completed
|
||||
|
||||
DoD:
|
||||
|
||||
- core domain types from `docs/data-model.md` exist
|
||||
- basic JSON and YAML serialization works
|
||||
- domain unit tests pass
|
||||
|
||||
## Next
|
||||
|
||||
### `feat/schema-engine`
|
||||
|
||||
Status: pending
|
||||
|
||||
DoD:
|
||||
|
||||
- schema model exists
|
||||
- schema validation works
|
||||
- JSON sample normalization works
|
||||
|
||||
### `feat/mapping-engine`
|
||||
|
||||
Status: pending
|
||||
|
||||
DoD:
|
||||
|
||||
- JSONPath parsing works
|
||||
- input and output mapping work
|
||||
- draft generation from samples works
|
||||
|
||||
## Backlog
|
||||
|
||||
- `feat/registry-storage`
|
||||
- `feat/rest-vertical-slice`
|
||||
- `feat/admin-api-v1`
|
||||
- `feat/ui-v1`
|
||||
- `feat/mcp-server`
|
||||
- `feat/graphql-support`
|
||||
- `feat/grpc-support`
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "admin-api"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
fn main() {}
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "mcp-server"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
fn main() {}
|
||||
@@ -0,0 +1,3 @@
|
||||
# UI
|
||||
|
||||
The UI app is planned as a separate TypeScript project and is intentionally kept outside the Cargo workspace.
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "mcpaas-adapter-graphql"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
mcpaas-core = { path = "../mcpaas-core" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub fn crate_name() -> &'static str {
|
||||
"mcpaas-adapter-graphql"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "mcpaas-adapter-grpc"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
mcpaas-core = { path = "../mcpaas-core" }
|
||||
mcpaas-proto = { path = "../mcpaas-proto" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub fn crate_name() -> &'static str {
|
||||
"mcpaas-adapter-grpc"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "mcpaas-adapter-rest"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
mcpaas-core = { path = "../mcpaas-core" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub fn crate_name() -> &'static str {
|
||||
"mcpaas-adapter-rest"
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "mcpaas-core"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
serde_yaml.workspace = true
|
||||
@@ -0,0 +1,59 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{ids::AuthProfileId, protocol::AuthKind};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SecretRef(pub String);
|
||||
|
||||
impl SecretRef {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BearerAuthConfig {
|
||||
pub header_name: String,
|
||||
pub secret_ref: SecretRef,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BasicAuthConfig {
|
||||
pub username_secret_ref: SecretRef,
|
||||
pub password_secret_ref: SecretRef,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ApiKeyHeaderAuthConfig {
|
||||
pub header_name: String,
|
||||
pub secret_ref: SecretRef,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ApiKeyQueryAuthConfig {
|
||||
pub param_name: String,
|
||||
pub secret_ref: SecretRef,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AuthConfig {
|
||||
Bearer(BearerAuthConfig),
|
||||
Basic(BasicAuthConfig),
|
||||
ApiKeyHeader(ApiKeyHeaderAuthConfig),
|
||||
ApiKeyQuery(ApiKeyQueryAuthConfig),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AuthProfile {
|
||||
pub id: AuthProfileId,
|
||||
pub name: String,
|
||||
pub kind: AuthKind,
|
||||
pub config: AuthConfig,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
macro_rules! define_id {
|
||||
($name:ident) => {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct $name(String);
|
||||
|
||||
impl $name {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for $name {
|
||||
fn from(value: String) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for $name {
|
||||
fn from(value: &str) -> Self {
|
||||
Self(value.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for $name {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
define_id!(OperationId);
|
||||
define_id!(DescriptorId);
|
||||
define_id!(ToolId);
|
||||
define_id!(SampleId);
|
||||
define_id!(AuthProfileId);
|
||||
@@ -0,0 +1,16 @@
|
||||
pub mod auth;
|
||||
pub mod ids;
|
||||
pub mod operation;
|
||||
pub mod protocol;
|
||||
|
||||
pub use auth::{
|
||||
ApiKeyHeaderAuthConfig, ApiKeyQueryAuthConfig, AuthConfig, AuthProfile, BasicAuthConfig,
|
||||
BearerAuthConfig, SecretRef,
|
||||
};
|
||||
pub use ids::{AuthProfileId, DescriptorId, OperationId, SampleId, ToolId};
|
||||
pub use operation::{
|
||||
ConfigExport, ExecutionConfig, GeneratedDraft, GeneratedDraftStatus, GraphqlTarget,
|
||||
GrpcProtocolOptions, GrpcTarget, Operation, OperationStatus, ProtocolOptions, RestTarget,
|
||||
RetryPolicy, Samples, Target, ToolDescription, ToolExample,
|
||||
};
|
||||
pub use protocol::{AuthKind, ExportMode, GraphqlOperationType, HttpMethod, Protocol};
|
||||
@@ -0,0 +1,363 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
ids::{AuthProfileId, DescriptorId, OperationId, SampleId},
|
||||
protocol::{ExportMode, GraphqlOperationType, HttpMethod, Protocol},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OperationStatus {
|
||||
Draft,
|
||||
Testing,
|
||||
Published,
|
||||
Archived,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RestTarget {
|
||||
pub base_url: String,
|
||||
pub method: HttpMethod,
|
||||
pub path_template: String,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub static_headers: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct GraphqlTarget {
|
||||
pub endpoint: String,
|
||||
pub operation_type: GraphqlOperationType,
|
||||
pub operation_name: String,
|
||||
pub query_template: String,
|
||||
pub response_path: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct GrpcTarget {
|
||||
pub server_addr: String,
|
||||
pub package: String,
|
||||
pub service: String,
|
||||
pub method: String,
|
||||
pub descriptor_ref: DescriptorId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum Target {
|
||||
Rest(RestTarget),
|
||||
Graphql(GraphqlTarget),
|
||||
Grpc(GrpcTarget),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct RetryPolicy {
|
||||
pub max_attempts: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct GrpcProtocolOptions {
|
||||
pub use_tls: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct ProtocolOptions {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub grpc: Option<GrpcProtocolOptions>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ExecutionConfig {
|
||||
pub timeout_ms: u64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub retry_policy: Option<RetryPolicy>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub auth_profile_ref: Option<AuthProfileId>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub headers: BTreeMap<String, String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub protocol_options: Option<ProtocolOptions>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ToolExample {
|
||||
pub input: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ToolDescription {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub tags: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub examples: Vec<ToolExample>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct Samples {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_json_sample_ref: Option<SampleId>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_json_sample_ref: Option<SampleId>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub proto_file_ref: Option<SampleId>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub descriptor_ref: Option<DescriptorId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GeneratedDraftStatus {
|
||||
None,
|
||||
Available,
|
||||
Stale,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct GeneratedDraft {
|
||||
pub status: GeneratedDraftStatus,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub source_types: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub generated_at: Option<String>,
|
||||
pub input_schema_generated: bool,
|
||||
pub output_schema_generated: bool,
|
||||
pub input_mapping_generated: bool,
|
||||
pub output_mapping_generated: bool,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ConfigExport {
|
||||
pub format_version: String,
|
||||
pub export_mode: ExportMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Operation<TSchema, TMapping> {
|
||||
pub id: OperationId,
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
pub protocol: Protocol,
|
||||
pub status: OperationStatus,
|
||||
pub version: u32,
|
||||
pub target: Target,
|
||||
pub input_schema: TSchema,
|
||||
pub output_schema: TSchema,
|
||||
pub input_mapping: TMapping,
|
||||
pub output_mapping: TMapping,
|
||||
pub execution_config: ExecutionConfig,
|
||||
pub tool_description: ToolDescription,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub samples: Option<Samples>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub generated_draft: Option<GeneratedDraft>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub config_export: Option<ConfigExport>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub published_at: Option<String>,
|
||||
}
|
||||
|
||||
impl<TSchema, TMapping> Operation<TSchema, TMapping> {
|
||||
pub fn tool_name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn is_draft(&self) -> bool {
|
||||
self.status == OperationStatus::Draft
|
||||
}
|
||||
|
||||
pub fn is_published(&self) -> bool {
|
||||
self.status == OperationStatus::Published
|
||||
}
|
||||
|
||||
pub fn protocol(&self) -> Protocol {
|
||||
self.protocol
|
||||
}
|
||||
|
||||
pub fn auth_profile_ref(&self) -> Option<&AuthProfileId> {
|
||||
self.execution_config.auth_profile_ref.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
auth::{AuthConfig, AuthProfile, BearerAuthConfig, SecretRef},
|
||||
ids::{AuthProfileId, OperationId},
|
||||
operation::{
|
||||
ConfigExport, ExecutionConfig, GraphqlTarget, Operation, OperationStatus,
|
||||
ProtocolOptions, RestTarget, Samples, Target, ToolDescription, ToolExample,
|
||||
},
|
||||
protocol::{AuthKind, ExportMode, GraphqlOperationType, HttpMethod, Protocol},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn rest_target_serializes_with_kind_tag() {
|
||||
let target = Target::Rest(RestTarget {
|
||||
base_url: "https://api.example.com".to_owned(),
|
||||
method: HttpMethod::Post,
|
||||
path_template: "/v1/leads".to_owned(),
|
||||
static_headers: BTreeMap::new(),
|
||||
});
|
||||
|
||||
let value = serde_json::to_value(target).unwrap();
|
||||
|
||||
assert_eq!(value["kind"], "rest");
|
||||
assert_eq!(value["method"], "POST");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphql_target_serializes_response_path() {
|
||||
let target = Target::Graphql(GraphqlTarget {
|
||||
endpoint: "https://api.example.com/graphql".to_owned(),
|
||||
operation_type: GraphqlOperationType::Mutation,
|
||||
operation_name: "CreateLead".to_owned(),
|
||||
query_template: "mutation {}".to_owned(),
|
||||
response_path: "$.response.body.data.createLead".to_owned(),
|
||||
});
|
||||
|
||||
let value = serde_json::to_value(target).unwrap();
|
||||
|
||||
assert_eq!(value["kind"], "graphql");
|
||||
assert_eq!(value["operation_type"], "mutation");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_exposes_local_domain_helpers() {
|
||||
let operation = Operation {
|
||||
id: OperationId::new("op_01"),
|
||||
name: "crm_create_lead".to_owned(),
|
||||
display_name: "Create Lead".to_owned(),
|
||||
protocol: Protocol::Rest,
|
||||
status: OperationStatus::Draft,
|
||||
version: 1,
|
||||
target: Target::Rest(RestTarget {
|
||||
base_url: "https://api.example.com".to_owned(),
|
||||
method: HttpMethod::Post,
|
||||
path_template: "/v1/leads".to_owned(),
|
||||
static_headers: BTreeMap::new(),
|
||||
}),
|
||||
input_schema: json!({"type":"object"}),
|
||||
output_schema: json!({"type":"object"}),
|
||||
input_mapping: json!({"rules":[]}),
|
||||
output_mapping: json!({"rules":[]}),
|
||||
execution_config: ExecutionConfig {
|
||||
timeout_ms: 10_000,
|
||||
retry_policy: None,
|
||||
auth_profile_ref: Some(AuthProfileId::new("auth_01")),
|
||||
headers: BTreeMap::new(),
|
||||
protocol_options: Some(ProtocolOptions::default()),
|
||||
},
|
||||
tool_description: ToolDescription {
|
||||
title: "Create CRM lead".to_owned(),
|
||||
description: "Creates a new lead.".to_owned(),
|
||||
tags: Vec::new(),
|
||||
examples: Vec::new(),
|
||||
},
|
||||
samples: Some(Samples::default()),
|
||||
generated_draft: None,
|
||||
config_export: None,
|
||||
created_at: "2026-03-25T08:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T08:00:00Z".to_owned(),
|
||||
published_at: None,
|
||||
};
|
||||
|
||||
assert_eq!(operation.tool_name(), "crm_create_lead");
|
||||
assert!(operation.is_draft());
|
||||
assert!(!operation.is_published());
|
||||
assert_eq!(operation.protocol(), Protocol::Rest);
|
||||
assert_eq!(
|
||||
operation.auth_profile_ref().map(|value| value.as_str()),
|
||||
Some("auth_01")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_profile_serializes_secret_refs_without_secret_values() {
|
||||
let profile = AuthProfile {
|
||||
id: AuthProfileId::new("auth_01"),
|
||||
name: "crm-prod-bearer".to_owned(),
|
||||
kind: AuthKind::Bearer,
|
||||
config: AuthConfig::Bearer(BearerAuthConfig {
|
||||
header_name: "Authorization".to_owned(),
|
||||
secret_ref: SecretRef::new("secret://auth/crm-prod-token"),
|
||||
}),
|
||||
created_at: "2026-03-25T08:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T08:00:00Z".to_owned(),
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(profile).unwrap();
|
||||
|
||||
assert_eq!(value["kind"], "bearer");
|
||||
assert_eq!(
|
||||
value["config"]["bearer"]["secret_ref"],
|
||||
"secret://auth/crm-prod-token"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_roundtrips_through_yaml() {
|
||||
let operation = Operation {
|
||||
id: OperationId::new("op_01"),
|
||||
name: "crm_create_lead".to_owned(),
|
||||
display_name: "Create Lead".to_owned(),
|
||||
protocol: Protocol::Rest,
|
||||
status: OperationStatus::Published,
|
||||
version: 3,
|
||||
target: Target::Rest(RestTarget {
|
||||
base_url: "https://api.example.com".to_owned(),
|
||||
method: HttpMethod::Post,
|
||||
path_template: "/v1/leads".to_owned(),
|
||||
static_headers: BTreeMap::from([("X-App-Source".to_owned(), "mcpaas".to_owned())]),
|
||||
}),
|
||||
input_schema: json!({"type":"object","fields":{"email":{"type":"string","required":true}}}),
|
||||
output_schema: json!({"type":"object","fields":{"id":{"type":"string","required":true}}}),
|
||||
input_mapping: json!({"rules":[{"source":"$.mcp.email","target":"$.request.body.email"}]}),
|
||||
output_mapping: json!({"rules":[{"source":"$.response.body.id","target":"$.output.id"}]}),
|
||||
execution_config: ExecutionConfig {
|
||||
timeout_ms: 10_000,
|
||||
retry_policy: None,
|
||||
auth_profile_ref: Some(AuthProfileId::new("auth_01")),
|
||||
headers: BTreeMap::new(),
|
||||
protocol_options: Some(ProtocolOptions::default()),
|
||||
},
|
||||
tool_description: ToolDescription {
|
||||
title: "Create CRM lead".to_owned(),
|
||||
description: "Creates a new lead.".to_owned(),
|
||||
tags: vec!["crm".to_owned()],
|
||||
examples: vec![ToolExample {
|
||||
input: json!({"email":"user@example.com"}),
|
||||
}],
|
||||
},
|
||||
samples: Some(Samples::default()),
|
||||
generated_draft: None,
|
||||
config_export: Some(ConfigExport {
|
||||
format_version: "1".to_owned(),
|
||||
export_mode: ExportMode::Portable,
|
||||
}),
|
||||
created_at: "2026-03-25T08:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T08:10:00Z".to_owned(),
|
||||
published_at: Some("2026-03-25T08:15:00Z".to_owned()),
|
||||
};
|
||||
|
||||
let yaml = serde_yaml::to_string(&operation).unwrap();
|
||||
let restored: Operation<serde_json::Value, serde_json::Value> =
|
||||
serde_yaml::from_str(&yaml).unwrap();
|
||||
|
||||
assert!(yaml.contains("protocol: rest"));
|
||||
assert!(yaml.contains("export_mode: portable"));
|
||||
assert_eq!(restored, operation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Protocol {
|
||||
Rest,
|
||||
Graphql,
|
||||
Grpc,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum HttpMethod {
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Patch,
|
||||
Delete,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GraphqlOperationType {
|
||||
Query,
|
||||
Mutation,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AuthKind {
|
||||
Bearer,
|
||||
Basic,
|
||||
ApiKeyHeader,
|
||||
ApiKeyQuery,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ExportMode {
|
||||
Portable,
|
||||
Bundle,
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "mcpaas-mapping"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
mcpaas-core = { path = "../mcpaas-core" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
serde_yaml.workspace = true
|
||||
@@ -0,0 +1,129 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MappingCondition {
|
||||
pub source: String,
|
||||
pub equals: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TransformKind {
|
||||
Identity,
|
||||
ToString,
|
||||
ToNumber,
|
||||
ToBoolean,
|
||||
Join,
|
||||
Split,
|
||||
WrapArray,
|
||||
UnwrapSingleton,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Transform {
|
||||
pub kind: TransformKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MappingRule {
|
||||
pub source: String,
|
||||
pub target: String,
|
||||
#[serde(default)]
|
||||
pub required: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub default_value: Option<Value>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub transform: Option<Transform>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub condition: Option<MappingCondition>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
|
||||
pub struct MappingSet {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub rules: Vec<MappingRule>,
|
||||
}
|
||||
|
||||
impl MappingSet {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.rules.is_empty()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.rules.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::{MappingRule, MappingSet, Transform, TransformKind};
|
||||
|
||||
#[test]
|
||||
fn mapping_set_reports_non_empty_rules() {
|
||||
let mapping = MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.mcp.email".to_owned(),
|
||||
target: "$.request.body.contact.email".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: Some(Transform {
|
||||
kind: TransformKind::Identity,
|
||||
}),
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
};
|
||||
|
||||
assert!(!mapping.is_empty());
|
||||
assert_eq!(mapping.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapping_rule_serializes_jsonpath_fields() {
|
||||
let mapping = MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.response.body.id".to_owned(),
|
||||
target: "$.output.id".to_owned(),
|
||||
required: false,
|
||||
default_value: Some(json!("lead_123")),
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: Some("map identifier".to_owned()),
|
||||
}],
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(mapping).unwrap();
|
||||
|
||||
assert_eq!(value["rules"][0]["source"], "$.response.body.id");
|
||||
assert_eq!(value["rules"][0]["target"], "$.output.id");
|
||||
assert_eq!(value["rules"][0]["default_value"], "lead_123");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mapping_set_roundtrips_through_yaml() {
|
||||
let mapping = MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.mcp.tags".to_owned(),
|
||||
target: "$.request.body.tags".to_owned(),
|
||||
required: false,
|
||||
default_value: None,
|
||||
transform: Some(Transform {
|
||||
kind: TransformKind::WrapArray,
|
||||
}),
|
||||
condition: None,
|
||||
notes: Some("normalize tags".to_owned()),
|
||||
}],
|
||||
};
|
||||
|
||||
let yaml = serde_yaml::to_string(&mapping).unwrap();
|
||||
let restored: MappingSet = serde_yaml::from_str(&yaml).unwrap();
|
||||
|
||||
assert!(yaml.contains("source: $.mcp.tags"));
|
||||
assert_eq!(restored, mapping);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "mcpaas-proto"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
mcpaas-core = { path = "../mcpaas-core" }
|
||||
mcpaas-schema = { path = "../mcpaas-schema" }
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub fn crate_name() -> &'static str {
|
||||
"mcpaas-proto"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "mcpaas-registry"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
mcpaas-core = { path = "../mcpaas-core" }
|
||||
mcpaas-mapping = { path = "../mcpaas-mapping" }
|
||||
mcpaas-schema = { path = "../mcpaas-schema" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub fn crate_name() -> &'static str {
|
||||
"mcpaas-registry"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "mcpaas-runtime"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
mcpaas-adapter-graphql = { path = "../mcpaas-adapter-graphql" }
|
||||
mcpaas-adapter-grpc = { path = "../mcpaas-adapter-grpc" }
|
||||
mcpaas-adapter-rest = { path = "../mcpaas-adapter-rest" }
|
||||
mcpaas-core = { path = "../mcpaas-core" }
|
||||
mcpaas-mapping = { path = "../mcpaas-mapping" }
|
||||
mcpaas-schema = { path = "../mcpaas-schema" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub fn crate_name() -> &'static str {
|
||||
"mcpaas-runtime"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "mcpaas-schema"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
mcpaas-core = { path = "../mcpaas-core" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
serde_yaml.workspace = true
|
||||
@@ -0,0 +1,164 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SchemaKind {
|
||||
Object,
|
||||
Array,
|
||||
String,
|
||||
Integer,
|
||||
Number,
|
||||
Boolean,
|
||||
Enum,
|
||||
#[serde(rename = "null")]
|
||||
Null,
|
||||
Oneof,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Schema {
|
||||
#[serde(rename = "type")]
|
||||
pub kind: SchemaKind,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub required: bool,
|
||||
#[serde(default)]
|
||||
pub nullable: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub default_value: Option<Value>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub fields: BTreeMap<String, Schema>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub items: Option<Box<Schema>>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub enum_values: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub variants: Vec<Schema>,
|
||||
}
|
||||
|
||||
impl Schema {
|
||||
pub fn is_object(&self) -> bool {
|
||||
self.kind == SchemaKind::Object
|
||||
}
|
||||
|
||||
pub fn field(&self, name: &str) -> Option<&Schema> {
|
||||
self.fields.get(name)
|
||||
}
|
||||
|
||||
pub fn has_required_fields(&self) -> bool {
|
||||
self.fields.values().any(|field| field.required)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::{Schema, SchemaKind};
|
||||
|
||||
#[test]
|
||||
fn object_schema_exposes_fields() {
|
||||
let mut fields = BTreeMap::new();
|
||||
fields.insert(
|
||||
"email".to_owned(),
|
||||
Schema {
|
||||
kind: SchemaKind::String,
|
||||
description: None,
|
||||
required: true,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields: BTreeMap::new(),
|
||||
items: None,
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
},
|
||||
);
|
||||
|
||||
let schema = Schema {
|
||||
kind: SchemaKind::Object,
|
||||
description: Some("User input".to_owned()),
|
||||
required: true,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields,
|
||||
items: None,
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
};
|
||||
|
||||
assert!(schema.is_object());
|
||||
assert!(schema.has_required_fields());
|
||||
assert_eq!(
|
||||
schema.field("email").map(|field| field.required),
|
||||
Some(true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_serializes_type_field() {
|
||||
let schema = Schema {
|
||||
kind: SchemaKind::Array,
|
||||
description: None,
|
||||
required: false,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields: BTreeMap::new(),
|
||||
items: Some(Box::new(Schema {
|
||||
kind: SchemaKind::String,
|
||||
description: None,
|
||||
required: false,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields: BTreeMap::new(),
|
||||
items: None,
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
})),
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(schema).unwrap();
|
||||
|
||||
assert_eq!(value["type"], "array");
|
||||
assert_eq!(value["items"]["type"], "string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_roundtrips_through_yaml() {
|
||||
let schema = Schema {
|
||||
kind: SchemaKind::Object,
|
||||
description: Some("Lead output".to_owned()),
|
||||
required: true,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields: BTreeMap::from([(
|
||||
"id".to_owned(),
|
||||
Schema {
|
||||
kind: SchemaKind::String,
|
||||
description: None,
|
||||
required: true,
|
||||
nullable: false,
|
||||
default_value: None,
|
||||
fields: BTreeMap::new(),
|
||||
items: None,
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
},
|
||||
)]),
|
||||
items: None,
|
||||
enum_values: Vec::new(),
|
||||
variants: Vec::new(),
|
||||
};
|
||||
|
||||
let yaml = serde_yaml::to_string(&schema).unwrap();
|
||||
let restored: Schema = serde_yaml::from_str(&yaml).unwrap();
|
||||
|
||||
assert!(yaml.contains("type: object"));
|
||||
assert_eq!(restored, schema);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
# Admin API
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ фиксирует HTTP-контракты административного API, через которое UI управляет операциями, загружает артефакты, тестирует вызовы и выполняет YAML import/export.
|
||||
|
||||
Документ задает логический контракт. Конкретные детали `axum` handlers, auth middleware и response envelope могут уточняться при реализации.
|
||||
|
||||
## 2. Общие правила API
|
||||
|
||||
- все payload по умолчанию в `JSON`;
|
||||
- import/export конфигурации используют `YAML` как payload или файл;
|
||||
- версии operation адресуются явно;
|
||||
- published операция - это ссылка на конкретную version;
|
||||
- ошибки валидации возвращаются отдельно от transport errors.
|
||||
|
||||
Базовый префикс:
|
||||
|
||||
```text
|
||||
/api/admin
|
||||
```
|
||||
|
||||
## 3. Основные ресурсы
|
||||
|
||||
- `operations`
|
||||
- `versions`
|
||||
- `samples`
|
||||
- `descriptors`
|
||||
- `auth-profiles`
|
||||
- `test-runs`
|
||||
- `config import/export`
|
||||
|
||||
## 4. CRUD операций
|
||||
|
||||
### `GET /api/admin/operations`
|
||||
|
||||
Назначение:
|
||||
|
||||
- список операций для UI.
|
||||
|
||||
Параметры:
|
||||
|
||||
- `protocol`
|
||||
- `status`
|
||||
- `search`
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": "op_01",
|
||||
"name": "crm_create_lead",
|
||||
"display_name": "Create Lead",
|
||||
"protocol": "rest",
|
||||
"status": "draft",
|
||||
"current_draft_version": 3,
|
||||
"latest_published_version": 2,
|
||||
"updated_at": "2026-03-25T09:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/admin/operations`
|
||||
|
||||
Назначение:
|
||||
|
||||
- создание новой операции и версии `1`.
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "crm_create_lead",
|
||||
"display_name": "Create Lead",
|
||||
"protocol": "rest",
|
||||
"target": {
|
||||
"kind": "rest",
|
||||
"base_url": "https://api.example.com",
|
||||
"method": "POST",
|
||||
"path_template": "/v1/leads"
|
||||
},
|
||||
"input_schema": { "type": "object", "fields": {} },
|
||||
"output_schema": { "type": "object", "fields": {} },
|
||||
"input_mapping": { "rules": [] },
|
||||
"output_mapping": { "rules": [] },
|
||||
"execution_config": {
|
||||
"timeout_ms": 10000
|
||||
},
|
||||
"tool_description": {
|
||||
"title": "Create CRM lead",
|
||||
"description": "Creates a new lead."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "op_01",
|
||||
"version": 1,
|
||||
"status": "draft"
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/admin/operations/{operation_id}`
|
||||
|
||||
Назначение:
|
||||
|
||||
- получить метаданные operation и ссылки на draft/published версии.
|
||||
|
||||
### `GET /api/admin/operations/{operation_id}/versions/{version}`
|
||||
|
||||
Назначение:
|
||||
|
||||
- получить полную конфигурацию конкретной версии.
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/versions`
|
||||
|
||||
Назначение:
|
||||
|
||||
- создать новую draft-версию на основе текущего payload.
|
||||
|
||||
Тело:
|
||||
|
||||
- полная конфигурация operation;
|
||||
- опционально `change_note`.
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "op_01",
|
||||
"version": 4,
|
||||
"status": "draft"
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Публикация
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/publish`
|
||||
|
||||
Назначение:
|
||||
|
||||
- опубликовать текущую draft-версию.
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 4
|
||||
}
|
||||
```
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "op_01",
|
||||
"published_version": 4,
|
||||
"published_at": "2026-03-25T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/archive`
|
||||
|
||||
Назначение:
|
||||
|
||||
- перевести operation в archived status.
|
||||
|
||||
## 6. Samples и schema artifacts
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/samples/input-json`
|
||||
|
||||
Назначение:
|
||||
|
||||
- загрузить sample входного JSON.
|
||||
|
||||
Тип:
|
||||
|
||||
- `multipart/form-data` или raw `application/json`.
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"sample_id": "file_01",
|
||||
"sample_kind": "input_json"
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/samples/output-json`
|
||||
|
||||
Назначение:
|
||||
|
||||
- загрузить sample выходного JSON.
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/descriptors/proto`
|
||||
|
||||
Назначение:
|
||||
|
||||
- загрузить `.proto`.
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/descriptors/descriptor-set`
|
||||
|
||||
Назначение:
|
||||
|
||||
- загрузить `descriptor set`.
|
||||
|
||||
### `GET /api/admin/operations/{operation_id}/grpc/services`
|
||||
|
||||
Назначение:
|
||||
|
||||
- получить discovery summary по services и methods.
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"package": "crm.v1",
|
||||
"service": "LeadService",
|
||||
"methods": [
|
||||
{
|
||||
"name": "CreateLead",
|
||||
"kind": "unary"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Черновая генерация схем и mappings
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/drafts/generate`
|
||||
|
||||
Назначение:
|
||||
|
||||
- построить черновую схему и mappings из samples и schema artifacts.
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"sources": [
|
||||
"input_json_sample",
|
||||
"output_json_sample"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"generated_draft": {
|
||||
"status": "available",
|
||||
"input_schema_generated": true,
|
||||
"output_schema_generated": true,
|
||||
"input_mapping_generated": true,
|
||||
"output_mapping_generated": true,
|
||||
"warnings": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 8. Тестовый запуск
|
||||
|
||||
### `POST /api/admin/operations/{operation_id}/test-runs`
|
||||
|
||||
Назначение:
|
||||
|
||||
- выполнить тестовый вызов draft-конфигурации.
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 4,
|
||||
"input": {
|
||||
"name": "Alice",
|
||||
"email": "alice@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"request_preview": {
|
||||
"body": {
|
||||
"name": "Alice",
|
||||
"email": "alice@example.com"
|
||||
}
|
||||
},
|
||||
"response_preview": {
|
||||
"id": "lead_123",
|
||||
"status": "created"
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Auth profiles
|
||||
|
||||
### `GET /api/admin/auth-profiles`
|
||||
|
||||
Назначение:
|
||||
|
||||
- список доступных профилей аутентификации.
|
||||
|
||||
### `POST /api/admin/auth-profiles`
|
||||
|
||||
Назначение:
|
||||
|
||||
- создать новый auth profile.
|
||||
|
||||
Тело:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "crm-prod-bearer",
|
||||
"kind": "bearer",
|
||||
"config": {
|
||||
"header_name": "Authorization",
|
||||
"secret_ref": "secret://auth/crm-prod-token"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/admin/auth-profiles/{auth_profile_id}`
|
||||
|
||||
Назначение:
|
||||
|
||||
- получить metadata auth profile без раскрытия секрета.
|
||||
|
||||
## 10. YAML export
|
||||
|
||||
### `GET /api/admin/operations/{operation_id}/export`
|
||||
|
||||
Назначение:
|
||||
|
||||
- экспортировать конфигурацию операции в `YAML`.
|
||||
|
||||
Параметры:
|
||||
|
||||
- `version` - опционально, если нужно экспортировать не current draft;
|
||||
- `mode=portable|bundle`
|
||||
|
||||
Ответ:
|
||||
|
||||
- `Content-Type: application/yaml`
|
||||
- тело ответа - YAML document
|
||||
|
||||
### Пример YAML response
|
||||
|
||||
```yaml
|
||||
format_version: "1"
|
||||
kind: operation
|
||||
operation:
|
||||
name: crm_create_lead
|
||||
protocol: rest
|
||||
status: draft
|
||||
```
|
||||
|
||||
## 11. YAML import
|
||||
|
||||
### `POST /api/admin/operations/import`
|
||||
|
||||
Назначение:
|
||||
|
||||
- импортировать operation из YAML.
|
||||
|
||||
Тип:
|
||||
|
||||
- `application/yaml`
|
||||
- или `multipart/form-data` с YAML файлом
|
||||
|
||||
Параметры:
|
||||
|
||||
- `mode=create|upsert`
|
||||
|
||||
Ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"operation_id": "op_01",
|
||||
"version": 5,
|
||||
"import_mode": "upsert",
|
||||
"warnings": []
|
||||
}
|
||||
```
|
||||
|
||||
## 12. Ошибки
|
||||
|
||||
Рекомендуемые классы ошибок:
|
||||
|
||||
- `validation_error`
|
||||
- `mapping_error`
|
||||
- `schema_error`
|
||||
- `descriptor_error`
|
||||
- `auth_profile_error`
|
||||
- `yaml_import_error`
|
||||
- `runtime_test_error`
|
||||
- `not_found`
|
||||
- `conflict`
|
||||
|
||||
Пример:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "validation_error",
|
||||
"message": "Invalid JSONPath in input_mapping rule 2",
|
||||
"details": {
|
||||
"field": "input_mapping.rules[1].source"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 13. Что важно не допустить
|
||||
|
||||
- смешивание CRUD и publish semantics в одном endpoint;
|
||||
- обновление draft "поверх" существующей версии без создания новой version;
|
||||
- YAML import как скрытый апдейт без явного режима `create|upsert`;
|
||||
- возврат открытых секретов из auth-profile endpoints;
|
||||
- привязку runtime к admin DTO;
|
||||
- endpoints, возвращающие разные формы одной и той же сущности без причины.
|
||||
|
||||
## 14. Практический итог
|
||||
|
||||
Минимальный рабочий набор admin API для MVP:
|
||||
|
||||
- список и чтение operations;
|
||||
- создание новой version;
|
||||
- publish;
|
||||
- upload samples и descriptors;
|
||||
- auth profiles;
|
||||
- generate draft;
|
||||
- test run;
|
||||
- YAML import/export.
|
||||
|
||||
Этого достаточно, чтобы UI полностью управлял жизненным циклом operation без ручного редактирования кода backend.
|
||||
@@ -0,0 +1,517 @@
|
||||
# Архитектура
|
||||
|
||||
## 1. Назначение проекта
|
||||
|
||||
Проект представляет собой платформу для динамической публикации внешних API в виде MCP tools. Пользователь конфигурирует операцию через административный UI вместо написания отдельного backend-обработчика. Платформа сохраняет конфигурацию, валидирует ее, позволяет выполнить тестовый вызов и публикует операцию для использования LLM через MCP.
|
||||
|
||||
Главная инженерная цель проекта - представить разные протоколы как единый набор операций с точки зрения MCP-слоя.
|
||||
|
||||
## 2. Ключевой принцип проектирования
|
||||
|
||||
Центральная абстракция системы - `Operation`.
|
||||
|
||||
Каждая операция описывает один вызываемый элемент независимо от протокола:
|
||||
|
||||
- `name` - внутреннее уникальное имя.
|
||||
- `display_name` - имя, отображаемое в UI.
|
||||
- `protocol` - `rest`, `graphql` или `grpc`.
|
||||
- `target` - хост и протокол-специфичное описание назначения.
|
||||
- `input_schema` - нормализованный входной контракт.
|
||||
- `input_mapping` - правила отображения MCP-входа в поля целевого запроса.
|
||||
- `execution_config` - auth-профиль, таймауты, заголовки и протокол-специфичные параметры.
|
||||
- `output_mapping` - правила отображения ответа внешней системы в нормализованный выход.
|
||||
- `tool_description` - метаданные для MCP и LLM.
|
||||
- `status` - draft, testing, published, archived.
|
||||
|
||||
MCP server должен понимать только нормализованный контракт. Протокольные адаптеры должны преобразовывать нормализованную модель в конкретный REST, GraphQL или gRPC вызов и затем возвращать ответ обратно в нормализованный JSON.
|
||||
|
||||
## 3. Границы продукта
|
||||
|
||||
### Входит в MVP
|
||||
|
||||
- Административный UI для создания и редактирования операций.
|
||||
- Динамический реестр операций.
|
||||
- Runtime-выполнение REST операций.
|
||||
- Runtime-выполнение GraphQL операций.
|
||||
- Runtime-выполнение unary gRPC методов.
|
||||
- Загрузка примеров `JSON` для ускоренного создания схем и mappings.
|
||||
- Импорт и экспорт конфигураций в `YAML`.
|
||||
- Тестирование операций до публикации.
|
||||
- Публикация MCP tools на основе данных из реестра.
|
||||
- Hot reload опубликованных операций без изменения backend-кода.
|
||||
|
||||
### Не входит в MVP
|
||||
|
||||
- gRPC streaming.
|
||||
- Полноценный импорт OpenAPI с автоматической генерацией маппинга.
|
||||
- Полноценный визуальный конструктор GraphQL-запросов.
|
||||
- SOAP.
|
||||
- Выполнение произвольного кода внутри mapping-правил.
|
||||
- Оркестрация нескольких операций в виде workflow.
|
||||
- Мультитенантность и биллинг.
|
||||
|
||||
## 4. Пользовательский сценарий
|
||||
|
||||
Сценарий работы оператора должен быть одинаковым для всех протоколов:
|
||||
|
||||
1. Выбрать протокол.
|
||||
2. Указать целевой хост или сервер.
|
||||
3. Выбрать или описать внешнюю операцию.
|
||||
4. Определить MCP-входные параметры.
|
||||
5. Сопоставить MCP-вход с внешним запросом.
|
||||
6. Сопоставить внешний ответ с MCP-выходом.
|
||||
7. Добавить описание для MCP и LLM.
|
||||
8. Выполнить тестовый вызов.
|
||||
9. Опубликовать операцию.
|
||||
|
||||
UI должен максимально скрывать протокольную сложность. REST endpoint, GraphQL operation и gRPC method должны отображаться для оператора как "операция с входными и выходными параметрами".
|
||||
|
||||
## 5. Стратегия по протоколам
|
||||
|
||||
### REST
|
||||
|
||||
REST-адаптер является базовым и должен реализовываться первым.
|
||||
|
||||
Поддержка в MVP:
|
||||
|
||||
- `GET`
|
||||
- `POST`
|
||||
- `PUT`
|
||||
- `PATCH`
|
||||
- `DELETE`
|
||||
- path parameters
|
||||
- query parameters
|
||||
- headers
|
||||
- JSON request body
|
||||
- JSON response body
|
||||
- аутентификация `Bearer`, `Basic` и API key
|
||||
|
||||
Пользователь настраивает:
|
||||
|
||||
- base URL,
|
||||
- HTTP method,
|
||||
- path template,
|
||||
- request mapping,
|
||||
- response mapping.
|
||||
|
||||
### GraphQL
|
||||
|
||||
Поддержка GraphQL в MVP должна быть намеренно упрощена.
|
||||
|
||||
Поддержка в MVP:
|
||||
|
||||
- `query`
|
||||
- `mutation`
|
||||
- endpoint URL
|
||||
- request headers
|
||||
- operation template
|
||||
- variables mapping
|
||||
- извлечение результата из `data`
|
||||
|
||||
Пользователь настраивает:
|
||||
|
||||
- GraphQL endpoint,
|
||||
- шаблон операции,
|
||||
- схему переменных,
|
||||
- маппинг переменных,
|
||||
- путь к нужным данным в ответе.
|
||||
|
||||
Introspection может быть добавлен позже как вспомогательная функция UI, но первая рабочая версия системы не должна от него зависеть.
|
||||
|
||||
Ключевое ограничение GraphQL в проекте: одна MCP operation должна соответствовать одному конкретному GraphQL-запросу или mutation с заранее определенным selection set. Платформа не должна пытаться передавать LLM всю гибкость GraphQL, потому что LLM не должен формировать произвольный набор полей и произвольную структуру параметров для одного и того же tool.
|
||||
|
||||
С точки зрения MCP GraphQL в этой системе намеренно превращается в более жесткий интерфейс:
|
||||
|
||||
- один tool;
|
||||
- один шаблон `query` или `mutation`;
|
||||
- фиксированный набор входных параметров;
|
||||
- один предсказуемый формат ответа.
|
||||
|
||||
Фактически на слое MCP "универсальность" GraphQL сознательно сужается до модели, близкой к RPC или REST operation. Это делается для того, чтобы tool оставался понятным для LLM, валидируемым, предсказуемым по структуре ответа и пригодным для явного mapping.
|
||||
|
||||
### gRPC
|
||||
|
||||
gRPC - наиболее сложный протокол в этом проекте, поэтому его нужно ограничить на раннем этапе.
|
||||
|
||||
Поддержка в MVP:
|
||||
|
||||
- только unary RPC,
|
||||
- загрузка `.proto`,
|
||||
- загрузка descriptor set,
|
||||
- опционально server reflection на более позднем этапе,
|
||||
- преобразование между нормализованным JSON и protobuf-сообщениями.
|
||||
|
||||
Рекомендуемый путь реализации:
|
||||
|
||||
1. Принимать descriptor set как основной машинно-читаемый источник схемы.
|
||||
2. Опционально принимать `.proto` для удобства оператора.
|
||||
3. Парсить descriptor во внутреннюю модель схемы, удобную для UI.
|
||||
4. Показывать services, methods, входные поля и выходные поля в виде структурированной формы.
|
||||
5. Позволять пользователю настраивать input и output mapping.
|
||||
|
||||
Такой подход превращает gRPC для оператора в тот же опыт, что и REST: выбрать метод, посмотреть параметры, сопоставить поля, протестировать, опубликовать.
|
||||
|
||||
Streaming gRPC сознательно не входит в рамки проекта. Платформа ориентирована на MCP tool invocation, а MCP tool в этой системе моделируется как сценарий `запрос -> один ответ`. LLM не работает с долгоживущими транспортными сессиями и не нуждается в обработке потока сообщений для такого типа интеграции. Поэтому `server streaming`, `client streaming` и `bidirectional streaming` исключаются как архитектурно избыточные для выбранной модели взаимодействия.
|
||||
|
||||
Тот же принцип применяется и к GraphQL: даже если внешний GraphQL endpoint допускает очень гибкий способ получения данных, в MCP публикуются только заранее зафиксированные операции с контролируемым входом и контролируемым ответом.
|
||||
|
||||
## 6. Работа с файлами и автогенерация черновика
|
||||
|
||||
Для упрощения конфигурирования система должна поддерживать загрузку файлов и примеров данных, из которых можно собрать стартовую конфигурацию operation.
|
||||
|
||||
Поддерживаемые источники:
|
||||
|
||||
- пример входного `JSON`;
|
||||
- пример выходного `JSON`;
|
||||
- `.proto`;
|
||||
- `descriptor set`.
|
||||
|
||||
Ожидаемый сценарий:
|
||||
|
||||
1. Оператор загружает пример входных данных и пример ответа.
|
||||
2. Система строит черновую схему входа и выхода.
|
||||
3. Система предлагает стартовый mapping по совпадающим или близким по структуре полям.
|
||||
4. Оператор вручную корректирует результат.
|
||||
5. Для точечной настройки используется `JSONPath`.
|
||||
6. Готовую конфигурацию можно экспортировать в `YAML` или импортировать обратно.
|
||||
|
||||
Для gRPC источником структуры является не пример JSON-сообщения, а `.proto` или descriptor set. Однако после преобразования protobuf-схемы во внутреннюю JSON-ориентированную модель пользовательский опыт должен оставаться тем же: видим структуру полей, получаем стартовый mapping, затем уточняем его вручную.
|
||||
|
||||
`YAML` используется как человекочитаемое представление конфигурации operation для:
|
||||
|
||||
- переноса между окружениями;
|
||||
- резервного копирования;
|
||||
- хранения в git;
|
||||
- редактирования вне UI;
|
||||
- пакетного импорта нескольких operation.
|
||||
|
||||
Storage backend для sample-файлов, `.proto`, `descriptor set` и YAML import payload в MVP должен быть локальным файловым хранилищем приложения с явным `storage_ref`. В дальнейшем этот слой можно заменить на S3-compatible storage без изменения доменной модели.
|
||||
|
||||
## 7. Внутренняя модель данных
|
||||
|
||||
Система должна приводить все данные к JSON-ориентированным структурам, чтобы UI, registry и MCP runtime работали с единым контрактом.
|
||||
|
||||
### Operation
|
||||
|
||||
- `id`
|
||||
- `name`
|
||||
- `display_name`
|
||||
- `protocol`
|
||||
- `status`
|
||||
- `target`
|
||||
- `input_schema`
|
||||
- `output_schema`
|
||||
- `input_mapping`
|
||||
- `output_mapping`
|
||||
- `execution_config`
|
||||
- `tool_description`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
### Target
|
||||
|
||||
REST target:
|
||||
|
||||
- `base_url`
|
||||
- `method`
|
||||
- `path_template`
|
||||
|
||||
GraphQL target:
|
||||
|
||||
- `endpoint`
|
||||
- `operation_type`
|
||||
- `operation_name`
|
||||
- `query_template`
|
||||
|
||||
gRPC target:
|
||||
|
||||
- `server_addr`
|
||||
- `package`
|
||||
- `service`
|
||||
- `method`
|
||||
- `descriptor_ref`
|
||||
|
||||
### Schema
|
||||
|
||||
Нормализованный формат схемы должен поддерживать:
|
||||
|
||||
- скалярные поля,
|
||||
- вложенные объекты,
|
||||
- массивы,
|
||||
- enum,
|
||||
- nullable-поля,
|
||||
- `oneof` для схем, пришедших из protobuf.
|
||||
|
||||
Транспортный формат между внутренними компонентами должен оставаться JSON, даже если конкретный адаптер под капотом работает с protobuf.
|
||||
|
||||
## 8. Модель маппинга
|
||||
|
||||
Платформе нужен отдельный слой маппинга, потому что MCP-facing параметры не совпадают напрямую с payload внешнего API.
|
||||
|
||||
Начальная версия mapping-системы должна оставаться простой, но при этом достаточно выразительной для работы со вложенными структурами:
|
||||
|
||||
- сопоставление поле-в-поле по `JSONPath`,
|
||||
- константы,
|
||||
- значения по умолчанию,
|
||||
- извлечение вложенных полей из ответа.
|
||||
|
||||
Примеры:
|
||||
|
||||
- `$.mcp.user_id -> $.request.path.userId`
|
||||
- `$.mcp.limit -> $.request.query.limit`
|
||||
- `$.response.data.user.name -> $.output.name`
|
||||
- `$.response.user.email -> $.output.email`
|
||||
|
||||
`JSONPath` используется как единый способ адресации вложенных значений в input/output mapping. Это позволяет управлять структурой и вложенностью без написания пользовательского кода.
|
||||
|
||||
Для MVP mapping engine не должен поддерживать произвольные скрипты. Достаточно единого движка `JSONPath`, констант, defaults и ограниченного набора встроенных преобразований.
|
||||
|
||||
Черновой mapping может генерироваться автоматически на основе загруженных примеров данных, но итоговая конфигурация всегда остается явной и редактируемой оператором.
|
||||
|
||||
Каноническая логическая модель остается общей для runtime и БД, но система должна уметь сериализовать и десериализовать ее также в `YAML`.
|
||||
|
||||
## 9. Основные компоненты
|
||||
|
||||
### `mcpaas-core`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- общие доменные типы,
|
||||
- идентификаторы,
|
||||
- статусы и базовые protocol-specific target types,
|
||||
- общие ошибки.
|
||||
|
||||
### `mcpaas-schema`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- нормализованные схемы входа и выхода,
|
||||
- представление типов и полей для UI и runtime,
|
||||
- валидация JSON относительно внутренней схемы.
|
||||
|
||||
### `mcpaas-mapping`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- модель mapping-правил,
|
||||
- `JSONPath` parser и validator,
|
||||
- применение input/output mapping,
|
||||
- генерация чернового mapping по sample-данным и схемам.
|
||||
|
||||
### `mcpaas-proto`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- загрузка `.proto` и `descriptor set`,
|
||||
- protobuf discovery,
|
||||
- извлечение services, methods и message schemas,
|
||||
- преобразование protobuf metadata в нормализованные схемы.
|
||||
|
||||
### `mcpaas-registry`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- постоянное хранение операций,
|
||||
- CRUD для draft и published операций,
|
||||
- выдача списка активных tools,
|
||||
- инвалидация кэша и сигналы на reload.
|
||||
|
||||
### `mcpaas-runtime`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- выполнение нормализованных операций,
|
||||
- выбор нужного протокольного адаптера,
|
||||
- применение input mapping,
|
||||
- применение output mapping,
|
||||
- единообразные runtime-ошибки.
|
||||
|
||||
### `mcpaas-adapter-rest`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- сборка HTTP-запроса из нормализованного входа,
|
||||
- отправка запроса через `reqwest`,
|
||||
- нормализация HTTP-ответа в JSON.
|
||||
|
||||
### `mcpaas-adapter-graphql`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- формирование GraphQL payload,
|
||||
- подстановка переменных,
|
||||
- отправка запроса,
|
||||
- извлечение `data` и ошибок из GraphQL-ответа.
|
||||
|
||||
### `mcpaas-adapter-grpc`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- сборка protobuf request message из нормализованного JSON,
|
||||
- вызов unary RPC метода,
|
||||
- преобразование protobuf response обратно в нормализованный JSON.
|
||||
|
||||
### `mcpaas-admin-api`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- CRUD endpoints для UI,
|
||||
- создание и управление version snapshots,
|
||||
- import/export конфигураций в `YAML`,
|
||||
- загрузка sample JSON,
|
||||
- загрузка `.proto` и descriptor set,
|
||||
- endpoints для тестового выполнения операций,
|
||||
- discovery endpoints для gRPC metadata.
|
||||
|
||||
### `mcpaas-mcp-server`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- список доступных MCP tools из registry,
|
||||
- валидация входа tool по нормализованной схеме,
|
||||
- делегирование выполнения в runtime,
|
||||
- возврат нормализованного результата MCP-клиенту.
|
||||
|
||||
### `mcpaas-ui`
|
||||
|
||||
Ответственность:
|
||||
|
||||
- wizard создания сервиса и операции,
|
||||
- editor для mapping,
|
||||
- загрузка sample-файлов и schema artifacts,
|
||||
- экран тестового вызова,
|
||||
- браузер gRPC схемы,
|
||||
- import/export конфигураций,
|
||||
- workflow публикации и отображение статуса.
|
||||
|
||||
## 10. Предлагаемая структура репозитория
|
||||
|
||||
Для реализации рекомендуется workspace-структура:
|
||||
|
||||
```text
|
||||
mcpaas/
|
||||
apps/
|
||||
admin-api/
|
||||
mcp-server/
|
||||
ui/
|
||||
crates/
|
||||
mcpaas-core/
|
||||
mcpaas-schema/
|
||||
mcpaas-mapping/
|
||||
mcpaas-proto/
|
||||
mcpaas-registry/
|
||||
mcpaas-runtime/
|
||||
mcpaas-adapter-rest/
|
||||
mcpaas-adapter-graphql/
|
||||
mcpaas-adapter-grpc/
|
||||
docs/
|
||||
```
|
||||
|
||||
Такая структура позволяет держать протокольные адаптеры независимыми и отдельно тестируемыми.
|
||||
|
||||
## 11. Технологический стек
|
||||
|
||||
### Backend
|
||||
|
||||
- Rust
|
||||
- `tokio` как async runtime
|
||||
- `axum` для HTTP API
|
||||
- `serde` и `serde_json`
|
||||
- `sqlx` для PostgreSQL или SQLite
|
||||
- `reqwest` для REST и GraphQL транспорта
|
||||
- `tonic` и `prost` для работы с gRPC
|
||||
- `tower` для middleware
|
||||
- `tracing` для логирования и диагностики
|
||||
|
||||
### Frontend
|
||||
|
||||
- TypeScript
|
||||
- React
|
||||
- Vite
|
||||
- React Router
|
||||
- TanStack Query
|
||||
- React Hook Form
|
||||
- Zod
|
||||
|
||||
Этот стек прагматичен для внутреннего административного UI: быстрая итерация, удобная работа с формами, понятная интеграция с API и отсутствие лишней сложности.
|
||||
|
||||
## 12. Почему React + Vite для UI
|
||||
|
||||
Frontend в этом проекте - это операторская консоль, а не контентный сайт. Server-side rendering здесь не требуется. Основные требования:
|
||||
|
||||
- динамические формы,
|
||||
- schema-driven рендеринг,
|
||||
- экраны тестирования и предпросмотра,
|
||||
- адаптивные административные страницы,
|
||||
- высокая скорость локальной разработки.
|
||||
|
||||
`React + TypeScript + Vite` хорошо подходит под эти условия, потому что позволяет развивать frontend независимо от Rust-сервисов и быстро собирать сложные формы вроде mapping editor и gRPC method inspector.
|
||||
|
||||
## 13. Почему Axum для backend
|
||||
|
||||
`axum` выбран как основной backend-фреймворк по следующим причинам:
|
||||
|
||||
- он построен поверх `tower` и хорошо согласуется с современным async-стеком Rust,
|
||||
- он естественно интегрируется с `tokio`, `hyper` и middleware-композицией,
|
||||
- он лучше подходит для модульной структуры с несколькими сервисами,
|
||||
- он удобен для typed handlers, shared state и собственных extractors,
|
||||
- он лучше сочетается с `tonic`, который используется для gRPC.
|
||||
|
||||
Детальная декомпозиция crates и модулей вынесена в `docs/module-decomposition.md`.
|
||||
Формальная модель данных вынесена в `docs/data-model.md`.
|
||||
Схема БД и versioning описаны в `docs/database-schema.md`.
|
||||
HTTP-контракты административного API описаны в `docs/admin-api.md`.
|
||||
Диаграммы компонентов, сущностей и потоков вынесены в `docs/diagrams.md`.
|
||||
MCP transport и способ публикации tools описаны в `docs/mcp-interface.md`.
|
||||
Стратегия тестирования описана в `docs/testing-strategy.md`, а runtime-конфигурация и storage assumptions - в `docs/runtime-config.md`.
|
||||
Rust-oriented распределение методов, `impl`, `trait` и service-слоя описано в `docs/rust-design.md`.
|
||||
Правила разработки и TDD-процесс описаны в `docs/development-rules.md`, а последовательность модулей и фич - в `docs/implementation-plan.md`.
|
||||
Rust-specific правила кода, linting и toolchain описаны в `docs/rust-code-rules.md`.
|
||||
Требования и ограничения по конкретным протоколам вынесены в `docs/protocols/rest.md`, `docs/protocols/graphql.md` и `docs/protocols/grpc.md`.
|
||||
|
||||
## 14. Runtime-поток
|
||||
|
||||
### Создание операции
|
||||
|
||||
1. UI отправляет draft операции в admin API.
|
||||
2. Admin API валидирует схему и mappings.
|
||||
3. Registry сохраняет draft.
|
||||
4. UI запускает тестовый вызов через runtime.
|
||||
5. Оператор публикует операцию.
|
||||
6. Registry помечает операцию как active.
|
||||
7. MCP server перезагружает активные операции.
|
||||
|
||||
### Выполнение tool
|
||||
|
||||
1. MCP client вызывает tool.
|
||||
2. MCP server берет определение tool из памяти.
|
||||
3. Runtime валидирует вход относительно нормализованной схемы.
|
||||
4. Runtime применяет input mapping.
|
||||
5. Runtime вызывает нужный протокольный адаптер.
|
||||
6. Runtime применяет output mapping.
|
||||
7. MCP server возвращает нормализованный результат.
|
||||
|
||||
Эта последовательность соответствует модели `один запрос -> один ответ`. Именно поэтому поддержка streaming-протоколов не рассматривается как часть MVP: она не соответствует целевой модели вызова tools со стороны LLM.
|
||||
По этой же причине GraphQL tools должны быть заранее специализированы под конкретный сценарий вызова, а не представлять собой общий конструктор запросов для LLM.
|
||||
|
||||
## 15. Нефункциональные требования
|
||||
|
||||
- Новые операции должны добавляться без изменения backend-кода.
|
||||
- Опубликованные операции должны становиться видимыми для MCP-клиентов без пересборки сервиса.
|
||||
- Runtime-ошибки должны быть наблюдаемыми и различимыми по этапам.
|
||||
- Система должна оставаться детерминированной и пригодной для аудита.
|
||||
- Протокольные адаптеры должны тестироваться независимо.
|
||||
- Все опубликованные операции должны укладываться в модель синхронного или квазисинхронного вызова `запрос -> ответ`.
|
||||
- Все опубликованные GraphQL operations должны иметь фиксированный шаблон запроса и фиксированную структуру ожидаемого результата.
|
||||
|
||||
## 16. Основные риски
|
||||
|
||||
- Динамическая работа с protobuf заметно сложнее, чем REST и GraphQL.
|
||||
- UX для маппинга может стать слишком тяжелым, если не ограничить его заранее.
|
||||
- Нормализация схем может стать непоследовательной без строгой внутренней модели.
|
||||
- Попытка поддержать слишком много возможностей протоколов замедлит реализацию.
|
||||
- Попытка сохранить всю динамическую гибкость GraphQL на уровне MCP приведет к слишком широким и плохо управляемым tools.
|
||||
|
||||
Поэтому проект должен в первую очередь реализовать один чистый end-to-end сценарий, а не широкий, но поверхностный охват возможностей.
|
||||
|
||||
`SOAP` в этой версии проекта сознательно отложен. Это не забытый протокол, а отдельное направление развития, которое потребует самостоятельного XML/WSDL слоя, отдельной схемной модели и отдельного адаптера.
|
||||
@@ -0,0 +1,696 @@
|
||||
# Модель данных
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ фиксирует формальную модель данных платформы. Его цель - определить такие структуры, которые можно без существенных изменений перенести в:
|
||||
|
||||
- Rust domain types,
|
||||
- HTTP DTO,
|
||||
- структуру таблиц БД,
|
||||
- runtime-представление operation,
|
||||
- UI-формы и конфигурационные экраны.
|
||||
|
||||
Документ не привязан к конкретной СУБД, но задает каноническую JSON-модель сущностей.
|
||||
|
||||
## 2. Общие принципы модели
|
||||
|
||||
### 2.1. Одна операция - один tool
|
||||
|
||||
Каждая `Operation` соответствует одному MCP tool. Это особенно важно для:
|
||||
|
||||
- GraphQL, где одна operation соответствует одному конкретному `query` или `mutation`;
|
||||
- gRPC, где одна operation соответствует одному unary-методу;
|
||||
- REST, где одна operation соответствует одному endpoint-сценарию.
|
||||
|
||||
### 2.2. Внутренний транспортный формат - JSON
|
||||
|
||||
Независимо от внешнего протокола внутри системы данные должны быть представлены в JSON-ориентированном виде. Даже если внешний вызов работает с protobuf, runtime, mapping и UI опираются на нормализованный JSON.
|
||||
|
||||
### 2.3. Mapping всегда явный
|
||||
|
||||
Даже если система умеет строить черновой mapping по примерам данных, итоговая конфигурация mapping должна быть явно сохранена в operation. Нельзя полагаться на неявную "магию" сопоставления во время выполнения.
|
||||
|
||||
### 2.4. JSONPath как единый язык адресации
|
||||
|
||||
Для input и output mapping используется `JSONPath`. Это позволяет единообразно ссылаться на вложенные поля во входе, промежуточном представлении запроса и нормализованном ответе.
|
||||
|
||||
### 2.5. YAML как формат обмена конфигурацией
|
||||
|
||||
Помимо канонической JSON-модели система должна поддерживать импорт и экспорт конфигураций в `YAML`. Это внешний формат обмена, а не отдельная доменная модель.
|
||||
|
||||
## 3. Корневая сущность `Operation`
|
||||
|
||||
`Operation` - основная конфигурационная сущность платформы.
|
||||
|
||||
### Поля
|
||||
|
||||
- `id` - уникальный идентификатор операции.
|
||||
- `name` - стабильное внутреннее имя.
|
||||
- `display_name` - отображаемое имя в UI.
|
||||
- `protocol` - `rest`, `graphql`, `grpc`.
|
||||
- `status` - `draft`, `testing`, `published`, `archived`.
|
||||
- `version` - версия конфигурации операции.
|
||||
- `target` - описание внешней операции.
|
||||
- `input_schema` - схема MCP-входа.
|
||||
- `output_schema` - схема MCP-выхода.
|
||||
- `input_mapping` - правила подготовки внешнего запроса.
|
||||
- `output_mapping` - правила формирования MCP-ответа.
|
||||
- `execution_config` - auth, headers, timeout, retries и protocol-specific execution settings.
|
||||
- `tool_description` - описание tool для MCP и LLM.
|
||||
- `samples` - загруженные образцы JSON и schema artifacts.
|
||||
- `generated_draft` - автоматически построенный черновик схем и mappings.
|
||||
- `config_export` - опциональные метаданные экспортируемой конфигурации.
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
- `published_at`
|
||||
|
||||
### Пример
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "op_01hr7w0m6p8x9z4n7s2k3q5t6u",
|
||||
"name": "crm_create_lead",
|
||||
"display_name": "Create Lead",
|
||||
"protocol": "rest",
|
||||
"status": "draft",
|
||||
"version": 3,
|
||||
"target": {
|
||||
"kind": "rest",
|
||||
"base_url": "https://api.example.com",
|
||||
"method": "POST",
|
||||
"path_template": "/v1/leads"
|
||||
},
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"fields": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"output_schema": {
|
||||
"type": "object",
|
||||
"fields": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"input_mapping": {
|
||||
"rules": [
|
||||
{
|
||||
"source": "$.mcp.name",
|
||||
"target": "$.request.body.name"
|
||||
},
|
||||
{
|
||||
"source": "$.mcp.email",
|
||||
"target": "$.request.body.email"
|
||||
}
|
||||
]
|
||||
},
|
||||
"output_mapping": {
|
||||
"rules": [
|
||||
{
|
||||
"source": "$.response.body.id",
|
||||
"target": "$.output.id"
|
||||
},
|
||||
{
|
||||
"source": "$.response.body.status",
|
||||
"target": "$.output.status"
|
||||
}
|
||||
]
|
||||
},
|
||||
"execution_config": {
|
||||
"timeout_ms": 10000,
|
||||
"auth_profile_ref": "auth_01hr7x8rj2d8nq8v0c4m4t1r9e"
|
||||
},
|
||||
"tool_description": {
|
||||
"title": "Create CRM lead",
|
||||
"description": "Creates a new lead in CRM by name and email."
|
||||
},
|
||||
"samples": {
|
||||
"input_json_sample_ref": "file_01hr7xj7z4k1t0qm0cxsw6f1wx",
|
||||
"output_json_sample_ref": "file_01hr7xkvt3m1p6ms3m7r2dr8wz"
|
||||
},
|
||||
"generated_draft": {
|
||||
"status": "available",
|
||||
"source_types": ["input_json_sample", "output_json_sample"]
|
||||
},
|
||||
"config_export": {
|
||||
"format_version": "1",
|
||||
"export_mode": "portable"
|
||||
},
|
||||
"created_at": "2026-03-25T08:00:00Z",
|
||||
"updated_at": "2026-03-25T08:10:00Z",
|
||||
"published_at": null
|
||||
}
|
||||
```
|
||||
|
||||
## 4. `Target`
|
||||
|
||||
`Target` описывает конкретный внешний вызов. Это discriminated union по протоколу.
|
||||
|
||||
### 4.1. `RestTarget`
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "rest",
|
||||
"base_url": "https://api.example.com",
|
||||
"method": "PATCH",
|
||||
"path_template": "/v1/users/{userId}",
|
||||
"static_headers": {
|
||||
"X-App-Source": "mcpaas"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `kind`
|
||||
- `base_url`
|
||||
- `method`
|
||||
- `path_template`
|
||||
- `static_headers`
|
||||
|
||||
### 4.2. `GraphqlTarget`
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "graphql",
|
||||
"endpoint": "https://api.example.com/graphql",
|
||||
"operation_type": "mutation",
|
||||
"operation_name": "CreateLead",
|
||||
"query_template": "mutation CreateLead($input: LeadInput!) { createLead(input: $input) { id status } }",
|
||||
"response_path": "$.response.body.data.createLead"
|
||||
}
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `kind`
|
||||
- `endpoint`
|
||||
- `operation_type`
|
||||
- `operation_name`
|
||||
- `query_template`
|
||||
- `response_path`
|
||||
|
||||
### 4.3. `GrpcTarget`
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "grpc",
|
||||
"server_addr": "https://grpc.example.com:443",
|
||||
"package": "crm.v1",
|
||||
"service": "LeadService",
|
||||
"method": "CreateLead",
|
||||
"descriptor_ref": "desc_01hr7yn4d6g1x6vwt7h9n0e7ab"
|
||||
}
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `kind`
|
||||
- `server_addr`
|
||||
- `package`
|
||||
- `service`
|
||||
- `method`
|
||||
- `descriptor_ref`
|
||||
|
||||
## 5. `Schema`
|
||||
|
||||
`Schema` - нормализованное описание входа или выхода. Это не JSON Schema в полном объеме, а внутренняя структурная модель, удобная для UI и runtime.
|
||||
|
||||
### Базовая форма
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"description": "Lead input",
|
||||
"fields": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "Lead full name"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Поддерживаемые типы
|
||||
|
||||
- `object`
|
||||
- `array`
|
||||
- `string`
|
||||
- `integer`
|
||||
- `number`
|
||||
- `boolean`
|
||||
- `enum`
|
||||
- `null`
|
||||
- `oneof`
|
||||
|
||||
### Модель поля
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"nullable": false,
|
||||
"description": "User email",
|
||||
"default": null
|
||||
}
|
||||
```
|
||||
|
||||
Тип объекта:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "object",
|
||||
"required": true,
|
||||
"fields": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Тип массива:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "array",
|
||||
"required": false,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"fields": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. `MappingSet` и `MappingRule`
|
||||
|
||||
`MappingSet` - набор правил преобразования между внутренним MCP input/output и protocol-specific request/response model.
|
||||
|
||||
### `MappingSet`
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"source": "$.mcp.user_id",
|
||||
"target": "$.request.path.userId"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `MappingRule`
|
||||
|
||||
Поля:
|
||||
|
||||
- `source` - `JSONPath` в исходном контексте.
|
||||
- `target` - `JSONPath` в целевом контексте.
|
||||
- `required` - обязательно ли правило для корректного вызова.
|
||||
- `default_value` - значение по умолчанию.
|
||||
- `transform` - встроенное преобразование.
|
||||
- `condition` - условие применения правила.
|
||||
- `notes` - служебное описание для UI.
|
||||
|
||||
Пример:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "$.mcp.profile.email",
|
||||
"target": "$.request.body.contact.email",
|
||||
"required": true,
|
||||
"default_value": null,
|
||||
"transform": {
|
||||
"kind": "identity"
|
||||
},
|
||||
"condition": null,
|
||||
"notes": "Map email to CRM contact payload"
|
||||
}
|
||||
```
|
||||
|
||||
### Контексты `source` и `target`
|
||||
|
||||
Для input mapping:
|
||||
|
||||
- `$.mcp.*`
|
||||
- `$.request.path.*`
|
||||
- `$.request.query.*`
|
||||
- `$.request.headers.*`
|
||||
- `$.request.body.*`
|
||||
- `$.request.variables.*`
|
||||
- `$.request.grpc.*`
|
||||
|
||||
Для output mapping:
|
||||
|
||||
- `$.response.body.*`
|
||||
- `$.response.data.*`
|
||||
- `$.response.grpc.*`
|
||||
- `$.output.*`
|
||||
|
||||
### `Transform`
|
||||
|
||||
Для MVP transformations должны быть ограничены:
|
||||
|
||||
- `identity`
|
||||
- `to_string`
|
||||
- `to_number`
|
||||
- `to_boolean`
|
||||
- `join`
|
||||
- `split`
|
||||
- `wrap_array`
|
||||
- `unwrap_singleton`
|
||||
|
||||
Пример:
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "to_string"
|
||||
}
|
||||
```
|
||||
|
||||
## 7. `ExecutionConfig`
|
||||
|
||||
`ExecutionConfig` задает параметры выполнения operation.
|
||||
|
||||
```json
|
||||
{
|
||||
"timeout_ms": 10000,
|
||||
"retry_policy": {
|
||||
"max_attempts": 1
|
||||
},
|
||||
"auth_profile_ref": "auth_01hr7x8rj2d8nq8v0c4m4t1r9e",
|
||||
"headers": {
|
||||
"X-Client": "mcpaas"
|
||||
},
|
||||
"protocol_options": {
|
||||
"rest": null,
|
||||
"graphql": null,
|
||||
"grpc": {
|
||||
"use_tls": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `timeout_ms`
|
||||
- `retry_policy`
|
||||
- `auth_profile_ref`
|
||||
- `headers`
|
||||
- `protocol_options`
|
||||
|
||||
Важно:
|
||||
|
||||
- здесь хранятся execution settings, а не описание бизнес-схемы;
|
||||
- `protocol_options` должны оставаться узкими и протокол-специфичными;
|
||||
- секреты не хранятся внутри operation, только ссылки на secret store или auth profile.
|
||||
|
||||
## 8. `AuthProfile`
|
||||
|
||||
`AuthProfile` - переиспользуемая конфигурация аутентификации для внешних вызовов.
|
||||
|
||||
Operation ссылается на auth profile через `auth_profile_ref`, а не хранит секреты внутри себя.
|
||||
|
||||
### Базовая форма
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "auth_01hr7x8rj2d8nq8v0c4m4t1r9e",
|
||||
"name": "crm-prod-bearer",
|
||||
"kind": "bearer",
|
||||
"config": {
|
||||
"header_name": "Authorization",
|
||||
"secret_ref": "secret://auth/crm-prod-token"
|
||||
},
|
||||
"created_at": "2026-03-25T08:00:00Z",
|
||||
"updated_at": "2026-03-25T08:10:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Поддерживаемые виды
|
||||
|
||||
- `bearer`
|
||||
- `basic`
|
||||
- `api_key_header`
|
||||
- `api_key_query`
|
||||
|
||||
### MVP-решение по секретам
|
||||
|
||||
Для MVP секреты должны храниться не в open text внутри operation version, а в отдельном secret storage слое.
|
||||
|
||||
Рекомендуемое решение:
|
||||
|
||||
- логическая ссылка в формате `secret://...`;
|
||||
- реальное значение хранится в приложении либо в зашифрованном хранилище, либо в env-backed secret store;
|
||||
- в документации и YAML export секреты всегда представляются только через `secret_ref`.
|
||||
|
||||
## 9. `ToolDescription`
|
||||
|
||||
`ToolDescription` задает MCP-представление operation.
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Get customer profile",
|
||||
"description": "Returns a customer profile by external customer identifier.",
|
||||
"tags": ["crm", "customer"],
|
||||
"examples": [
|
||||
{
|
||||
"input": {
|
||||
"customer_id": "123"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `title`
|
||||
- `description`
|
||||
- `tags`
|
||||
- `examples`
|
||||
|
||||
## 10. `Samples`
|
||||
|
||||
`Samples` связывает operation с загруженными артефактами, на основе которых может быть построен черновик.
|
||||
|
||||
```json
|
||||
{
|
||||
"input_json_sample_ref": "file_01hr7xj7z4k1t0qm0cxsw6f1wx",
|
||||
"output_json_sample_ref": "file_01hr7xkvt3m1p6ms3m7r2dr8wz",
|
||||
"proto_file_ref": null,
|
||||
"descriptor_ref": null
|
||||
}
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `input_json_sample_ref`
|
||||
- `output_json_sample_ref`
|
||||
- `proto_file_ref`
|
||||
- `descriptor_ref`
|
||||
|
||||
Примечания:
|
||||
|
||||
- для REST чаще всего используются входной и выходной JSON samples;
|
||||
- для GraphQL чаще всего полезен sample ответа;
|
||||
- для gRPC основным artifact остается `.proto` или descriptor set, но input/output JSON samples тоже могут использоваться для MCP-facing модели.
|
||||
|
||||
## 11. `GeneratedDraft`
|
||||
|
||||
`GeneratedDraft` хранит результат автоматической генерации схем и mappings.
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "available",
|
||||
"source_types": ["input_json_sample", "output_json_sample"],
|
||||
"generated_at": "2026-03-25T08:05:00Z",
|
||||
"input_schema_generated": true,
|
||||
"output_schema_generated": true,
|
||||
"input_mapping_generated": true,
|
||||
"output_mapping_generated": true,
|
||||
"warnings": [
|
||||
"Field $.response.body.meta was not mapped automatically"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
- `status` - `none`, `available`, `stale`, `failed`
|
||||
- `source_types`
|
||||
- `generated_at`
|
||||
- `input_schema_generated`
|
||||
- `output_schema_generated`
|
||||
- `input_mapping_generated`
|
||||
- `output_mapping_generated`
|
||||
- `warnings`
|
||||
|
||||
Важно:
|
||||
|
||||
- generated draft - это не runtime-источник истины;
|
||||
- runtime использует только сохраненные `input_schema`, `output_schema`, `input_mapping`, `output_mapping`;
|
||||
- generated draft нужен как вспомогательный слой для UI и ускорения конфигурирования.
|
||||
|
||||
## 12. Runtime view
|
||||
|
||||
Для исполнения operation должно существовать упрощенное runtime-представление без UI-специфики.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "op_01hr7w0m6p8x9z4n7s2k3q5t6u",
|
||||
"protocol": "rest",
|
||||
"target": {
|
||||
"kind": "rest",
|
||||
"base_url": "https://api.example.com",
|
||||
"method": "POST",
|
||||
"path_template": "/v1/leads"
|
||||
},
|
||||
"input_schema": { "type": "object", "fields": {} },
|
||||
"output_schema": { "type": "object", "fields": {} },
|
||||
"input_mapping": { "rules": [] },
|
||||
"output_mapping": { "rules": [] },
|
||||
"execution_config": {
|
||||
"timeout_ms": 10000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Runtime view должно:
|
||||
|
||||
- не содержать UI draft metadata;
|
||||
- не зависеть от raw uploaded files;
|
||||
- быть готовым к немедленному исполнению адаптером.
|
||||
|
||||
## 13. YAML-конфигурация
|
||||
|
||||
Для импорта и экспорта система должна поддерживать `YAML`-представление operation.
|
||||
|
||||
Принцип:
|
||||
|
||||
- внутренняя доменная модель одна;
|
||||
- `JSON` и `YAML` - это два способа сериализации одной и той же конфигурации;
|
||||
- runtime не зависит от конкретного формата файла;
|
||||
- `YAML` нужен для переносимости и ручного редактирования.
|
||||
|
||||
### Базовая структура YAML
|
||||
|
||||
```yaml
|
||||
format_version: "1"
|
||||
kind: operation
|
||||
operation:
|
||||
id: op_01hr7w0m6p8x9z4n7s2k3q5t6u
|
||||
name: crm_create_lead
|
||||
display_name: Create Lead
|
||||
protocol: rest
|
||||
status: draft
|
||||
version: 3
|
||||
target:
|
||||
kind: rest
|
||||
base_url: https://api.example.com
|
||||
method: POST
|
||||
path_template: /v1/leads
|
||||
input_schema:
|
||||
type: object
|
||||
fields:
|
||||
name:
|
||||
type: string
|
||||
required: true
|
||||
email:
|
||||
type: string
|
||||
required: true
|
||||
output_schema:
|
||||
type: object
|
||||
fields:
|
||||
id:
|
||||
type: string
|
||||
required: true
|
||||
status:
|
||||
type: string
|
||||
required: true
|
||||
input_mapping:
|
||||
rules:
|
||||
- source: $.mcp.name
|
||||
target: $.request.body.name
|
||||
- source: $.mcp.email
|
||||
target: $.request.body.email
|
||||
output_mapping:
|
||||
rules:
|
||||
- source: $.response.body.id
|
||||
target: $.output.id
|
||||
- source: $.response.body.status
|
||||
target: $.output.status
|
||||
execution_config:
|
||||
timeout_ms: 10000
|
||||
auth_profile_ref: auth_01hr7x8rj2d8nq8v0c4m4t1r9e
|
||||
tool_description:
|
||||
title: Create CRM lead
|
||||
description: Creates a new lead in CRM by name and email.
|
||||
```
|
||||
|
||||
### Требования к YAML import/export
|
||||
|
||||
- формат должен быть детерминированным;
|
||||
- структура должна быть человекочитаемой;
|
||||
- импорт должен валидировать схему, mapping и protocol-specific target;
|
||||
- экспорт не должен включать секреты в открытом виде;
|
||||
- ссылки на внешние артефакты допустимы, но режим экспорта должен быть явным.
|
||||
|
||||
### Режимы экспорта
|
||||
|
||||
Минимально стоит предусмотреть два режима:
|
||||
|
||||
- `portable` - экспорт только конфигурации operation и ссылок на внешние артефакты;
|
||||
- `bundle` - экспорт конфигурации operation вместе с вложенными sample metadata и descriptor metadata, если это допустимо.
|
||||
|
||||
Для MVP можно начать только с `portable`.
|
||||
|
||||
## 14. Что важно не допустить
|
||||
|
||||
- одну гигантскую `Operation`, в которой protocol-specific поля лежат вперемешку;
|
||||
- неявный mapping, который не сохраняется после генерации черновика;
|
||||
- смешивание uploaded artifacts и runtime-ready configuration;
|
||||
- хранение секретов внутри operation;
|
||||
- хранение реальных auth credentials внутри YAML export;
|
||||
- произвольные пользовательские скрипты в mapping;
|
||||
- YAML-экспорт, который становится отдельной несовместимой моделью по отношению к доменной структуре.
|
||||
|
||||
## 15. Практический итог
|
||||
|
||||
Эта модель задает основу для:
|
||||
|
||||
- Rust structs в `mcpaas-core`, `mcpaas-schema`, `mcpaas-mapping`;
|
||||
- DTO для `admin-api`;
|
||||
- таблиц `operations`, `operation_versions`, `operation_samples`, `operation_descriptors`;
|
||||
- import/export layer для `YAML` конфигураций;
|
||||
- runtime view, который будет передаваться в `mcpaas-runtime`.
|
||||
|
||||
Следующим логическим шагом после этого документа должна стать схема БД, в которой эти сущности будут разложены по таблицам и связям.
|
||||
@@ -0,0 +1,355 @@
|
||||
# Схема БД
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ фиксирует структуру хранения конфигураций, версий операций, загруженных артефактов и published runtime-view. Его цель - дать основу для SQL-миграций и для реализации `mcpaas-registry`.
|
||||
|
||||
В документе предполагается реляционная модель, ориентированная на `PostgreSQL`. Для MVP допускается адаптация под `SQLite`, но канонической считается схема, совместимая с `PostgreSQL`.
|
||||
|
||||
## 2. Общие принципы хранения
|
||||
|
||||
### 2.1. Версионирование обязательно
|
||||
|
||||
Конфигурация operation не должна храниться только в одной "живой" записи. Каждое существенное изменение должно приводить к появлению новой версии конфигурации.
|
||||
|
||||
### 2.2. Published и draft разделяются логически
|
||||
|
||||
- `draft` может меняться;
|
||||
- `published` должна ссылаться на конкретную зафиксированную версию;
|
||||
- runtime читает только опубликованные версии.
|
||||
|
||||
### 2.3. Артефакты и конфигурация не смешиваются
|
||||
|
||||
`.proto`, descriptor set, sample JSON и YAML import payload не должны храниться в той же структуре, что и runtime-ready configuration.
|
||||
|
||||
### 2.4. Секреты не хранятся внутри operation
|
||||
|
||||
В БД operation должны храниться только ссылки на auth profiles или secret references.
|
||||
|
||||
Для MVP рекомендуется отдельная таблица `auth_profiles`, где metadata и secret refs отделены от operation versions.
|
||||
|
||||
## 3. Основные таблицы
|
||||
|
||||
Минимальный набор таблиц:
|
||||
|
||||
- `operations`
|
||||
- `operation_versions`
|
||||
- `published_operations`
|
||||
- `operation_samples`
|
||||
- `descriptors`
|
||||
- `auth_profiles`
|
||||
- `yaml_import_jobs`
|
||||
|
||||
Опционально позже:
|
||||
|
||||
- `operation_test_runs`
|
||||
- `audit_log`
|
||||
|
||||
## 4. Таблица `operations`
|
||||
|
||||
Хранит стабильную сущность операции, не зависящую от конкретной версии.
|
||||
|
||||
### Поля
|
||||
|
||||
- `id` `text primary key`
|
||||
- `name` `text not null unique`
|
||||
- `display_name` `text not null`
|
||||
- `protocol` `text not null`
|
||||
- `status` `text not null`
|
||||
- `current_draft_version` `integer not null default 1`
|
||||
- `latest_published_version` `integer null`
|
||||
- `created_at` `timestamptz not null`
|
||||
- `updated_at` `timestamptz not null`
|
||||
- `published_at` `timestamptz null`
|
||||
|
||||
### Назначение
|
||||
|
||||
- быстрый список операций;
|
||||
- стабильный идентификатор для UI и MCP;
|
||||
- привязка к актуальному draft и опубликованной версии.
|
||||
|
||||
## 5. Таблица `operation_versions`
|
||||
|
||||
Хранит полную сериализованную конфигурацию конкретной версии operation.
|
||||
|
||||
### Поля
|
||||
|
||||
- `operation_id` `text not null`
|
||||
- `version` `integer not null`
|
||||
- `status` `text not null`
|
||||
- `target_json` `jsonb not null`
|
||||
- `input_schema_json` `jsonb not null`
|
||||
- `output_schema_json` `jsonb not null`
|
||||
- `input_mapping_json` `jsonb not null`
|
||||
- `output_mapping_json` `jsonb not null`
|
||||
- `execution_config_json` `jsonb not null`
|
||||
- `tool_description_json` `jsonb not null`
|
||||
- `samples_json` `jsonb null`
|
||||
- `generated_draft_json` `jsonb null`
|
||||
- `config_export_json` `jsonb null`
|
||||
- `change_note` `text null`
|
||||
- `created_at` `timestamptz not null`
|
||||
- `created_by` `text null`
|
||||
|
||||
### Ключи
|
||||
|
||||
- primary key: `(operation_id, version)`
|
||||
- foreign key: `operation_id -> operations(id)`
|
||||
- рекомендованный composite foreign key для связанных таблиц: `(operation_id, version)`
|
||||
|
||||
### Почему так
|
||||
|
||||
Для MVP выгоднее хранить version snapshot целиком, а не дробить по десятку связанных таблиц. Это:
|
||||
|
||||
- упрощает versioning;
|
||||
- упрощает откат;
|
||||
- упрощает YAML export;
|
||||
- хорошо сочетается с JSON-oriented доменной моделью.
|
||||
|
||||
## 6. Таблица `published_operations`
|
||||
|
||||
Хранит явную published-привязку, которую читает runtime.
|
||||
|
||||
### Поля
|
||||
|
||||
- `operation_id` `text primary key`
|
||||
- `version` `integer not null`
|
||||
- `published_at` `timestamptz not null`
|
||||
- `published_by` `text null`
|
||||
|
||||
### Назначение
|
||||
|
||||
- быстрый доступ к published runtime-view;
|
||||
- отсутствие двусмысленности, какая именно версия сейчас активна;
|
||||
- простой invalidation для runtime cache.
|
||||
|
||||
### Рекомендуемая целостность
|
||||
|
||||
- `operation_id -> operations(id)`
|
||||
- `(operation_id, version) -> operation_versions(operation_id, version)`
|
||||
|
||||
## 7. Таблица `operation_samples`
|
||||
|
||||
Хранит метаданные и ссылки на sample artifacts.
|
||||
|
||||
### Поля
|
||||
|
||||
- `id` `text primary key`
|
||||
- `operation_id` `text not null`
|
||||
- `version` `integer not null`
|
||||
- `sample_kind` `text not null`
|
||||
- `storage_ref` `text not null`
|
||||
- `content_type` `text not null`
|
||||
- `file_name` `text null`
|
||||
- `created_at` `timestamptz not null`
|
||||
|
||||
### Варианты `sample_kind`
|
||||
|
||||
- `input_json`
|
||||
- `output_json`
|
||||
- `yaml_import_source`
|
||||
|
||||
### Назначение
|
||||
|
||||
- не класть большие sample payload в основные version records;
|
||||
- иметь возможность переиспользовать или пересобирать draft mapping;
|
||||
- отслеживать, из каких sample-данных строился черновик.
|
||||
|
||||
### Рекомендуемая целостность
|
||||
|
||||
- `operation_id -> operations(id)`
|
||||
- `(operation_id, version) -> operation_versions(operation_id, version)`
|
||||
|
||||
## 8. Таблица `descriptors`
|
||||
|
||||
Хранит gRPC schema artifacts.
|
||||
|
||||
### Поля
|
||||
|
||||
- `id` `text primary key`
|
||||
- `operation_id` `text null`
|
||||
- `version` `integer null`
|
||||
- `descriptor_kind` `text not null`
|
||||
- `storage_ref` `text not null`
|
||||
- `source_name` `text null`
|
||||
- `package_index_json` `jsonb null`
|
||||
- `created_at` `timestamptz not null`
|
||||
|
||||
### Варианты `descriptor_kind`
|
||||
|
||||
- `proto_upload`
|
||||
- `descriptor_set`
|
||||
- `reflection_snapshot`
|
||||
|
||||
### Назначение
|
||||
|
||||
- связывать gRPC operation с конкретной схемой;
|
||||
- не хранить binary descriptor внутри основной operation version;
|
||||
- иметь отдельную точку для discovery metadata.
|
||||
|
||||
### Рекомендуемая целостность
|
||||
|
||||
- если descriptor привязан к version, то `(operation_id, version) -> operation_versions(operation_id, version)`
|
||||
|
||||
## 9. Таблица `yaml_import_jobs`
|
||||
|
||||
Для MVP можно импортировать YAML синхронно, но таблицу под журнал импорта лучше предусмотреть сразу.
|
||||
|
||||
### Поля
|
||||
|
||||
- `id` `text primary key`
|
||||
- `source_sample_id` `text null`
|
||||
- `status` `text not null`
|
||||
- `format_version` `text not null`
|
||||
- `mode` `text not null`
|
||||
- `result_operation_id` `text null`
|
||||
- `result_version` `integer null`
|
||||
- `error_text` `text null`
|
||||
- `created_at` `timestamptz not null`
|
||||
- `finished_at` `timestamptz null`
|
||||
|
||||
### Назначение
|
||||
|
||||
- аудит импортов;
|
||||
- разбор ошибок валидации;
|
||||
- поддержка будущего async import pipeline.
|
||||
|
||||
## 10. Таблица `auth_profiles`
|
||||
|
||||
Хранит переиспользуемые профили аутентификации для внешних вызовов.
|
||||
|
||||
### Поля
|
||||
|
||||
- `id` `text primary key`
|
||||
- `name` `text not null unique`
|
||||
- `kind` `text not null`
|
||||
- `config_json` `jsonb not null`
|
||||
- `created_at` `timestamptz not null`
|
||||
- `updated_at` `timestamptz not null`
|
||||
|
||||
### Варианты `kind`
|
||||
|
||||
- `bearer`
|
||||
- `basic`
|
||||
- `api_key_header`
|
||||
- `api_key_query`
|
||||
|
||||
### Правило
|
||||
|
||||
`config_json` должен содержать только `secret_ref`, а не открытые секреты.
|
||||
|
||||
## 11. Предлагаемая SQL-форма
|
||||
|
||||
```sql
|
||||
create table operations (
|
||||
id text primary key,
|
||||
name text not null unique,
|
||||
display_name text not null,
|
||||
protocol text not null,
|
||||
status text not null,
|
||||
current_draft_version integer not null default 1,
|
||||
latest_published_version integer null,
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz not null,
|
||||
published_at timestamptz null
|
||||
);
|
||||
|
||||
create table operation_versions (
|
||||
operation_id text not null references operations(id),
|
||||
version integer not null,
|
||||
status text not null,
|
||||
target_json jsonb not null,
|
||||
input_schema_json jsonb not null,
|
||||
output_schema_json jsonb not null,
|
||||
input_mapping_json jsonb not null,
|
||||
output_mapping_json jsonb not null,
|
||||
execution_config_json jsonb not null,
|
||||
tool_description_json jsonb not null,
|
||||
samples_json jsonb null,
|
||||
generated_draft_json jsonb null,
|
||||
config_export_json jsonb null,
|
||||
change_note text null,
|
||||
created_at timestamptz not null,
|
||||
created_by text null,
|
||||
primary key (operation_id, version)
|
||||
);
|
||||
|
||||
create table published_operations (
|
||||
operation_id text primary key references operations(id),
|
||||
version integer not null,
|
||||
published_at timestamptz not null,
|
||||
published_by text null,
|
||||
foreign key (operation_id, version)
|
||||
references operation_versions(operation_id, version)
|
||||
);
|
||||
|
||||
create table auth_profiles (
|
||||
id text primary key,
|
||||
name text not null unique,
|
||||
kind text not null,
|
||||
config_json jsonb not null,
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz not null
|
||||
);
|
||||
```
|
||||
|
||||
## 12. Индексы
|
||||
|
||||
Минимально нужны:
|
||||
|
||||
- index on `operations(protocol)`
|
||||
- index on `operations(status)`
|
||||
- index on `operation_versions(operation_id, created_at desc)`
|
||||
- index on `published_operations(version)`
|
||||
- index on `operation_samples(operation_id, version)`
|
||||
- index on `descriptors(operation_id, version)`
|
||||
- index on `auth_profiles(kind)`
|
||||
|
||||
## 13. Versioning flow
|
||||
|
||||
### Создание операции
|
||||
|
||||
1. Создается запись в `operations`.
|
||||
2. Создается версия `1` в `operation_versions`.
|
||||
3. `current_draft_version = 1`.
|
||||
|
||||
### Изменение draft
|
||||
|
||||
1. Читается текущий draft.
|
||||
2. Создается новая версия `n + 1`.
|
||||
3. В `operations.current_draft_version` пишется новая версия.
|
||||
4. Published версия не меняется.
|
||||
|
||||
### Публикация
|
||||
|
||||
1. Берется текущий draft version.
|
||||
2. В `published_operations` upsert-ится ссылка на эту версию.
|
||||
3. В `operations.latest_published_version` пишется та же версия.
|
||||
4. Runtime cache получает сигнал на reload.
|
||||
|
||||
### Импорт YAML
|
||||
|
||||
1. YAML валидируется.
|
||||
2. Определяется create или update сценарий.
|
||||
3. Создается новая запись в `operation_versions`.
|
||||
4. При необходимости создается запись в `yaml_import_jobs`.
|
||||
|
||||
## 14. Что не должно храниться в БД в таком виде
|
||||
|
||||
- секреты в открытом виде;
|
||||
- runtime cache;
|
||||
- скомпилированные adapter clients;
|
||||
- невалидированные черновики, не приводимые к доменной модели.
|
||||
|
||||
## 15. Практический итог
|
||||
|
||||
Для MVP рекомендован такой подход:
|
||||
|
||||
- `operations` - стабильная идентичность;
|
||||
- `operation_versions` - полные version snapshots;
|
||||
- `published_operations` - текущая активная версия;
|
||||
- `operation_samples` и `descriptors` - внешние артефакты;
|
||||
- `auth_profiles` - переиспользуемая внешняя аутентификация;
|
||||
- `yaml_import_jobs` - журнал импортов.
|
||||
|
||||
Эта схема хорошо ложится на `sqlx`, не требует избыточной нормализации и соответствует JSON-oriented модели домена.
|
||||
@@ -0,0 +1,251 @@
|
||||
# Правила разработки
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ фиксирует, как именно должен разрабатываться проект:
|
||||
|
||||
- в каком стиле писать код;
|
||||
- как применять `TDD`;
|
||||
- как вести git;
|
||||
- как принимать архитектурные решения во время реализации.
|
||||
|
||||
Цель документа - сделать процесс разработки предсказуемым и не дать проекту расползтись по качеству.
|
||||
|
||||
## 2. Базовые принципы
|
||||
|
||||
- сначала проектирование, потом код;
|
||||
- сначала тест, потом реализация;
|
||||
- идти по циклу `RGR + commit`;
|
||||
- сначала маленький модуль, потом интеграция;
|
||||
- одна ответственность на один модуль;
|
||||
- никаких "временных" god-struct и "потом распилим".
|
||||
|
||||
## 3. Основной процесс разработки
|
||||
|
||||
Рекомендуемый цикл для каждой фичи:
|
||||
|
||||
1. Зафиксировать контракт в документации или тесте.
|
||||
2. Написать failing test.
|
||||
3. Реализовать минимальный код, который проходит тест.
|
||||
4. Выполнить refactor без изменения поведения.
|
||||
5. Сделать атомарный commit.
|
||||
6. Если логически завершена часть фичи, сделать push.
|
||||
7. Добавить integration test, если фича выходит за границы одного модуля.
|
||||
8. Обновить документацию, если изменился контракт.
|
||||
|
||||
Это и есть базовый `TDD`-процесс проекта в форме `Red -> Green -> Refactor -> Commit`.
|
||||
|
||||
## 4. RGR + commit
|
||||
|
||||
Для проекта принимается классический цикл:
|
||||
|
||||
1. `Red`
|
||||
2. `Green`
|
||||
3. `Refactor`
|
||||
4. `Commit`
|
||||
|
||||
Правила:
|
||||
|
||||
- без commit после завершенного `RGR`-цикла шаг не считается завершенным;
|
||||
- commit должен фиксировать одну логическую единицу изменения;
|
||||
- если несколько `RGR + commit` логично закрывают часть фичи, после этого делается push;
|
||||
- не нужно ждать полного завершения всей feature branch, чтобы отправить изменения в удаленный репозиторий.
|
||||
|
||||
## 5. TDD-правила
|
||||
|
||||
### 5.1. Что пишется через TDD обязательно
|
||||
|
||||
- `mcpaas-schema`
|
||||
- `mcpaas-mapping`
|
||||
- `mcpaas-registry`
|
||||
- `mcpaas-runtime`
|
||||
- YAML import/export
|
||||
- versioning logic
|
||||
|
||||
### 5.2. Что допускается делать сначала каркасом, потом тестами
|
||||
|
||||
- frontend layout;
|
||||
- wiring приложений;
|
||||
- пустые `axum` handlers;
|
||||
- начальный scaffold `cargo workspace`.
|
||||
|
||||
Но как только появляется логика, она должна переходить под тесты.
|
||||
|
||||
### 5.3. Правило минимального шага
|
||||
|
||||
Нельзя писать сразу большую "умную" реализацию на сотни строк без промежуточных тестов.
|
||||
|
||||
Особенно это запрещено для:
|
||||
|
||||
- mapping engine;
|
||||
- protobuf normalization;
|
||||
- publish flow;
|
||||
- YAML import pipeline.
|
||||
|
||||
## 6. Правила по коду
|
||||
|
||||
### 6.1. Стиль модулей
|
||||
|
||||
- модуль должен иметь одну четкую ответственность;
|
||||
- публичный API модуля должен быть минимальным;
|
||||
- если модуль начинает решать две разные задачи, он делится;
|
||||
- `utils`, `common`, `helpers` допускаются только в очень редких случаях и с узким смыслом.
|
||||
|
||||
### 6.2. Стиль структур
|
||||
|
||||
- маленькие `struct`;
|
||||
- явные типы вместо "универсальных" JSON-объектов там, где контракт уже известен;
|
||||
- protocol-specific поля не смешиваются в одной структуре без discriminated union;
|
||||
- методы на `impl` не должны тащить инфраструктурные зависимости.
|
||||
|
||||
### 6.3. Самоописывающийся код
|
||||
|
||||
Для проекта принимается подход self-documenting code:
|
||||
|
||||
- названия функций, переменных, типов и модулей должны быть достаточно точными, чтобы код читался как текст;
|
||||
- комментарии в коде считаются исключением, а не нормой;
|
||||
- если код хочется "объяснить" комментарием, сначала нужно попытаться упростить названия и декомпозицию;
|
||||
- комментарии в коде по умолчанию не пишутся.
|
||||
|
||||
Допустимое исключение:
|
||||
|
||||
- редкий комментарий для неочевидного инварианта или ограничения внешнего протокола.
|
||||
|
||||
Но базовое правило проекта:
|
||||
|
||||
- комментарии в коде исключаем.
|
||||
|
||||
### 6.4. Язык кода и git
|
||||
|
||||
Для проекта фиксируется:
|
||||
|
||||
- commit messages только на английском языке;
|
||||
- комментарии в коде только на английском языке, если они все-таки неизбежны;
|
||||
- имена типов, функций, переменных, модулей и тестов только на английском языке.
|
||||
|
||||
### 6.5. Стиль ошибок
|
||||
|
||||
- использовать типизированные ошибки;
|
||||
- ошибка должна сохранять стадию отказа: schema, mapping, adapter, external, persistence;
|
||||
- нельзя сваливать все в строковый `anyhow` на границах домена.
|
||||
|
||||
### 6.6. Стиль async
|
||||
|
||||
- `async` использовать только там, где есть реальная I/O или async boundaries;
|
||||
- не превращать чистую доменную логику в `async` без причины;
|
||||
- не смешивать чистую валидацию и сетевые вызовы в одном методе.
|
||||
|
||||
## 7. Правила проектирования
|
||||
|
||||
Проект разрабатывается с опорой на `Clean Architecture`.
|
||||
|
||||
### 7.1. Что нельзя делать
|
||||
|
||||
- один большой `OperationService` на весь проект;
|
||||
- один `AppState` со всеми зависимостями мира;
|
||||
- один adapter с `match protocol` на сотни строк;
|
||||
- доменные методы, которые знают про SQL, HTTP и файлы одновременно;
|
||||
- скрытую магию в mapping generation.
|
||||
|
||||
### 7.2. Что нужно делать
|
||||
|
||||
- использовать `trait` на инфраструктурных границах;
|
||||
- использовать `service/use case` для orchestration;
|
||||
- держать доменные типы отдельно от DTO;
|
||||
- создавать explicit runtime view для исполнения;
|
||||
- держать published flow отдельно от draft editing.
|
||||
|
||||
### 7.3. Dependency rule
|
||||
|
||||
Для проекта фиксируется dependency rule:
|
||||
|
||||
- внешние слои могут зависеть от внутренних;
|
||||
- внутренние слои не зависят от внешних;
|
||||
- домен не знает про HTTP, SQL, storage и transport;
|
||||
- adapters и repositories реализуют контракты, заданные ближе к домену.
|
||||
|
||||
## 8. Git workflow
|
||||
|
||||
## 8.1. Remote
|
||||
|
||||
Удаленный репозиторий проекта:
|
||||
|
||||
```text
|
||||
git@github.com:bsodfather/rmcp.git
|
||||
```
|
||||
|
||||
### 8.2. Ветки
|
||||
|
||||
Фиксируем такой workflow:
|
||||
|
||||
- `main` - стабильная ветка;
|
||||
- каждая фича делается в отдельной ветке `feat/<feature-name>`.
|
||||
|
||||
Примеры:
|
||||
|
||||
- `feat/workspace-scaffold`
|
||||
- `feat/schema-model`
|
||||
- `feat/mapping-engine`
|
||||
- `feat/admin-api-v1`
|
||||
|
||||
### 8.3. Коммиты
|
||||
|
||||
Коммит должен:
|
||||
|
||||
- быть маленьким;
|
||||
- содержать одну логическую единицу;
|
||||
- по возможности включать тесты вместе с реализацией;
|
||||
- не смешивать refactor и новую фичу без причины.
|
||||
|
||||
Коммиты пишутся только на английском языке.
|
||||
|
||||
Хороший порядок:
|
||||
|
||||
1. `test: add failing tests for schema validation`
|
||||
2. `feat: implement schema validator`
|
||||
3. `refactor: simplify schema field traversal`
|
||||
|
||||
### 8.4. Push policy
|
||||
|
||||
Фиксируем такую политику push:
|
||||
|
||||
- push делается периодически;
|
||||
- не нужно ждать полного закрытия feature branch;
|
||||
- push делается после одного или нескольких логически связанных `RGR + commit`;
|
||||
- push должен оставлять ветку в консистентном состоянии.
|
||||
|
||||
## 9. Definition of Done
|
||||
|
||||
Фича считается законченной, если:
|
||||
|
||||
- код реализован;
|
||||
- unit/integration tests добавлены и проходят;
|
||||
- документация обновлена, если изменился контракт;
|
||||
- нет явного архитектурного долга "починим потом";
|
||||
- фича вписывается в принятые границы слоев.
|
||||
|
||||
При этом для каждой конкретной фичи должен существовать свой локальный `DoD`, описанный в плане реализации.
|
||||
|
||||
## 10. Порядок принятия решений
|
||||
|
||||
Если в ходе реализации возникает спорное решение:
|
||||
|
||||
1. Проверяется текущая документация.
|
||||
2. Если решение уже зафиксировано, следуем ему.
|
||||
3. Если решение не зафиксировано, сначала обновляется документация.
|
||||
4. Только после этого пишется код.
|
||||
|
||||
Это важно, чтобы код не начал определять архитектуру задним числом.
|
||||
|
||||
## 11. Практический итог
|
||||
|
||||
Для этого проекта правильный режим разработки такой:
|
||||
|
||||
- проектируем заранее;
|
||||
- пишем через `TDD` в форме `RGR + commit` там, где есть логика;
|
||||
- двигаемся маленькими этапами;
|
||||
- ведем каждую фичу в отдельной ветке `feat/<feature-name>`;
|
||||
- пушим атомарно и периодически;
|
||||
- пишем commit messages и неизбежные code comments только на английском;
|
||||
- стараемся вообще обходиться без комментариев в коде за счет самоописывающегося дизайна;
|
||||
- не допускаем временных архитектурных компромиссов, которые потом невозможно разгрести.
|
||||
@@ -0,0 +1,386 @@
|
||||
# Диаграммы
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ собирает диаграммы, которые фиксируют проект до начала разработки:
|
||||
|
||||
- компонентную структуру;
|
||||
- связи между доменными сущностями;
|
||||
- хранение данных в БД;
|
||||
- основные runtime и admin-потоки.
|
||||
|
||||
Диаграммы даны в формате `Mermaid`, чтобы их можно было хранить прямо в репозитории и рендерить в Markdown-compatible tooling.
|
||||
|
||||
## 2. Компонентная диаграмма
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
UI[mcpaas-ui]
|
||||
ADMIN[admin-api]
|
||||
MCP[mcp-server]
|
||||
REG[mcpaas-registry]
|
||||
RUN[mcpaas-runtime]
|
||||
CORE[mcpaas-core]
|
||||
SCHEMA[mcpaas-schema]
|
||||
MAP[mcpaas-mapping]
|
||||
PROTO[mcpaas-proto]
|
||||
REST[adapter-rest]
|
||||
GQL[adapter-graphql]
|
||||
GRPC[adapter-grpc]
|
||||
DB[(PostgreSQL/SQLite)]
|
||||
STORE[(Artifact Storage)]
|
||||
|
||||
UI --> ADMIN
|
||||
MCP --> REG
|
||||
MCP --> RUN
|
||||
ADMIN --> REG
|
||||
ADMIN --> RUN
|
||||
ADMIN --> PROTO
|
||||
|
||||
REG --> DB
|
||||
REG --> CORE
|
||||
REG --> SCHEMA
|
||||
REG --> MAP
|
||||
|
||||
RUN --> CORE
|
||||
RUN --> SCHEMA
|
||||
RUN --> MAP
|
||||
RUN --> REST
|
||||
RUN --> GQL
|
||||
RUN --> GRPC
|
||||
|
||||
GRPC --> PROTO
|
||||
PROTO --> STORE
|
||||
ADMIN --> STORE
|
||||
```
|
||||
|
||||
## 3. Диаграмма зависимостей crates
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
CORE[mcpaas-core]
|
||||
SCHEMA[mcpaas-schema]
|
||||
MAP[mcpaas-mapping]
|
||||
PROTO[mcpaas-proto]
|
||||
REG[mcpaas-registry]
|
||||
RUN[mcpaas-runtime]
|
||||
REST[mcpaas-adapter-rest]
|
||||
GQL[mcpaas-adapter-graphql]
|
||||
GRPC[mcpaas-adapter-grpc]
|
||||
ADMIN[apps/admin-api]
|
||||
MCP[apps/mcp-server]
|
||||
|
||||
SCHEMA --> CORE
|
||||
MAP --> CORE
|
||||
PROTO --> CORE
|
||||
PROTO --> SCHEMA
|
||||
REG --> CORE
|
||||
REG --> SCHEMA
|
||||
REG --> MAP
|
||||
REST --> CORE
|
||||
GQL --> CORE
|
||||
GRPC --> CORE
|
||||
GRPC --> PROTO
|
||||
RUN --> CORE
|
||||
RUN --> SCHEMA
|
||||
RUN --> MAP
|
||||
RUN --> REST
|
||||
RUN --> GQL
|
||||
RUN --> GRPC
|
||||
ADMIN --> CORE
|
||||
ADMIN --> SCHEMA
|
||||
ADMIN --> MAP
|
||||
ADMIN --> PROTO
|
||||
ADMIN --> REG
|
||||
ADMIN --> RUN
|
||||
MCP --> CORE
|
||||
MCP --> REG
|
||||
MCP --> RUN
|
||||
```
|
||||
|
||||
## 4. Структурная диаграмма доменной модели
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Operation {
|
||||
+id
|
||||
+name
|
||||
+display_name
|
||||
+protocol
|
||||
+status
|
||||
+version
|
||||
+target
|
||||
+input_schema
|
||||
+output_schema
|
||||
+input_mapping
|
||||
+output_mapping
|
||||
+execution_config
|
||||
+tool_description
|
||||
+samples
|
||||
+generated_draft
|
||||
+config_export
|
||||
}
|
||||
|
||||
class RestTarget {
|
||||
+base_url
|
||||
+method
|
||||
+path_template
|
||||
+static_headers
|
||||
}
|
||||
|
||||
class GraphqlTarget {
|
||||
+endpoint
|
||||
+operation_type
|
||||
+operation_name
|
||||
+query_template
|
||||
+response_path
|
||||
}
|
||||
|
||||
class GrpcTarget {
|
||||
+server_addr
|
||||
+package
|
||||
+service
|
||||
+method
|
||||
+descriptor_ref
|
||||
}
|
||||
|
||||
class Schema {
|
||||
+type
|
||||
+description
|
||||
+fields
|
||||
}
|
||||
|
||||
class MappingSet {
|
||||
+rules[]
|
||||
}
|
||||
|
||||
class MappingRule {
|
||||
+source
|
||||
+target
|
||||
+required
|
||||
+default_value
|
||||
+transform
|
||||
+condition
|
||||
}
|
||||
|
||||
class ExecutionConfig {
|
||||
+timeout_ms
|
||||
+retry_policy
|
||||
+auth_profile_ref
|
||||
+headers
|
||||
+protocol_options
|
||||
}
|
||||
|
||||
class AuthProfile {
|
||||
+id
|
||||
+name
|
||||
+kind
|
||||
+config
|
||||
}
|
||||
|
||||
class ToolDescription {
|
||||
+title
|
||||
+description
|
||||
+tags
|
||||
+examples
|
||||
}
|
||||
|
||||
class Samples {
|
||||
+input_json_sample_ref
|
||||
+output_json_sample_ref
|
||||
+proto_file_ref
|
||||
+descriptor_ref
|
||||
}
|
||||
|
||||
class GeneratedDraft {
|
||||
+status
|
||||
+source_types
|
||||
+generated_at
|
||||
+warnings
|
||||
}
|
||||
|
||||
Operation --> RestTarget : target
|
||||
Operation --> GraphqlTarget : target
|
||||
Operation --> GrpcTarget : target
|
||||
Operation --> Schema : input_schema
|
||||
Operation --> Schema : output_schema
|
||||
Operation --> MappingSet : input_mapping
|
||||
Operation --> MappingSet : output_mapping
|
||||
Operation --> ExecutionConfig : execution_config
|
||||
Operation --> ToolDescription : tool_description
|
||||
Operation --> Samples : samples
|
||||
Operation --> GeneratedDraft : generated_draft
|
||||
ExecutionConfig --> AuthProfile : auth_profile_ref
|
||||
MappingSet --> MappingRule : contains
|
||||
```
|
||||
|
||||
## 5. ER-диаграмма БД
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
OPERATIONS ||--o{ OPERATION_VERSIONS : has
|
||||
OPERATIONS ||--o| PUBLISHED_OPERATIONS : publishes
|
||||
OPERATIONS ||--o{ OPERATION_SAMPLES : owns
|
||||
OPERATIONS ||--o{ DESCRIPTORS : may_use
|
||||
OPERATIONS ||--o{ YAML_IMPORT_JOBS : may_create
|
||||
AUTH_PROFILES ||--o{ OPERATION_VERSIONS : referenced_by
|
||||
|
||||
OPERATIONS {
|
||||
text id PK
|
||||
text name
|
||||
text display_name
|
||||
text protocol
|
||||
text status
|
||||
int current_draft_version
|
||||
int latest_published_version
|
||||
timestamptz created_at
|
||||
timestamptz updated_at
|
||||
timestamptz published_at
|
||||
}
|
||||
|
||||
OPERATION_VERSIONS {
|
||||
text operation_id FK
|
||||
int version
|
||||
text status
|
||||
jsonb target_json
|
||||
jsonb input_schema_json
|
||||
jsonb output_schema_json
|
||||
jsonb input_mapping_json
|
||||
jsonb output_mapping_json
|
||||
jsonb execution_config_json
|
||||
jsonb tool_description_json
|
||||
jsonb samples_json
|
||||
jsonb generated_draft_json
|
||||
jsonb config_export_json
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
PUBLISHED_OPERATIONS {
|
||||
text operation_id PK
|
||||
int version
|
||||
timestamptz published_at
|
||||
text published_by
|
||||
}
|
||||
|
||||
OPERATION_SAMPLES {
|
||||
text id PK
|
||||
text operation_id FK
|
||||
int version
|
||||
text sample_kind
|
||||
text storage_ref
|
||||
text content_type
|
||||
text file_name
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
DESCRIPTORS {
|
||||
text id PK
|
||||
text operation_id FK
|
||||
int version
|
||||
text descriptor_kind
|
||||
text storage_ref
|
||||
jsonb package_index_json
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
YAML_IMPORT_JOBS {
|
||||
text id PK
|
||||
text source_sample_id
|
||||
text status
|
||||
text format_version
|
||||
text mode
|
||||
text result_operation_id
|
||||
int result_version
|
||||
text error_text
|
||||
timestamptz created_at
|
||||
timestamptz finished_at
|
||||
}
|
||||
|
||||
AUTH_PROFILES {
|
||||
text id PK
|
||||
text name
|
||||
text kind
|
||||
jsonb config_json
|
||||
timestamptz created_at
|
||||
timestamptz updated_at
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Sequence: создание и публикация operation
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI
|
||||
participant API as admin-api
|
||||
participant REG as registry
|
||||
participant RUN as runtime
|
||||
participant MCP as mcp-server
|
||||
|
||||
UI->>API: POST /operations
|
||||
API->>REG: create operation v1
|
||||
REG-->>API: created
|
||||
API-->>UI: operation_id, version
|
||||
|
||||
UI->>API: upload samples / descriptors
|
||||
API-->>UI: artifact refs
|
||||
|
||||
UI->>API: POST /drafts/generate
|
||||
API->>REG: save generated draft metadata
|
||||
API-->>UI: generated draft
|
||||
|
||||
UI->>API: POST /test-runs
|
||||
API->>RUN: execute draft version
|
||||
RUN-->>API: request_preview + response_preview
|
||||
API-->>UI: test result
|
||||
|
||||
UI->>API: POST /publish
|
||||
API->>REG: mark version as published
|
||||
REG-->>API: published
|
||||
API-->>MCP: reload signal
|
||||
API-->>UI: published_version
|
||||
```
|
||||
|
||||
## 7. Sequence: MCP tool execution
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as MCP Client
|
||||
participant MCP as mcp-server
|
||||
participant REG as registry/cache
|
||||
participant RUN as runtime
|
||||
participant ADP as protocol adapter
|
||||
|
||||
Client->>MCP: call tool(name, input)
|
||||
MCP->>REG: resolve published runtime view
|
||||
REG-->>MCP: runtime operation
|
||||
MCP->>RUN: execute(operation, input)
|
||||
RUN->>RUN: validate input schema
|
||||
RUN->>RUN: apply input mapping
|
||||
RUN->>ADP: execute prepared request
|
||||
ADP-->>RUN: normalized response
|
||||
RUN->>RUN: apply output mapping
|
||||
RUN-->>MCP: output
|
||||
MCP-->>Client: tool result
|
||||
```
|
||||
|
||||
## 8. Sequence: YAML import
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI
|
||||
participant API as admin-api
|
||||
participant REG as registry
|
||||
|
||||
UI->>API: POST /operations/import (YAML)
|
||||
API->>API: parse YAML
|
||||
API->>API: validate schema, target, mapping
|
||||
API->>REG: create or upsert new version
|
||||
REG-->>API: operation_id, version
|
||||
API-->>UI: import result
|
||||
```
|
||||
|
||||
## 9. Что важно помнить
|
||||
|
||||
- Диаграммы фиксируют целевую архитектуру, а не точную реализацию каждого файла.
|
||||
- Если меняется модель данных или поток исполнения, сначала нужно обновлять документы, потом код.
|
||||
- Для старта разработки этого набора достаточно: компоненты, сущности, БД и ключевые sequence flows уже описаны.
|
||||
@@ -0,0 +1,399 @@
|
||||
# План реализации
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ фиксирует порядок реализации модулей и фич. Он нужен затем, чтобы разработка шла последовательно, а не параллельно во все стороны сразу.
|
||||
|
||||
Принцип:
|
||||
|
||||
- сначала фундамент;
|
||||
- потом минимальный end-to-end сценарий;
|
||||
- потом расширение протоколов;
|
||||
- потом polish и demo readiness.
|
||||
|
||||
## 2. Этап 0. Scaffold проекта
|
||||
|
||||
Цель:
|
||||
|
||||
- создать `cargo workspace`;
|
||||
- создать приложения и crates;
|
||||
- подключить базовый CI/test workflow;
|
||||
- зафиксировать структуру каталогов.
|
||||
|
||||
Состав:
|
||||
|
||||
- `apps/admin-api`
|
||||
- `apps/mcp-server`
|
||||
- `apps/ui`
|
||||
- `crates/mcpaas-core`
|
||||
- `crates/mcpaas-schema`
|
||||
- `crates/mcpaas-mapping`
|
||||
- `crates/mcpaas-proto`
|
||||
- `crates/mcpaas-registry`
|
||||
- `crates/mcpaas-runtime`
|
||||
- `crates/mcpaas-adapter-rest`
|
||||
- `crates/mcpaas-adapter-graphql`
|
||||
- `crates/mcpaas-adapter-grpc`
|
||||
|
||||
Результат:
|
||||
|
||||
- проект собирается;
|
||||
- тестовый pipeline запускается;
|
||||
- есть пустые crate boundaries.
|
||||
|
||||
DoD:
|
||||
|
||||
- создан `cargo workspace`;
|
||||
- все crates и apps объявлены в workspace;
|
||||
- проект собирается без бизнес-логики;
|
||||
- базовые test targets запускаются;
|
||||
- сделан атомарный commit со scaffold.
|
||||
|
||||
## 3. Этап 1. Базовая доменная модель
|
||||
|
||||
Цель:
|
||||
|
||||
- реализовать типы из `data-model`.
|
||||
|
||||
Фичи:
|
||||
|
||||
- `Operation`
|
||||
- `Target`
|
||||
- `Schema`
|
||||
- `MappingSet`
|
||||
- `ExecutionConfig`
|
||||
- `ToolDescription`
|
||||
- `AuthProfile`
|
||||
|
||||
Параллельно:
|
||||
|
||||
- unit tests на доменные типы;
|
||||
- базовая сериализация `JSON`/`YAML`.
|
||||
|
||||
Результат:
|
||||
|
||||
- модель данных существует как код;
|
||||
- нет инфраструктурных зависимостей внутри домена.
|
||||
|
||||
DoD:
|
||||
|
||||
- типы из `data-model` реализованы;
|
||||
- базовая сериализация `JSON` и `YAML` проходит тесты;
|
||||
- доменные `impl` не содержат инфраструктурной логики;
|
||||
- unit tests на ключевые типы проходят;
|
||||
- изменения зафиксированы через один или несколько `RGR + commit`.
|
||||
|
||||
## 4. Этап 2. Schema engine
|
||||
|
||||
Цель:
|
||||
|
||||
- реализовать `mcpaas-schema`.
|
||||
|
||||
Фичи:
|
||||
|
||||
- model полей и типов;
|
||||
- schema validation;
|
||||
- field traversal;
|
||||
- нормализация JSON samples;
|
||||
- protobuf -> schema bridge contracts.
|
||||
|
||||
Результат:
|
||||
|
||||
- можно описывать и валидировать вход/выход.
|
||||
|
||||
DoD:
|
||||
|
||||
- реализована схема полей и типов;
|
||||
- работает schema validation;
|
||||
- JSON sample normalization покрыт тестами;
|
||||
- контракты protobuf -> schema зафиксированы;
|
||||
- нет смешивания schema logic с adapter logic.
|
||||
|
||||
## 5. Этап 3. Mapping engine
|
||||
|
||||
Цель:
|
||||
|
||||
- реализовать `mcpaas-mapping`.
|
||||
|
||||
Фичи:
|
||||
|
||||
- `JSONPath` parsing и validation;
|
||||
- input mapping;
|
||||
- output mapping;
|
||||
- transforms;
|
||||
- generation draft mapping из samples.
|
||||
|
||||
Результат:
|
||||
|
||||
- можно преобразовывать MCP input в request model и response в output model.
|
||||
|
||||
DoD:
|
||||
|
||||
- `JSONPath` parsing и validation работают;
|
||||
- input/output mapping проходят unit tests;
|
||||
- generation draft mapping покрыта фикстурами;
|
||||
- transforms ограничены и задокументированы;
|
||||
- mapping engine не знает о конкретных protocol adapters.
|
||||
|
||||
## 6. Этап 4. Registry и БД
|
||||
|
||||
Цель:
|
||||
|
||||
- реализовать `mcpaas-registry` и миграции.
|
||||
|
||||
Фичи:
|
||||
|
||||
- таблицы из `database-schema`;
|
||||
- version snapshots;
|
||||
- published operations;
|
||||
- auth profiles;
|
||||
- sample metadata;
|
||||
- descriptor metadata;
|
||||
- YAML import job log.
|
||||
|
||||
Результат:
|
||||
|
||||
- конфигурации можно хранить и версионировать.
|
||||
|
||||
DoD:
|
||||
|
||||
- миграции создают таблицы из `database-schema`;
|
||||
- version snapshots работают корректно;
|
||||
- publish linkage реализован;
|
||||
- auth profiles и artifact metadata сохраняются;
|
||||
- integration tests на registry проходят на реальной БД.
|
||||
|
||||
## 7. Этап 5. REST vertical slice
|
||||
|
||||
Цель:
|
||||
|
||||
- получить первый рабочий end-to-end сценарий.
|
||||
|
||||
Фичи:
|
||||
|
||||
- `mcpaas-adapter-rest`
|
||||
- `mcpaas-runtime` для REST
|
||||
- REST test run
|
||||
- создание REST operation
|
||||
- publish REST operation
|
||||
- вызов published REST tool из MCP слоя
|
||||
|
||||
Результат:
|
||||
|
||||
- MVP работает хотя бы для REST.
|
||||
|
||||
DoD:
|
||||
|
||||
- REST operation можно создать, протестировать и опубликовать;
|
||||
- runtime исполняет REST operation end-to-end;
|
||||
- published REST tool вызывается через MCP слой;
|
||||
- negative tests на mapping и external errors существуют;
|
||||
- есть демонстрационный REST сценарий.
|
||||
|
||||
## 8. Этап 6. Admin API v1
|
||||
|
||||
Цель:
|
||||
|
||||
- дать UI полный backend-контракт для базового сценария.
|
||||
|
||||
Фичи:
|
||||
|
||||
- CRUD operations;
|
||||
- create version;
|
||||
- publish;
|
||||
- upload input/output JSON samples;
|
||||
- generate draft;
|
||||
- test run;
|
||||
- auth profiles CRUD;
|
||||
- YAML import/export.
|
||||
|
||||
Результат:
|
||||
|
||||
- UI может полностью управлять REST operation без ручных правок кода.
|
||||
|
||||
DoD:
|
||||
|
||||
- доступны CRUD, versioning, publish, samples, draft generation, test runs;
|
||||
- доступны auth profiles и YAML import/export;
|
||||
- API контракты соответствуют документации;
|
||||
- integration tests на ключевые endpoints проходят;
|
||||
- нет скрытой бизнес-логики в handlers.
|
||||
|
||||
## 9. Этап 7. UI v1
|
||||
|
||||
Цель:
|
||||
|
||||
- собрать рабочую административную консоль.
|
||||
|
||||
Фичи:
|
||||
|
||||
- список операций;
|
||||
- мастер создания операции;
|
||||
- sample upload;
|
||||
- schema viewer;
|
||||
- mapping editor;
|
||||
- test run screen;
|
||||
- publish flow;
|
||||
- YAML import/export screen.
|
||||
|
||||
Результат:
|
||||
|
||||
- есть демонстрируемый пользовательский интерфейс.
|
||||
|
||||
DoD:
|
||||
|
||||
- UI покрывает основной сценарий от создания operation до publish;
|
||||
- sample upload и mapping editor работают;
|
||||
- YAML import/export доступен из UI;
|
||||
- нет блокирующих заглушек на критическом пути демо;
|
||||
- основные пользовательские сценарии проверены вручную или integration tests.
|
||||
|
||||
## 10. Этап 8. MCP server
|
||||
|
||||
Цель:
|
||||
|
||||
- публиковать published operations как MCP tools.
|
||||
|
||||
Фичи:
|
||||
|
||||
- `Streamable HTTP`;
|
||||
- list tools;
|
||||
- call tool;
|
||||
- reload published tools;
|
||||
- error mapping MCP layer.
|
||||
|
||||
Результат:
|
||||
|
||||
- REST operation доступна как полноценный MCP tool.
|
||||
|
||||
DoD:
|
||||
|
||||
- `Streamable HTTP` transport работает;
|
||||
- list tools и call tool реализованы;
|
||||
- reload published tools работает без перезапуска;
|
||||
- ошибки runtime корректно транслируются в MCP слой;
|
||||
- есть end-to-end test или demo flow вызова published REST tool.
|
||||
|
||||
## 11. Этап 9. GraphQL support
|
||||
|
||||
Цель:
|
||||
|
||||
- добавить второй протокол без разрушения архитектуры.
|
||||
|
||||
Фичи:
|
||||
|
||||
- `mcpaas-adapter-graphql`
|
||||
- GraphQL target support;
|
||||
- variables mapping;
|
||||
- `response_path`;
|
||||
- GraphQL test runs;
|
||||
- publish и вызов через MCP.
|
||||
|
||||
Результат:
|
||||
|
||||
- второй end-to-end сценарий.
|
||||
|
||||
DoD:
|
||||
|
||||
- GraphQL operation можно создать, протестировать и опубликовать;
|
||||
- variables mapping и `response_path` работают;
|
||||
- GraphQL `errors` корректно обрабатываются;
|
||||
- published GraphQL tool вызывается через MCP слой;
|
||||
- есть demo fixture или integration scenario.
|
||||
|
||||
## 12. Этап 10. gRPC support
|
||||
|
||||
Цель:
|
||||
|
||||
- добавить unary gRPC.
|
||||
|
||||
Фичи:
|
||||
|
||||
- `mcpaas-proto`
|
||||
- descriptor loading;
|
||||
- service/method discovery;
|
||||
- protobuf normalization;
|
||||
- `mcpaas-adapter-grpc`
|
||||
- unary test runs;
|
||||
- publish и вызов через MCP.
|
||||
|
||||
Результат:
|
||||
|
||||
- третий end-to-end сценарий.
|
||||
|
||||
DoD:
|
||||
|
||||
- `.proto` или descriptor set можно загрузить;
|
||||
- unary method discovery работает;
|
||||
- JSON <-> protobuf conversion покрыта тестами;
|
||||
- gRPC operation можно протестировать и опубликовать;
|
||||
- published gRPC tool вызывается через MCP слой.
|
||||
|
||||
## 13. Этап 11. Harden и demo readiness
|
||||
|
||||
Цель:
|
||||
|
||||
- довести проект до стабильного demo state.
|
||||
|
||||
Фичи:
|
||||
|
||||
- логирование и tracing;
|
||||
- улучшение ошибок;
|
||||
- фикстуры и demo scenarios;
|
||||
- polish UI;
|
||||
- документация по запуску;
|
||||
- проверка YAML roundtrip;
|
||||
- проверка publish/reload flow.
|
||||
|
||||
DoD:
|
||||
|
||||
- демонстрационные сценарии воспроизводимы;
|
||||
- логирование и ошибки читаемы;
|
||||
- YAML roundtrip проверен;
|
||||
- publish/reload flow стабилен;
|
||||
- документация по запуску достаточна для повторения демо.
|
||||
|
||||
## 14. Приоритеты по реализации
|
||||
|
||||
Если времени не хватает, сохраняется такой приоритет:
|
||||
|
||||
1. REST end-to-end
|
||||
2. Registry + versioning
|
||||
3. YAML import/export
|
||||
4. MCP server
|
||||
5. GraphQL
|
||||
6. gRPC
|
||||
|
||||
Причина:
|
||||
|
||||
- диплом должен показать работающую платформу;
|
||||
- лучше один полный вертикальный сценарий, чем три недоделанных адаптера.
|
||||
|
||||
## 15. Разбиение по фичам
|
||||
|
||||
Каждый этап желательно бить на маленькие фичи:
|
||||
|
||||
- `schema-field-model`
|
||||
- `schema-validator`
|
||||
- `jsonpath-parser`
|
||||
- `input-mapping-engine`
|
||||
- `output-mapping-engine`
|
||||
- `registry-create-version`
|
||||
- `registry-publish`
|
||||
- `rest-adapter-post-json`
|
||||
- `yaml-export-portable`
|
||||
- `mcp-list-tools`
|
||||
|
||||
Для каждой такой фичи локальный `DoD` должен быть записан до начала реализации хотя бы в рабочем описании задачи или ветки.
|
||||
|
||||
## 16. Практический итог
|
||||
|
||||
Правильная последовательность для проекта:
|
||||
|
||||
- сначала домен и фундамент;
|
||||
- потом registry;
|
||||
- потом один полный REST vertical slice;
|
||||
- потом admin-ui и MCP слой;
|
||||
- только после этого расширение на GraphQL и gRPC.
|
||||
|
||||
Такой порядок минимизирует архитектурный риск и дает ранний рабочий результат.
|
||||
@@ -0,0 +1,162 @@
|
||||
# MCP Interface
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ фиксирует, как именно платформа публикует operations в виде MCP tools и какой transport используется в MVP.
|
||||
|
||||
Главная цель - убрать неопределенность вокруг вопроса "каким именно будет MCP server" до начала реализации.
|
||||
|
||||
## 2. Архитектурное решение
|
||||
|
||||
Для MVP `mcp-server` должен публиковать tools через network-oriented MCP transport.
|
||||
|
||||
Рекомендуемое решение:
|
||||
|
||||
- основной transport: `Streamable HTTP`;
|
||||
- отдельный `mcp-server` как сервис;
|
||||
- `stdio` не является обязательной частью MVP.
|
||||
|
||||
Причина:
|
||||
|
||||
- проект задуман как `MCPaaS`, а не как локальный single-process adapter;
|
||||
- нужен удаленный доступ к опубликованным tools;
|
||||
- published tools должны обновляться без пересборки и без локального обертывания каждого клиента.
|
||||
|
||||
## 3. Модель публикации tools
|
||||
|
||||
Каждая published operation превращается в один MCP tool.
|
||||
|
||||
Соответствие:
|
||||
|
||||
- одна published version;
|
||||
- один tool name;
|
||||
- одна input schema;
|
||||
- один результат.
|
||||
|
||||
Публикация tool основана на:
|
||||
|
||||
- `operation.name`
|
||||
- `tool_description`
|
||||
- `input_schema`
|
||||
- `published runtime view`
|
||||
|
||||
## 4. Что делает `mcp-server`
|
||||
|
||||
`mcp-server` должен:
|
||||
|
||||
- загрузить published operations из registry;
|
||||
- преобразовать их в MCP tool definitions;
|
||||
- принимать вызовы tools от MCP clients;
|
||||
- валидировать вход;
|
||||
- делегировать исполнение в runtime;
|
||||
- возвращать нормализованный output.
|
||||
|
||||
## 5. Что не делает `mcp-server`
|
||||
|
||||
`mcp-server` не должен:
|
||||
|
||||
- читать draft-конфигурации;
|
||||
- управлять versioning;
|
||||
- импортировать YAML;
|
||||
- выполнять CRUD;
|
||||
- заниматься protobuf discovery;
|
||||
- содержать бизнес-логику admin UI.
|
||||
|
||||
## 6. Published runtime view
|
||||
|
||||
`mcp-server` должен работать не с полной admin-конфигурацией, а с runtime-ready view.
|
||||
|
||||
В published runtime view остаются:
|
||||
|
||||
- `operation_id`
|
||||
- `protocol`
|
||||
- `target`
|
||||
- `input_schema`
|
||||
- `output_schema`
|
||||
- `input_mapping`
|
||||
- `output_mapping`
|
||||
- `execution_config`
|
||||
- `tool_description`
|
||||
|
||||
В published runtime view не должны попадать:
|
||||
|
||||
- raw uploaded samples;
|
||||
- generated draft metadata;
|
||||
- YAML import metadata;
|
||||
- UI-specific helper fields.
|
||||
|
||||
## 7. Transport для MVP
|
||||
|
||||
### Поддерживается
|
||||
|
||||
- `Streamable HTTP`
|
||||
|
||||
### Не обязательно в MVP
|
||||
|
||||
- `stdio`
|
||||
- дополнительные transport adapters
|
||||
|
||||
Если позже понадобится локальная интеграция, `stdio` можно добавить как отдельный transport layer поверх того же runtime.
|
||||
|
||||
## 8. MCP lifecycle
|
||||
|
||||
### Tool listing
|
||||
|
||||
При старте и после reload:
|
||||
|
||||
1. `mcp-server` читает список published operations.
|
||||
2. Строит in-memory registry tools.
|
||||
3. Отдает их через MCP list tools.
|
||||
|
||||
### Tool call
|
||||
|
||||
1. MCP client вызывает tool.
|
||||
2. `mcp-server` находит published runtime view.
|
||||
3. Валидирует input относительно schema.
|
||||
4. Делегирует вызов в `mcpaas-runtime`.
|
||||
5. Возвращает результат.
|
||||
|
||||
## 9. Обновление tools
|
||||
|
||||
После публикации новой версии:
|
||||
|
||||
1. `admin-api` фиксирует published version в registry.
|
||||
2. `registry` обновляет published_operations.
|
||||
3. `mcp-server` получает reload signal или выполняет controlled refresh.
|
||||
4. Новый tool contract становится доступен MCP clients.
|
||||
|
||||
## 10. Именование tools
|
||||
|
||||
Рекомендуется использовать стабильные tool names:
|
||||
|
||||
- `crm_create_lead`
|
||||
- `user_get_profile`
|
||||
- `inventory_list_items`
|
||||
|
||||
Требования:
|
||||
|
||||
- имя уникально в пределах платформы;
|
||||
- имя не зависит от внутреннего numeric version;
|
||||
- rename operation должен считаться отдельным осознанным изменением.
|
||||
|
||||
## 11. Ошибки MCP слоя
|
||||
|
||||
На MCP слое нужно различать:
|
||||
|
||||
- schema validation error;
|
||||
- mapping error;
|
||||
- adapter execution error;
|
||||
- external service error;
|
||||
- internal runtime error.
|
||||
|
||||
`mcp-server` не должен терять стадию ошибки при трансляции ответа клиенту.
|
||||
|
||||
## 12. Практический итог
|
||||
|
||||
Для MVP достаточно следующей фиксации:
|
||||
|
||||
- `mcp-server` - отдельный сервис;
|
||||
- transport - `Streamable HTTP`;
|
||||
- одна published operation = один MCP tool;
|
||||
- reload published tools без пересборки сервиса;
|
||||
- никакой draft-логики или admin CRUD в MCP слое.
|
||||
@@ -0,0 +1,709 @@
|
||||
# Декомпозиция модулей
|
||||
|
||||
## 1. Цель документа
|
||||
|
||||
Этот документ фиксирует детальную структуру проекта до начала активной разработки. Его задача - заранее ограничить ответственность каждого компонента, избежать разрастания `mcpaas-core`, не допустить появления "универсальных" структур на все случаи жизни и сохранить понятные границы между доменной логикой, runtime, адаптерами, API и UI.
|
||||
|
||||
Основной принцип: каждый crate отвечает за один слой системы. Внутри crate модули должны быть маленькими, тематическими и с минимальным количеством публичных сущностей.
|
||||
|
||||
## 2. Общие архитектурные правила
|
||||
|
||||
### 2.1. Что считается правильной декомпозицией
|
||||
|
||||
- `core` содержит только базовую доменную модель, идентификаторы, типы ошибок и общие контракты.
|
||||
- `registry` отвечает только за хранение и загрузку конфигурации операций.
|
||||
- `runtime` исполняет операции, но не знает о способе их хранения.
|
||||
- адаптеры знают только свой протокол и общий контракт runtime.
|
||||
- `admin-api` оркестрирует use case для UI, но не содержит протокольной логики.
|
||||
- `mcp-server` публикует tools и вызывает runtime, но не содержит бизнес-логики конфигурирования.
|
||||
- `ui` не знает внутреннюю реализацию runtime и работает только через HTTP API.
|
||||
|
||||
### 2.2. Что запрещено
|
||||
|
||||
- помещать SQL, HTTP-клиенты или gRPC-клиенты в `mcpaas-core`;
|
||||
- хранить в `core` "общие утилиты", не относящиеся к доменной модели;
|
||||
- делать `runtime`, который напрямую читает БД;
|
||||
- писать mapping-логику внутри REST, GraphQL или gRPC адаптеров;
|
||||
- дублировать доменные типы в `admin-api`, `mcp-server` и адаптерах;
|
||||
- создавать большие структуры вида `AppState`, в которые складывается все подряд;
|
||||
- создавать большие enum или config-объекты, содержащие поля всех протоколов одновременно без выделенных вложенных типов.
|
||||
|
||||
### 2.3. Предпочтительный стиль
|
||||
|
||||
- узкие интерфейсы;
|
||||
- маленькие DTO;
|
||||
- отдельные типы для draft, published и runtime-view сущностей;
|
||||
- отдельные модули для чтения, записи, валидации и исполнения;
|
||||
- композиция из небольших сервисов вместо одного глобального сервиса.
|
||||
|
||||
## 3. Workspace-структура
|
||||
|
||||
Рекомендуемая структура:
|
||||
|
||||
```text
|
||||
mcpaas/
|
||||
apps/
|
||||
admin-api/
|
||||
mcp-server/
|
||||
ui/
|
||||
crates/
|
||||
mcpaas-core/
|
||||
mcpaas-registry/
|
||||
mcpaas-runtime/
|
||||
mcpaas-adapter-rest/
|
||||
mcpaas-adapter-graphql/
|
||||
mcpaas-adapter-grpc/
|
||||
mcpaas-mapping/
|
||||
mcpaas-schema/
|
||||
mcpaas-proto/
|
||||
```
|
||||
|
||||
Дополнительные crates `mcpaas-mapping`, `mcpaas-schema` и `mcpaas-proto` нужны затем, чтобы не перегружать `mcpaas-core`.
|
||||
|
||||
## 4. Детальная декомпозиция по crate
|
||||
|
||||
### 4.1. `mcpaas-core`
|
||||
|
||||
Назначение:
|
||||
|
||||
- базовые доменные типы;
|
||||
- идентификаторы;
|
||||
- метаданные операций;
|
||||
- общие контракты и ошибки верхнего уровня.
|
||||
|
||||
Что должно лежать в crate:
|
||||
|
||||
```text
|
||||
mcpaas-core/
|
||||
src/
|
||||
lib.rs
|
||||
ids.rs
|
||||
protocol.rs
|
||||
operation/
|
||||
mod.rs
|
||||
model.rs
|
||||
status.rs
|
||||
target.rs
|
||||
metadata.rs
|
||||
auth/
|
||||
mod.rs
|
||||
profile.rs
|
||||
secret_ref.rs
|
||||
errors/
|
||||
mod.rs
|
||||
domain.rs
|
||||
validation.rs
|
||||
runtime.rs
|
||||
```
|
||||
|
||||
Описание модулей:
|
||||
|
||||
- `ids.rs` - типы `OperationId`, `DescriptorId`, `ToolId` и другие идентификаторы.
|
||||
- `protocol.rs` - enum протоколов и общие protocol capability flags.
|
||||
- `operation/model.rs` - основная доменная модель операции без технических деталей хранения.
|
||||
- `operation/status.rs` - типы состояний операции.
|
||||
- `operation/target.rs` - базовые protocol-specific target structs.
|
||||
- `operation/metadata.rs` - описание tool, display name, version, tags.
|
||||
- `auth/profile.rs` - типы auth-профилей без привязки к конкретному клиенту.
|
||||
- `auth/secret_ref.rs` - ссылки на секреты, а не сами секреты.
|
||||
- `errors/*` - типизированные ошибки доменного слоя.
|
||||
|
||||
Что не должно лежать в crate:
|
||||
|
||||
- JSON Schema реализация;
|
||||
- mapping engine;
|
||||
- SQL-модели;
|
||||
- HTTP DTO;
|
||||
- protobuf parsing;
|
||||
- `reqwest`, `sqlx`, `tonic`, `axum`.
|
||||
|
||||
Причина:
|
||||
|
||||
`mcpaas-core` должен быть максимально стабильным и независимым. Если положить туда все подряд, он станет точкой связности всей системы.
|
||||
|
||||
### 4.2. `mcpaas-schema`
|
||||
|
||||
Назначение:
|
||||
|
||||
- внутренняя модель схем;
|
||||
- нормализация входа и выхода;
|
||||
- представление полей для UI и runtime;
|
||||
- преобразование схем из разных источников в единый вид.
|
||||
|
||||
Структура:
|
||||
|
||||
```text
|
||||
mcpaas-schema/
|
||||
src/
|
||||
lib.rs
|
||||
schema/
|
||||
mod.rs
|
||||
model.rs
|
||||
field.rs
|
||||
scalar.rs
|
||||
object.rs
|
||||
collection.rs
|
||||
oneof.rs
|
||||
enums.rs
|
||||
normalize/
|
||||
mod.rs
|
||||
json.rs
|
||||
graphql.rs
|
||||
protobuf.rs
|
||||
validate/
|
||||
mod.rs
|
||||
input.rs
|
||||
output.rs
|
||||
```
|
||||
|
||||
Описание:
|
||||
|
||||
- `schema/model.rs` - корневая структура схемы.
|
||||
- `field.rs` - описание поля, nullable, required, description.
|
||||
- `scalar.rs` - базовые scalar types.
|
||||
- `object.rs` - вложенные объекты.
|
||||
- `collection.rs` - массивы и map-подобные структуры.
|
||||
- `oneof.rs` - представление protobuf `oneof`.
|
||||
- `enums.rs` - enum-значения и метаданные.
|
||||
- `normalize/*` - преобразование GraphQL и protobuf моделей в единую схему.
|
||||
- `normalize/json.rs` - нормализация загруженных JSON-примеров во внутреннюю schema model.
|
||||
- `validate/*` - проверка JSON относительно внутренней схемы.
|
||||
|
||||
Почему отдельный crate:
|
||||
|
||||
Схемы будут использоваться почти везде, но это не повод тащить их в `core`. Иначе `core` станет тяжелым и начнет менять версию при каждом изменении схемной логики.
|
||||
|
||||
### 4.3. `mcpaas-mapping`
|
||||
|
||||
Назначение:
|
||||
|
||||
- описание mapping DSL;
|
||||
- компиляция mappings в runtime-представление;
|
||||
- применение mappings к входу и выходу;
|
||||
- автогенерация чернового mapping по загруженным примерам;
|
||||
- трассировка ошибок маппинга.
|
||||
|
||||
Структура:
|
||||
|
||||
```text
|
||||
mcpaas-mapping/
|
||||
src/
|
||||
lib.rs
|
||||
model/
|
||||
mod.rs
|
||||
mapping.rs
|
||||
source.rs
|
||||
target.rs
|
||||
transform.rs
|
||||
parser/
|
||||
mod.rs
|
||||
jsonpath.rs
|
||||
compile/
|
||||
mod.rs
|
||||
plan.rs
|
||||
infer/
|
||||
mod.rs
|
||||
from_samples.rs
|
||||
from_schema.rs
|
||||
execute/
|
||||
mod.rs
|
||||
input.rs
|
||||
output.rs
|
||||
errors.rs
|
||||
```
|
||||
|
||||
Описание:
|
||||
|
||||
- `model/mapping.rs` - описание одного правила mapping.
|
||||
- `model/source.rs` - откуда берем данные: `mcp`, `response`, `constant`.
|
||||
- `model/target.rs` - куда кладем данные: `request.path`, `request.query`, `request.body`, `output`.
|
||||
- `model/transform.rs` - ограниченный набор допустимых преобразований.
|
||||
- `parser/jsonpath.rs` - единый parser и validator для `JSONPath` выражений.
|
||||
- `compile/plan.rs` - предварительно скомпилированный план маппинга.
|
||||
- `infer/from_samples.rs` - генерация чернового mapping по загруженным примерам JSON.
|
||||
- `infer/from_schema.rs` - генерация чернового mapping по нормализованной схеме.
|
||||
- `execute/input.rs` - применение mappings к запросу.
|
||||
- `execute/output.rs` - применение mappings к ответу.
|
||||
|
||||
Антипаттерн, которого нужно избежать:
|
||||
|
||||
не помещать mapping-правила в строковые поля, которые потом интерпретируются каждым адаптером по-своему. Mapping должен быть единым движком.
|
||||
|
||||
### 4.4. `mcpaas-proto`
|
||||
|
||||
Назначение:
|
||||
|
||||
- работа с `.proto` и descriptor set;
|
||||
- извлечение services, methods и message schemas;
|
||||
- преобразование protobuf metadata во внутренние типы.
|
||||
|
||||
Структура:
|
||||
|
||||
```text
|
||||
mcpaas-proto/
|
||||
src/
|
||||
lib.rs
|
||||
descriptor/
|
||||
mod.rs
|
||||
loader.rs
|
||||
source.rs
|
||||
registry.rs
|
||||
reflect/
|
||||
mod.rs
|
||||
client.rs
|
||||
model/
|
||||
mod.rs
|
||||
service.rs
|
||||
method.rs
|
||||
message.rs
|
||||
field.rs
|
||||
convert/
|
||||
mod.rs
|
||||
to_schema.rs
|
||||
to_json.rs
|
||||
from_json.rs
|
||||
errors.rs
|
||||
```
|
||||
|
||||
Описание:
|
||||
|
||||
- `descriptor/loader.rs` - загрузка descriptor set.
|
||||
- `descriptor/source.rs` - типы источников: upload, file, reflection.
|
||||
- `descriptor/registry.rs` - индексирование описаний для поиска services/methods.
|
||||
- `reflect/client.rs` - клиент server reflection, если будет добавлен.
|
||||
- `model/*` - protobuf-ориентированная промежуточная модель.
|
||||
- `convert/to_schema.rs` - перевод protobuf message в `mcpaas-schema`.
|
||||
- `convert/to_json.rs` и `from_json.rs` - преобразование runtime payload.
|
||||
|
||||
Почему отдельный crate:
|
||||
|
||||
protobuf-логика объемная и быстро начнет загрязнять gRPC adapter, если не отделить ее сразу.
|
||||
|
||||
### 4.5. `mcpaas-registry`
|
||||
|
||||
Назначение:
|
||||
|
||||
- хранение операций, схем, descriptor links и статусов;
|
||||
- выдача draft/published представлений;
|
||||
- поиск активных операций для runtime и MCP server.
|
||||
|
||||
Структура:
|
||||
|
||||
```text
|
||||
mcpaas-registry/
|
||||
src/
|
||||
lib.rs
|
||||
model/
|
||||
mod.rs
|
||||
record.rs
|
||||
draft.rs
|
||||
published.rs
|
||||
repo/
|
||||
mod.rs
|
||||
operation_repo.rs
|
||||
descriptor_repo.rs
|
||||
service/
|
||||
mod.rs
|
||||
create_operation.rs
|
||||
update_operation.rs
|
||||
publish_operation.rs
|
||||
list_operations.rs
|
||||
get_runtime_view.rs
|
||||
storage/
|
||||
mod.rs
|
||||
sqlite.rs
|
||||
postgres.rs
|
||||
cache/
|
||||
mod.rs
|
||||
runtime_cache.rs
|
||||
errors.rs
|
||||
```
|
||||
|
||||
Описание:
|
||||
|
||||
- `model/record.rs` - DB-aligned record model.
|
||||
- `model/draft.rs` - модель черновика.
|
||||
- `model/published.rs` - модель опубликованной операции.
|
||||
- `repo/*` - контракты репозиториев.
|
||||
- `service/*` - use case операции над реестром.
|
||||
- `storage/*` - реализации репозиториев на `sqlx`.
|
||||
- `cache/runtime_cache.rs` - кэш активных операций.
|
||||
|
||||
Правило:
|
||||
|
||||
`registry` не выполняет операции и не знает о `reqwest`/`tonic`. Он только хранит и отдает согласованные представления.
|
||||
|
||||
### 4.6. `mcpaas-runtime`
|
||||
|
||||
Назначение:
|
||||
|
||||
- исполнение операций;
|
||||
- orchestration между схемой, mapping и адаптерами;
|
||||
- выдача нормализованного результата.
|
||||
|
||||
Структура:
|
||||
|
||||
```text
|
||||
mcpaas-runtime/
|
||||
src/
|
||||
lib.rs
|
||||
executor/
|
||||
mod.rs
|
||||
operation_executor.rs
|
||||
input_prepare.rs
|
||||
output_finalize.rs
|
||||
adapter/
|
||||
mod.rs
|
||||
traits.rs
|
||||
dispatch.rs
|
||||
context/
|
||||
mod.rs
|
||||
execution_context.rs
|
||||
model/
|
||||
mod.rs
|
||||
runtime_operation.rs
|
||||
prepared_request.rs
|
||||
adapter_response.rs
|
||||
errors.rs
|
||||
```
|
||||
|
||||
Описание:
|
||||
|
||||
- `executor/operation_executor.rs` - основной orchestration use case.
|
||||
- `executor/input_prepare.rs` - валидация входа и применение input mapping.
|
||||
- `executor/output_finalize.rs` - обработка adapter response и output mapping.
|
||||
- `adapter/traits.rs` - общий контракт для протокольных адаптеров.
|
||||
- `adapter/dispatch.rs` - выбор адаптера по протоколу.
|
||||
- `context/execution_context.rs` - correlation id, deadlines, tracing data.
|
||||
- `model/runtime_operation.rs` - runtime-ready представление операции.
|
||||
|
||||
Правило:
|
||||
|
||||
`runtime` не должен знать, где хранится операция. Он получает уже готовую `runtime_operation`.
|
||||
|
||||
### 4.7. `mcpaas-adapter-rest`
|
||||
|
||||
Назначение:
|
||||
|
||||
- построение и выполнение REST-вызовов.
|
||||
|
||||
Структура:
|
||||
|
||||
```text
|
||||
mcpaas-adapter-rest/
|
||||
src/
|
||||
lib.rs
|
||||
client.rs
|
||||
request/
|
||||
mod.rs
|
||||
build.rs
|
||||
path.rs
|
||||
query.rs
|
||||
headers.rs
|
||||
body.rs
|
||||
response/
|
||||
mod.rs
|
||||
decode.rs
|
||||
normalize.rs
|
||||
errors.rs
|
||||
```
|
||||
|
||||
Правило:
|
||||
|
||||
REST adapter не валидирует MCP input и не знает о registry. Он получает уже подготовленный request contract.
|
||||
|
||||
### 4.8. `mcpaas-adapter-graphql`
|
||||
|
||||
Назначение:
|
||||
|
||||
- построение и выполнение GraphQL-вызовов.
|
||||
|
||||
Структура:
|
||||
|
||||
```text
|
||||
mcpaas-adapter-graphql/
|
||||
src/
|
||||
lib.rs
|
||||
client.rs
|
||||
request/
|
||||
mod.rs
|
||||
build.rs
|
||||
variables.rs
|
||||
response/
|
||||
mod.rs
|
||||
decode.rs
|
||||
extract.rs
|
||||
errors.rs
|
||||
```
|
||||
|
||||
Правило:
|
||||
|
||||
GraphQL adapter не занимается introspection по умолчанию и не содержит редактор схем. Он только исполняет подготовленный operation template.
|
||||
|
||||
Дополнительное ограничение:
|
||||
|
||||
один GraphQL tool соответствует одному заранее определенному `query` или `mutation`. Адаптер не должен принимать от LLM произвольный GraphQL-документ, потому что в MCP-модели операция должна оставаться узкой, предсказуемой и валидируемой по фиксированной схеме.
|
||||
|
||||
### 4.9. `mcpaas-adapter-grpc`
|
||||
|
||||
Назначение:
|
||||
|
||||
- выполнение unary gRPC-вызовов на основе уже выбранного метода и descriptor metadata.
|
||||
|
||||
Структура:
|
||||
|
||||
```text
|
||||
mcpaas-adapter-grpc/
|
||||
src/
|
||||
lib.rs
|
||||
channel.rs
|
||||
invoke/
|
||||
mod.rs
|
||||
unary.rs
|
||||
request/
|
||||
mod.rs
|
||||
build.rs
|
||||
response/
|
||||
mod.rs
|
||||
decode.rs
|
||||
errors.rs
|
||||
```
|
||||
|
||||
Правило:
|
||||
|
||||
gRPC adapter не должен сам парсить `.proto`. Этим занимается `mcpaas-proto`. Иначе в адаптере смешаются discovery и execution.
|
||||
|
||||
Дополнительное ограничение:
|
||||
|
||||
adapter поддерживает только unary RPC. Streaming-вызовы не реализуются, потому что целевая модель MCP tool в проекте соответствует сценарию `запрос -> ответ`, а не долгоживущей сессии обмена сообщениями.
|
||||
|
||||
### 4.10. `admin-api`
|
||||
|
||||
Назначение:
|
||||
|
||||
- HTTP API для UI;
|
||||
- координация use case создания, редактирования, тестирования и публикации.
|
||||
|
||||
Структура:
|
||||
|
||||
```text
|
||||
apps/admin-api/
|
||||
src/
|
||||
main.rs
|
||||
app.rs
|
||||
state.rs
|
||||
router.rs
|
||||
http/
|
||||
mod.rs
|
||||
dto/
|
||||
mod.rs
|
||||
operation.rs
|
||||
mapping.rs
|
||||
test_run.rs
|
||||
handlers/
|
||||
mod.rs
|
||||
create_operation.rs
|
||||
update_operation.rs
|
||||
publish_operation.rs
|
||||
list_operations.rs
|
||||
test_operation.rs
|
||||
export_operation_yaml.rs
|
||||
import_operation_yaml.rs
|
||||
upload_json_samples.rs
|
||||
upload_proto.rs
|
||||
list_grpc_services.rs
|
||||
response.rs
|
||||
services/
|
||||
mod.rs
|
||||
operation_service.rs
|
||||
descriptor_service.rs
|
||||
config_portability_service.rs
|
||||
test_service.rs
|
||||
errors.rs
|
||||
```
|
||||
|
||||
Правила:
|
||||
|
||||
- HTTP DTO не должны протекать в доменный слой;
|
||||
- handlers должны быть тонкими;
|
||||
- orchestration должна жить в `services/*`;
|
||||
- `state.rs` не должен разрастаться в огромную структуру. Лучше использовать вложенные state-компоненты или отдельные service bundles.
|
||||
- import/export конфигурации в `YAML` должен быть отдельным use case, а не побочным эффектом обычного CRUD.
|
||||
|
||||
### 4.11. `mcp-server`
|
||||
|
||||
Назначение:
|
||||
|
||||
- публикация MCP tools;
|
||||
- вызов runtime по имени tool;
|
||||
- обновление активного списка tools.
|
||||
|
||||
Структура:
|
||||
|
||||
```text
|
||||
apps/mcp-server/
|
||||
src/
|
||||
main.rs
|
||||
app.rs
|
||||
state.rs
|
||||
tools/
|
||||
mod.rs
|
||||
list.rs
|
||||
call.rs
|
||||
cache.rs
|
||||
translate/
|
||||
mod.rs
|
||||
to_mcp_tool.rs
|
||||
from_mcp_input.rs
|
||||
to_mcp_output.rs
|
||||
errors.rs
|
||||
```
|
||||
|
||||
Правило:
|
||||
|
||||
`mcp-server` не должен реализовывать business rules публикации. Он читает уже опубликованные операции и транслирует их в MCP.
|
||||
|
||||
### 4.12. `ui`
|
||||
|
||||
Назначение:
|
||||
|
||||
- интерфейс оператора.
|
||||
|
||||
Предлагаемая frontend-структура:
|
||||
|
||||
```text
|
||||
apps/ui/
|
||||
src/
|
||||
main.tsx
|
||||
app/
|
||||
router.tsx
|
||||
providers.tsx
|
||||
pages/
|
||||
operation-list/
|
||||
operation-create/
|
||||
operation-edit/
|
||||
operation-test/
|
||||
grpc-browser/
|
||||
features/
|
||||
operation-form/
|
||||
mapping-editor/
|
||||
sample-upload/
|
||||
grpc-method-picker/
|
||||
schema-viewer/
|
||||
publish-operation/
|
||||
entities/
|
||||
operation/
|
||||
descriptor/
|
||||
shared/
|
||||
api/
|
||||
lib/
|
||||
ui/
|
||||
config/
|
||||
```
|
||||
|
||||
Правило:
|
||||
|
||||
UI должен декомпозироваться по пользовательским сценариям, а не по типам файлов уровня "все компоненты в одной папке".
|
||||
|
||||
## 5. Правила зависимостей между crate
|
||||
|
||||
Целевой граф зависимостей:
|
||||
|
||||
```text
|
||||
mcpaas-core
|
||||
mcpaas-schema -> mcpaas-core
|
||||
mcpaas-mapping -> mcpaas-core
|
||||
mcpaas-proto -> mcpaas-core, mcpaas-schema
|
||||
mcpaas-registry -> mcpaas-core, mcpaas-schema, mcpaas-mapping
|
||||
mcpaas-adapter-rest -> mcpaas-core
|
||||
mcpaas-adapter-graphql -> mcpaas-core
|
||||
mcpaas-adapter-grpc -> mcpaas-core, mcpaas-proto
|
||||
mcpaas-runtime -> mcpaas-core, mcpaas-schema, mcpaas-mapping, adapters
|
||||
admin-api -> mcpaas-core, mcpaas-schema, mcpaas-mapping, mcpaas-proto, mcpaas-registry, mcpaas-runtime
|
||||
mcp-server -> mcpaas-core, mcpaas-registry, mcpaas-runtime
|
||||
```
|
||||
|
||||
Критические ограничения:
|
||||
|
||||
- `mcpaas-core` ни от кого не зависит;
|
||||
- адаптеры не зависят от `registry`;
|
||||
- `runtime` не зависит от `admin-api` и `mcp-server`;
|
||||
- `registry` не зависит от адаптеров;
|
||||
- `ui` зависит только от HTTP API.
|
||||
|
||||
## 6. Границы публичных API модулей
|
||||
|
||||
Чтобы структура не разъехалась, нужно заранее ограничить публичность.
|
||||
|
||||
Рекомендуемое правило:
|
||||
|
||||
- наружу экспортируются только корневые доменные типы, service-интерфейсы и ошибки;
|
||||
- внутренние DTO, record-модели и промежуточные builder-структуры остаются `pub(crate)`;
|
||||
- не реэкспортировать целые деревья модулей без необходимости;
|
||||
- не делать `mod utils`, если можно назвать модуль по смыслу.
|
||||
|
||||
Пример плохого решения:
|
||||
|
||||
- `pub mod common;`
|
||||
- `pub mod helpers;`
|
||||
- `pub struct AppContext { ... 25 полей ... }`
|
||||
|
||||
Пример правильного решения:
|
||||
|
||||
- `pub struct OperationExecutor`
|
||||
- `pub trait OperationRepository`
|
||||
- `pub struct RuntimeOperation`
|
||||
|
||||
## 7. Какие большие структуры точно не нужны
|
||||
|
||||
Ниже список сущностей, которые легко превращаются в антипаттерн:
|
||||
|
||||
- одна гигантская `Operation`, содержащая сразу все REST, GraphQL и gRPC поля;
|
||||
- один `MappingConfig`, содержащий и input, и output, и transforms, и validation rules без разделения;
|
||||
- единый `AppState` со всеми репозиториями, клиентами, кэшами и конфигами;
|
||||
- один `ProtocolAdapter` с ветвлением `match protocol` внутри на сотни строк;
|
||||
- один `SchemaField` без выделения object/array/enum/oneof вариантов.
|
||||
|
||||
Правильный подход:
|
||||
|
||||
- отдельные target-типы по протоколам;
|
||||
- отдельные input/output mapping модели;
|
||||
- отдельные bounded state-наборы для каждого приложения;
|
||||
- отдельные adapter crates;
|
||||
- выделенная иерархия schema types.
|
||||
|
||||
## 8. Порядок реализации без архитектурного долга
|
||||
|
||||
Рекомендуемый порядок разработки:
|
||||
|
||||
1. `mcpaas-core`
|
||||
2. `mcpaas-schema`
|
||||
3. `mcpaas-mapping`
|
||||
4. `mcpaas-registry`
|
||||
5. `mcpaas-adapter-rest`
|
||||
6. `mcpaas-runtime`
|
||||
7. `admin-api`
|
||||
8. `ui`
|
||||
9. `mcpaas-proto`
|
||||
10. `mcpaas-adapter-grpc`
|
||||
11. `mcpaas-adapter-graphql`
|
||||
12. `mcp-server`
|
||||
|
||||
Причина такого порядка:
|
||||
|
||||
- сначала фиксируется доменная модель;
|
||||
- затем схема и mapping как самые чувствительные части;
|
||||
- затем реестр и базовое выполнение REST;
|
||||
- после этого можно собирать UI и только потом наращивать сложные протоколы.
|
||||
|
||||
## 9. Практический итог
|
||||
|
||||
Если придерживаться этой декомпозиции, то:
|
||||
|
||||
- `mcpaas-core` останется маленьким и стабильным;
|
||||
- schema и mapping не смешаются с transport-логикой;
|
||||
- protobuf discovery не загрязнит gRPC runtime;
|
||||
- `admin-api` и `mcp-server` останутся тонкими входными слоями;
|
||||
- добавление нового протокола не потребует переписывать половину проекта.
|
||||
|
||||
Это и есть целевая архитектурная дисциплина проекта: отдельные слои, отдельные модели, минимально необходимая публичность и отсутствие "универсальных" структур, в которые со временем начинает стекаться вся система.
|
||||
@@ -0,0 +1,107 @@
|
||||
# GraphQL
|
||||
|
||||
## 1. Роль протокола в проекте
|
||||
|
||||
GraphQL поддерживается как отдельный тип интеграции, но на слое MCP намеренно ограничивается. Цель платформы не в том, чтобы дать LLM универсальный доступ ко всему GraphQL endpoint, а в том, чтобы превратить конкретный GraphQL-запрос в узкий и предсказуемый MCP tool.
|
||||
|
||||
## 2. Что поддерживается в MVP
|
||||
|
||||
- `query`
|
||||
- `mutation`
|
||||
- один GraphQL endpoint на operation
|
||||
- фиксированный `query_template`
|
||||
- фиксированный `selection set`
|
||||
- загрузка примера выходного `JSON`
|
||||
- схема переменных
|
||||
- variables mapping
|
||||
- response extraction из `data`
|
||||
- разбор `errors`
|
||||
- auth и headers
|
||||
- автогенерация чернового mapping
|
||||
- ручная донастройка через `JSONPath`
|
||||
- тестовый вызов перед публикацией
|
||||
|
||||
## 3. Что не входит в MVP
|
||||
|
||||
- `subscription`
|
||||
- универсальный GraphQL explorer для LLM
|
||||
- передача произвольного GraphQL-документа от LLM
|
||||
- визуальный конструктор сложных selection set
|
||||
- обязательная зависимость от introspection
|
||||
- автоматическое построение любого запроса по полной GraphQL schema
|
||||
|
||||
## 4. Ключевое архитектурное ограничение
|
||||
|
||||
Платформа не должна публиковать в MCP общий GraphQL tool, который умеет получать любые поля и принимать любые параметры в зависимости от намерения LLM.
|
||||
|
||||
Правильная модель только одна:
|
||||
|
||||
- один tool;
|
||||
- один конкретный `query` или `mutation`;
|
||||
- один заранее зафиксированный `selection set`;
|
||||
- фиксированный набор входных параметров;
|
||||
- один предсказуемый формат ответа.
|
||||
|
||||
Иными словами, на MCP-слое GraphQL сознательно сужается до модели, близкой к RPC или REST operation. Это делается потому, что LLM должен работать с понятным контрактом, а не конструировать произвольный GraphQL-запрос на лету.
|
||||
|
||||
## 5. Внутренняя модель GraphQL operation
|
||||
|
||||
GraphQL operation должна включать:
|
||||
|
||||
- `endpoint`
|
||||
- `operation_type`
|
||||
- `operation_name`
|
||||
- `query_template`
|
||||
- `variables_schema`
|
||||
- `input_mapping`
|
||||
- `response_path`
|
||||
- `error_policy`
|
||||
- `headers`
|
||||
- `auth_profile`
|
||||
|
||||
## 6. Как оператор настраивает GraphQL operation
|
||||
|
||||
1. Указывает GraphQL endpoint.
|
||||
2. Выбирает `query` или `mutation`.
|
||||
3. Задает имя операции.
|
||||
4. Вставляет готовый шаблон запроса.
|
||||
5. Описывает переменные, которые разрешено передавать в эту операцию.
|
||||
6. При необходимости загружает пример JSON-ответа.
|
||||
7. Система строит черновую схему ответа и стартовый mapping.
|
||||
8. Настраивает маппинг `MCP input -> GraphQL variables`.
|
||||
9. Указывает `response_path`, по которому извлекается полезный результат из `data`.
|
||||
10. При необходимости уточняет mapping через `JSONPath`.
|
||||
11. Выполняет тест.
|
||||
12. Публикует operation как MCP tool.
|
||||
|
||||
## 7. Поведение runtime
|
||||
|
||||
При выполнении GraphQL operation runtime должен:
|
||||
|
||||
1. Валидировать вход по фиксированной схеме переменных.
|
||||
2. Применить input mapping.
|
||||
3. Собрать GraphQL payload вида `query + variables`.
|
||||
4. Выполнить HTTP request.
|
||||
5. Отдельно разобрать `data` и `errors`.
|
||||
6. Применить output mapping или `response_path`.
|
||||
7. Вернуть нормализованный результат.
|
||||
|
||||
## 8. Критические нюансы
|
||||
|
||||
- HTTP `200 OK` не означает успешное выполнение, если в теле присутствует `errors`.
|
||||
- структура ответа зависит от `selection set`, значит она должна быть фиксирована заранее;
|
||||
- GraphQL endpoint обычно один, поэтому операция определяется не URL, а телом запроса;
|
||||
- variables должны быть строго ограничены, иначе один tool станет слишком широким и плохо управляемым;
|
||||
- `subscription` по смыслу не подходит модели MCP tool, потому что это потоковая, а не request-response интеграция.
|
||||
- `JSONPath` используется для точечного извлечения вложенных данных из `data` и для управления структурой итогового ответа.
|
||||
|
||||
## 9. Почему GraphQL не считается "почти REST"
|
||||
|
||||
GraphQL похож на REST только тем, что часто передается по HTTP. Но с точки зрения платформы это другой тип контракта:
|
||||
|
||||
- смысл операции задается не endpoint, а запросом;
|
||||
- ответ зависит от `selection set`;
|
||||
- ошибки живут в теле ответа, а не только в HTTP status;
|
||||
- одна и та же точка входа может обслуживать много операций.
|
||||
|
||||
Поэтому GraphQL в системе должен иметь отдельный адаптер и отдельную конфигурационную модель.
|
||||
@@ -0,0 +1,107 @@
|
||||
# gRPC
|
||||
|
||||
## 1. Роль протокола в проекте
|
||||
|
||||
gRPC поддерживается как третий основной протокол платформы, но в самой узкой и управляемой форме. Цель состоит не в том, чтобы покрыть все возможности gRPC, а в том, чтобы представить unary RPC-методы как обычные MCP tools с формой входа и формой выхода.
|
||||
|
||||
## 2. Что поддерживается в MVP
|
||||
|
||||
- только unary RPC
|
||||
- загрузка `.proto`
|
||||
- загрузка descriptor set
|
||||
- загрузка примеров JSON для MCP input/output при необходимости
|
||||
- извлечение `services`, `methods`, request/response messages
|
||||
- отображение входных и выходных параметров в UI
|
||||
- mapping `MCP input -> protobuf request`
|
||||
- mapping `protobuf response -> MCP output`
|
||||
- автогенерация чернового mapping
|
||||
- ручная донастройка через `JSONPath`
|
||||
- вызов метода по descriptor metadata
|
||||
- auth/transport settings на уровне соединения
|
||||
- тестовый вызов перед публикацией
|
||||
|
||||
## 3. Что не входит в MVP
|
||||
|
||||
- `server streaming`
|
||||
- `client streaming`
|
||||
- `bidirectional streaming`
|
||||
- обязательная поддержка server reflection
|
||||
- генерация нового Rust-кода под каждый загруженный `.proto`
|
||||
- сложные сценарии с долгоживущими сессиями вызовов
|
||||
|
||||
## 4. Ключевое архитектурное ограничение
|
||||
|
||||
В проекте поддерживаются только unary-методы, потому что MCP tool в этой архитектуре соответствует модели `один запрос -> один ответ`.
|
||||
|
||||
Это означает:
|
||||
|
||||
- один request message;
|
||||
- один response message;
|
||||
- один завершенный вызов;
|
||||
- отсутствие потоковых сообщений;
|
||||
- отсутствие отдельного жизненного цикла stream-сессии.
|
||||
|
||||
Streaming gRPC не нужен для выбранной модели взаимодействия с LLM и только усложнит runtime, UI и хранение состояния.
|
||||
|
||||
## 5. Внутренняя модель gRPC operation
|
||||
|
||||
gRPC operation должна включать:
|
||||
|
||||
- `server_addr`
|
||||
- `package`
|
||||
- `service`
|
||||
- `method`
|
||||
- `descriptor_ref`
|
||||
- `input_schema`
|
||||
- `output_schema`
|
||||
- `input_mapping`
|
||||
- `output_mapping`
|
||||
- `execution_config`
|
||||
- `tool_description`
|
||||
|
||||
## 6. Как оператор настраивает gRPC operation
|
||||
|
||||
1. Загружает `.proto` или descriptor set.
|
||||
2. Система извлекает список services и methods.
|
||||
3. Оператор выбирает конкретный unary-метод.
|
||||
4. UI показывает структуру request message и response message.
|
||||
5. При необходимости загружает примеры JSON для MCP input/output.
|
||||
6. Система строит черновую схему и стартовый mapping.
|
||||
7. Оператор задает или уточняет входные MCP-параметры.
|
||||
8. Настраивает маппинг во входные protobuf fields.
|
||||
9. Настраивает маппинг из response fields в MCP output.
|
||||
10. При необходимости уточняет mapping через `JSONPath`.
|
||||
11. Выполняет тест.
|
||||
12. Публикует operation как MCP tool.
|
||||
|
||||
## 7. Поведение runtime
|
||||
|
||||
При выполнении gRPC operation runtime должен:
|
||||
|
||||
1. Валидировать MCP input по нормализованной схеме.
|
||||
2. Применить input mapping.
|
||||
3. Построить protobuf request message из JSON.
|
||||
4. Выполнить unary RPC вызов.
|
||||
5. Преобразовать protobuf response в нормализованный JSON.
|
||||
6. Применить output mapping.
|
||||
7. Вернуть итоговый результат.
|
||||
|
||||
## 8. Критические нюансы
|
||||
|
||||
- `.proto` и descriptor handling должны быть отделены от runtime-вызова;
|
||||
- protobuf discovery не должен жить внутри gRPC adapter;
|
||||
- `oneof`, `enum`, `repeated`, `map` и well-known types требуют отдельной нормализации;
|
||||
- схема сообщения должна быть представлена в UI как обычная форма полей, а не как сырой protobuf descriptor;
|
||||
- пользователь не должен видеть внутреннюю сложность protobuf-контракта больше, чем это нужно для настройки operation.
|
||||
- `JSONPath` используется как единый способ точечной адресации вложенных полей при настройке mapping поверх нормализованной JSON-модели.
|
||||
|
||||
## 9. Почему gRPC ограничивается unary
|
||||
|
||||
Причина не только в сложности реализации. Главное ограничение архитектурное:
|
||||
|
||||
- MCP tool моделируется как завершенный вызов;
|
||||
- LLM работает с запросом и конечным ответом;
|
||||
- UI платформы построен вокруг формы входа и формы выхода;
|
||||
- streaming требует отдельной session-модели, buffering, cancellation и состояния.
|
||||
|
||||
Поэтому unary gRPC - это не "обрезанная" поддержка, а осознанно выбранная форма, которая действительно совместима с MCP-платформой.
|
||||
@@ -0,0 +1,112 @@
|
||||
# REST
|
||||
|
||||
## 1. Роль протокола в проекте
|
||||
|
||||
REST - базовый и первый по очередности реализации протокол платформы. На нем должна быть обкатана общая модель `Operation`, схема входа и выхода, маппинг, тестовый запуск и публикация MCP tool.
|
||||
|
||||
## 2. Что поддерживается в MVP
|
||||
|
||||
- HTTP methods: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`
|
||||
- загрузка примера входного `JSON`
|
||||
- загрузка примера выходного `JSON`
|
||||
- path parameters
|
||||
- query parameters
|
||||
- headers
|
||||
- JSON request body
|
||||
- JSON response body
|
||||
- auth: `Bearer`, `Basic`, API key
|
||||
- timeout и базовые transport settings
|
||||
- request mapping
|
||||
- response mapping
|
||||
- автогенерация чернового mapping
|
||||
- ручная донастройка через `JSONPath`
|
||||
- тестовый вызов перед публикацией
|
||||
|
||||
## 3. Что не входит в MVP
|
||||
|
||||
- multipart/form-data
|
||||
- file upload/download как отдельный сценарий
|
||||
- XML payload как основной формат
|
||||
- OpenAPI import с автоматическим созданием mappings
|
||||
- webhooks
|
||||
- long polling как специальный режим
|
||||
- `HEAD` и `OPTIONS` как отдельные пользовательские сценарии
|
||||
|
||||
## 4. Внутренняя модель REST operation
|
||||
|
||||
REST operation в системе описывается следующими основными частями:
|
||||
|
||||
- `base_url`
|
||||
- `method`
|
||||
- `path_template`
|
||||
- `headers`
|
||||
- `auth_profile`
|
||||
- `input_schema`
|
||||
- `input_mapping`
|
||||
- `output_schema`
|
||||
- `output_mapping`
|
||||
- `tool_description`
|
||||
|
||||
На слое MCP REST operation всегда выглядит как вызов `запрос -> ответ` с фиксированной схемой входа и выхода.
|
||||
|
||||
## 5. Как оператор настраивает REST operation
|
||||
|
||||
1. Указывает `base_url`.
|
||||
2. Выбирает HTTP method.
|
||||
3. Указывает `path_template`.
|
||||
4. При необходимости загружает пример входного и выходного `JSON`.
|
||||
5. Система строит черновую схему и стартовый mapping.
|
||||
6. Описывает или уточняет входные MCP-параметры.
|
||||
7. Сопоставляет параметры с `path`, `query`, `headers` и `body`.
|
||||
8. Указывает, откуда извлекать полезные данные в ответе.
|
||||
9. При необходимости уточняет mapping через `JSONPath`.
|
||||
10. Запускает тест.
|
||||
11. Публикует operation как MCP tool.
|
||||
|
||||
## 6. Требования к маппингу
|
||||
|
||||
Input mapping должен поддерживать:
|
||||
|
||||
- `$.mcp.* -> $.request.path.*`
|
||||
- `$.mcp.* -> $.request.query.*`
|
||||
- `$.mcp.* -> $.request.headers.*`
|
||||
- `$.mcp.* -> $.request.body.*`
|
||||
- константы
|
||||
- значения по умолчанию
|
||||
|
||||
Output mapping должен поддерживать:
|
||||
|
||||
- `$.response.body.* -> $.output.*`
|
||||
- извлечение вложенных полей
|
||||
- нормализацию отсутствующих значений
|
||||
|
||||
`JSONPath` является основным способом адресации конкретных параметров при работе со вложенными объектами и массивами.
|
||||
|
||||
## 7. Поведение runtime
|
||||
|
||||
При выполнении REST operation runtime должен:
|
||||
|
||||
1. Валидировать вход по нормализованной схеме.
|
||||
2. Применить input mapping.
|
||||
3. Собрать HTTP request.
|
||||
4. Выполнить вызов через `reqwest`.
|
||||
5. Преобразовать ответ в нормализованный JSON.
|
||||
6. Применить output mapping.
|
||||
7. Вернуть итоговый результат MCP server.
|
||||
|
||||
## 8. Нюансы и ограничения
|
||||
|
||||
- `DELETE` допускается, но body для него не считается обязательным сценарием совместимости.
|
||||
- `PATCH` требует аккуратной работы с частичными payload, поэтому mapping должен позволять заполнять только выбранные поля.
|
||||
- Успешный HTTP status сам по себе не гарантирует корректность бизнес-ответа, если response mapping не может извлечь ожидаемые данные.
|
||||
- REST adapter не должен содержать бизнес-логику маппинга, только transport-логику.
|
||||
- Загруженные JSON-примеры используются для генерации черновика, но не заменяют явную конфигурацию operation.
|
||||
|
||||
## 9. Почему REST остается отдельным протоколом
|
||||
|
||||
REST нельзя считать просто частным случаем другого HTTP-based интерфейса, потому что:
|
||||
|
||||
- контракт определяется URL, методом и payload;
|
||||
- semantics HTTP methods важны;
|
||||
- поведение интеграции часто завязано на headers и auth;
|
||||
- UX настройки REST operation отличается от GraphQL и gRPC.
|
||||
@@ -0,0 +1,127 @@
|
||||
# Runtime Config
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ фиксирует конфигурацию окружения, storage и базовые operational assumptions для MVP.
|
||||
|
||||
Его задача - убрать неявные решения, которые обычно всплывают уже в процессе написания кода.
|
||||
|
||||
## 2. Базовые решения для MVP
|
||||
|
||||
- каноническая БД: `PostgreSQL`
|
||||
- допустимый упрощенный режим разработки: `SQLite`
|
||||
- artifact storage: локальная файловая система
|
||||
- MCP transport: `Streamable HTTP`
|
||||
- admin API и mcp-server запускаются как отдельные приложения
|
||||
|
||||
## 3. Artifact storage
|
||||
|
||||
В MVP sample JSON, `.proto`, `descriptor set` и YAML import payload должны храниться в локальном файловом storage.
|
||||
|
||||
Требования:
|
||||
|
||||
- все файлы кладутся в контролируемый базовый каталог;
|
||||
- в БД хранится только `storage_ref`;
|
||||
- структура каталогов должна быть детерминированной;
|
||||
- storage слой должен быть абстрагирован, чтобы потом заменить его на S3-compatible backend.
|
||||
|
||||
Рекомендуемая структура:
|
||||
|
||||
```text
|
||||
var/mcpaas/
|
||||
samples/
|
||||
descriptors/
|
||||
yaml-imports/
|
||||
```
|
||||
|
||||
## 4. Секреты и auth profiles
|
||||
|
||||
Для MVP:
|
||||
|
||||
- operation хранит только `auth_profile_ref`;
|
||||
- auth profile хранит только `secret_ref`;
|
||||
- реальные секреты не должны попадать в YAML export;
|
||||
- секреты не должны логироваться.
|
||||
|
||||
Допустимые варианты secret storage:
|
||||
|
||||
- env-backed secret store;
|
||||
- encrypted local secret storage.
|
||||
|
||||
Минимальный безопасный вариант для MVP:
|
||||
|
||||
- `secret_ref` указывает на env variable alias или key в локальном secret store;
|
||||
- приложение резолвит его на runtime.
|
||||
|
||||
## 5. Переменные окружения
|
||||
|
||||
Минимально ожидаются:
|
||||
|
||||
- `MCPAAS_DATABASE_URL`
|
||||
- `MCPAAS_STORAGE_ROOT`
|
||||
- `MCPAAS_ADMIN_BIND`
|
||||
- `MCPAAS_MCP_BIND`
|
||||
- `MCPAAS_LOG_LEVEL`
|
||||
- `MCPAAS_SECRET_PROVIDER`
|
||||
|
||||
Опционально:
|
||||
|
||||
- `MCPAAS_ADMIN_TOKEN`
|
||||
- `MCPAAS_MASTER_KEY`
|
||||
|
||||
## 6. Логирование и трассировка
|
||||
|
||||
Для MVP нужно использовать:
|
||||
|
||||
- structured logging через `tracing`;
|
||||
- correlation id для test runs и runtime execution;
|
||||
- раздельные стадии ошибок: schema, mapping, adapter, external service.
|
||||
|
||||
## 7. Таймауты и retries
|
||||
|
||||
Рекомендуемые стартовые значения:
|
||||
|
||||
- default timeout: `10s`
|
||||
- retry default: `1` attempt, то есть без автоматического повтора
|
||||
|
||||
Причина:
|
||||
|
||||
- сначала важнее детерминированность и прозрачность;
|
||||
- aggressive retries могут маскировать реальные ошибки интеграции.
|
||||
|
||||
## 8. Режимы запуска
|
||||
|
||||
Минимально нужны два режима:
|
||||
|
||||
- local development
|
||||
- demo/deployment
|
||||
|
||||
Local development:
|
||||
|
||||
- `SQLite` допустим;
|
||||
- локальный storage;
|
||||
- упрощенная auth-модель admin-api.
|
||||
|
||||
Demo/deployment:
|
||||
|
||||
- `PostgreSQL`;
|
||||
- локальный или сетевой storage;
|
||||
- включенная auth-защита admin-api;
|
||||
- стабильный `Streamable HTTP` endpoint для MCP.
|
||||
|
||||
## 9. Что важно не допустить
|
||||
|
||||
- пути к storage, зашитые в код;
|
||||
- секреты в `.yaml` exports;
|
||||
- разные конфигурационные модели для local и production без причины;
|
||||
- смешивание runtime config и business config operation.
|
||||
|
||||
## 10. Практический итог
|
||||
|
||||
До старта разработки должны быть приняты как минимум такие решения:
|
||||
|
||||
- где лежит БД;
|
||||
- где лежат artifacts;
|
||||
- как резолвятся `secret_ref`;
|
||||
- на каких bind-address запускаются `admin-api` и `mcp-server`;
|
||||
- какой transport использует MCP server.
|
||||
@@ -0,0 +1,316 @@
|
||||
# Rust Code Rules
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ фиксирует Rust-specific правила кода для проекта:
|
||||
|
||||
- toolchain;
|
||||
- linting;
|
||||
- formatting;
|
||||
- ошибки;
|
||||
- async;
|
||||
- ownership;
|
||||
- visibility;
|
||||
- dependency hygiene.
|
||||
|
||||
Цель документа - убрать плавающие договоренности по стилю и практике командной Rust-разработки.
|
||||
|
||||
## 2. Toolchain
|
||||
|
||||
### 2.1. Версия Rust
|
||||
|
||||
Для проекта должен быть зафиксирован `rust-toolchain.toml`.
|
||||
|
||||
В нем должны быть определены:
|
||||
|
||||
- стабильный `channel`;
|
||||
- `edition`;
|
||||
- при необходимости `components`.
|
||||
|
||||
Рекомендуемый состав:
|
||||
|
||||
- `rustfmt`
|
||||
- `clippy`
|
||||
|
||||
### 2.2. MSRV
|
||||
|
||||
Нужно зафиксировать `MSRV` - минимально поддерживаемую версию Rust.
|
||||
|
||||
Правило:
|
||||
|
||||
- без необходимости не использовать возможности языка новее зафиксированного `MSRV`;
|
||||
- обновление `MSRV` - это отдельное осознанное решение.
|
||||
|
||||
## 3. Formatting и linting
|
||||
|
||||
### 3.1. Formatting
|
||||
|
||||
Обязательное правило:
|
||||
|
||||
- весь код форматируется через `cargo fmt`.
|
||||
|
||||
Ручной стиль форматирования не обсуждается и не поддерживается.
|
||||
|
||||
### 3.2. Clippy
|
||||
|
||||
Обязательное правило:
|
||||
|
||||
- `cargo clippy --all-targets --all-features -- -D warnings`
|
||||
|
||||
Предупреждения считаются ошибками, если нет явно зафиксированного исключения.
|
||||
|
||||
### 3.3. CI quality gates
|
||||
|
||||
Минимально в CI должны запускаться:
|
||||
|
||||
- `cargo fmt --check`
|
||||
- `cargo clippy --all-targets --all-features -- -D warnings`
|
||||
- `cargo test`
|
||||
|
||||
Опционально позже:
|
||||
|
||||
- `cargo deny`
|
||||
- `cargo audit`
|
||||
|
||||
## 4. `unsafe`
|
||||
|
||||
Для проекта принимается правило:
|
||||
|
||||
- `unsafe` запрещен по умолчанию.
|
||||
|
||||
Если когда-либо потребуется `unsafe`, то:
|
||||
|
||||
- это должно быть отдельное осознанное решение;
|
||||
- причина должна быть технически обоснована;
|
||||
- блок должен быть минимальным;
|
||||
- вокруг него должны быть тесты.
|
||||
|
||||
Для MVP можно считать:
|
||||
|
||||
- `unsafe_code = deny`
|
||||
|
||||
## 5. Panic policy
|
||||
|
||||
В production code запрещены:
|
||||
|
||||
- `unwrap()`
|
||||
- `expect()`
|
||||
- `todo!()`
|
||||
- `unimplemented!()`
|
||||
- `dbg!()`
|
||||
- необоснованные `panic!()`
|
||||
|
||||
Допускается:
|
||||
|
||||
- в тестах;
|
||||
- в очень раннем bootstrap-коде, если это действительно аварийное завершение и не часть доменной логики.
|
||||
|
||||
Базовое правило:
|
||||
|
||||
- ошибки возвращаются через `Result`, а не через panic.
|
||||
|
||||
## 6. Правила ошибок
|
||||
|
||||
### 6.1. Domain и service errors
|
||||
|
||||
В домене и сервисах использовать типизированные ошибки.
|
||||
|
||||
Рекомендуемо:
|
||||
|
||||
- `thiserror`
|
||||
|
||||
### 6.2. Application boundary
|
||||
|
||||
На верхних слоях приложений допускается агрегирование ошибок, если это упрощает wiring.
|
||||
|
||||
При необходимости:
|
||||
|
||||
- `anyhow` только на внешних границах приложения, не в доменной модели.
|
||||
|
||||
### 6.3. Error context
|
||||
|
||||
Ошибка должна сохранять стадию отказа:
|
||||
|
||||
- schema
|
||||
- mapping
|
||||
- adapter
|
||||
- persistence
|
||||
- external service
|
||||
- internal runtime
|
||||
|
||||
## 7. Visibility rules
|
||||
|
||||
Правило:
|
||||
|
||||
- по умолчанию все приватное;
|
||||
- `pub(crate)` предпочтительнее `pub`;
|
||||
- публичный API должен быть минимальным.
|
||||
|
||||
Нельзя:
|
||||
|
||||
- открывать модуль наружу "на всякий случай";
|
||||
- делать `pub` просто ради удобства из соседнего файла;
|
||||
- реэкспортировать целые деревья модулей без причины.
|
||||
|
||||
## 8. Ownership и данные
|
||||
|
||||
### 8.1. Клонирование
|
||||
|
||||
Правило:
|
||||
|
||||
- не клонировать данные без необходимости;
|
||||
- клон должен быть осознанным, а не способом обойти borrow checker без понимания причины.
|
||||
|
||||
### 8.2. Shared mutability
|
||||
|
||||
Правило:
|
||||
|
||||
- не использовать `Arc<Mutex<_>>` как универсальный контейнер состояния;
|
||||
- shared mutability допускается только там, где она действительно нужна по архитектуре.
|
||||
|
||||
### 8.3. ID types
|
||||
|
||||
Идентификаторы должны быть отдельными типами, а не просто `String`.
|
||||
|
||||
Примеры:
|
||||
|
||||
- `OperationId`
|
||||
- `DescriptorId`
|
||||
- `AuthProfileId`
|
||||
|
||||
## 9. Async rules
|
||||
|
||||
### 9.1. Где допускается `async`
|
||||
|
||||
`async` используется только там, где есть:
|
||||
|
||||
- I/O;
|
||||
- network;
|
||||
- storage;
|
||||
- async boundary приложения.
|
||||
|
||||
### 9.2. Где `async` не нужен
|
||||
|
||||
Нельзя превращать:
|
||||
|
||||
- schema validation;
|
||||
- mapping;
|
||||
- чистую доменную логику;
|
||||
- небольшие derived methods
|
||||
|
||||
в `async fn` без причины.
|
||||
|
||||
### 9.3. `tokio`
|
||||
|
||||
`tokio` должен находиться:
|
||||
|
||||
- в приложениях;
|
||||
- в I/O слоях;
|
||||
- в адаптерах и runtime orchestration, если там есть реальный async.
|
||||
|
||||
Доменный слой не должен зависеть от `tokio`.
|
||||
|
||||
## 10. API design rules
|
||||
|
||||
### 10.1. Конструкторы
|
||||
|
||||
Использовать:
|
||||
|
||||
- `new()` для гарантированно валидного и простого создания;
|
||||
- `try_new()` там, где есть валидация и возможна ошибка.
|
||||
|
||||
### 10.2. Builders
|
||||
|
||||
Если структура имеет много параметров и прямой конструктор становится нечитаемым, допускается builder.
|
||||
|
||||
Но:
|
||||
|
||||
- builder не должен маскировать плохую модель данных;
|
||||
- builder не должен использоваться как замена нормальной декомпозиции.
|
||||
|
||||
### 10.3. DTO отдельно от domain
|
||||
|
||||
Если HTTP payload начинает расходиться с доменной моделью, нужно вводить отдельный DTO слой.
|
||||
|
||||
Нельзя:
|
||||
|
||||
- тащить `serde`-ориентированный API payload прямо в домен только ради удобства.
|
||||
|
||||
## 11. Dependency rules
|
||||
|
||||
### 11.1. Внешние crates
|
||||
|
||||
Правило:
|
||||
|
||||
- сначала использовать `std`;
|
||||
- потом существующие внутренние abstractions;
|
||||
- только потом тянуть новый внешний crate.
|
||||
|
||||
Нельзя:
|
||||
|
||||
- добавлять зависимость "на всякий случай";
|
||||
- дублировать crates с пересекающейся функцией без причины.
|
||||
|
||||
### 11.2. Макросы
|
||||
|
||||
Правило:
|
||||
|
||||
- не злоупотреблять макросами там, где обычный Rust-код читается лучше;
|
||||
- derive-макросы допустимы;
|
||||
- сложные процедурные макросы без сильной причины не нужны.
|
||||
|
||||
## 12. Serialization rules
|
||||
|
||||
### 12.1. JSON/YAML
|
||||
|
||||
Правило:
|
||||
|
||||
- доменная модель одна;
|
||||
- `JSON` и `YAML` - только два формата сериализации;
|
||||
- нельзя допускать, чтобы YAML export стал отдельной несовместимой моделью.
|
||||
|
||||
### 12.2. Secrets
|
||||
|
||||
Никогда не сериализовать:
|
||||
|
||||
- реальные токены;
|
||||
- пароли;
|
||||
- API keys
|
||||
|
||||
в exports, logs и test snapshots.
|
||||
|
||||
## 13. Тестовые практики на уровне Rust-кода
|
||||
|
||||
Минимально:
|
||||
|
||||
- unit tests рядом с модулем или в `tests`;
|
||||
- integration tests для crate boundaries;
|
||||
- фикстуры для schema/mapping/proto/yaml roundtrip.
|
||||
|
||||
Полезное правило:
|
||||
|
||||
- баг сначала воспроизводится тестом, потом фиксится кодом.
|
||||
|
||||
## 14. Что часто запрещают в Rust-командах
|
||||
|
||||
Практически всегда под запретом:
|
||||
|
||||
- `unwrap()` в production code;
|
||||
- `unsafe` без review;
|
||||
- giant modules;
|
||||
- giant enums со всем подряд;
|
||||
- giant services, где смешаны orchestration и transport;
|
||||
- абстракции "на будущее" без второго реального кейса.
|
||||
|
||||
## 15. Практический итог
|
||||
|
||||
Для этого проекта правильный Rust-профиль такой:
|
||||
|
||||
- фиксированный toolchain;
|
||||
- обязательные `fmt` и `clippy`;
|
||||
- `unsafe` запрещен по умолчанию;
|
||||
- panics запрещены в production code;
|
||||
- ошибки типизированы;
|
||||
- `pub` минимизируется;
|
||||
- `async` только на реальных async boundaries;
|
||||
- код читается за счет имен и декомпозиции, а не за счет комментариев.
|
||||
@@ -0,0 +1,402 @@
|
||||
# Rust Design
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ фиксирует, как проектировать поведение в Rust-коде:
|
||||
|
||||
- какие методы допустимы на `struct` и `enum`;
|
||||
- что должно жить в `impl`;
|
||||
- что должно быть вынесено в `trait`;
|
||||
- что должно быть оформлено как `service` или `use case`.
|
||||
|
||||
Главная цель документа - не допустить появления `god-struct`, когда одна сущность одновременно:
|
||||
|
||||
- хранит данные;
|
||||
- валидирует себя целиком;
|
||||
- ходит в БД;
|
||||
- дергает HTTP;
|
||||
- строит mapping;
|
||||
- управляет publish flow;
|
||||
- содержит половину бизнес-логики проекта.
|
||||
|
||||
## 2. Базовое правило
|
||||
|
||||
В Rust нужно разделять:
|
||||
|
||||
- `data model`
|
||||
- `domain behavior`
|
||||
- `integration contracts`
|
||||
- `application services`
|
||||
|
||||
То есть:
|
||||
|
||||
- `struct` и `enum` хранят состояние;
|
||||
- `impl` на них содержит только локально связанное поведение;
|
||||
- `trait` задает внешний контракт;
|
||||
- `service` и `use case` координируют несколько сущностей и внешние зависимости.
|
||||
|
||||
## 3. Что допустимо держать в `impl` на структурах
|
||||
|
||||
На `impl` допустимы только методы, которые:
|
||||
|
||||
- опираются только на внутреннее состояние структуры;
|
||||
- не требуют инфраструктурных зависимостей;
|
||||
- не ходят в БД;
|
||||
- не выполняют сетевые вызовы;
|
||||
- не меняют чужие aggregate boundaries.
|
||||
|
||||
Подходящие примеры:
|
||||
|
||||
- `Operation::is_published()`
|
||||
- `Operation::supports_protocol(protocol)`
|
||||
- `Operation::tool_name()`
|
||||
- `MappingRule::is_required()`
|
||||
- `GeneratedDraft::is_available()`
|
||||
- `Schema::field(path)`
|
||||
- `Protocol::as_str()`
|
||||
|
||||
Неподходящие примеры:
|
||||
|
||||
- `Operation::save(db)`
|
||||
- `Operation::publish(repo, runtime, cache)`
|
||||
- `Operation::call_external_api()`
|
||||
- `Operation::load_descriptor()`
|
||||
|
||||
## 4. Какие методы должны жить на ключевых доменных структурах
|
||||
|
||||
### 4.1. `Operation`
|
||||
|
||||
Допустимые методы:
|
||||
|
||||
- `fn tool_name(&self) -> &str`
|
||||
- `fn is_draft(&self) -> bool`
|
||||
- `fn is_published(&self) -> bool`
|
||||
- `fn protocol(&self) -> &Protocol`
|
||||
- `fn auth_profile_ref(&self) -> Option<&str>`
|
||||
- `fn can_be_published(&self) -> bool`
|
||||
|
||||
Что не должно жить здесь:
|
||||
|
||||
- создание новой версии;
|
||||
- publish;
|
||||
- YAML import/export;
|
||||
- DB persistence;
|
||||
- runtime execution;
|
||||
- adapter dispatch.
|
||||
|
||||
### 4.2. `Target`
|
||||
|
||||
Допустимые методы:
|
||||
|
||||
- `fn kind(&self) -> Protocol`
|
||||
- `fn summary(&self) -> String`
|
||||
|
||||
Что не должно жить здесь:
|
||||
|
||||
- реальный вызов REST/GraphQL/gRPC;
|
||||
- загрузка descriptor set;
|
||||
- introspection;
|
||||
- network logic.
|
||||
|
||||
### 4.3. `Schema`
|
||||
|
||||
Допустимые методы:
|
||||
|
||||
- `fn is_object(&self) -> bool`
|
||||
- `fn field(&self, name: &str) -> Option<&SchemaField>`
|
||||
- `fn has_required_fields(&self) -> bool`
|
||||
- `fn validate_shape(&self, value: &serde_json::Value) -> Result<(), SchemaError>`
|
||||
|
||||
Что не должно жить здесь:
|
||||
|
||||
- UI rendering logic;
|
||||
- DB serialization logic;
|
||||
- adapter-specific request assembly.
|
||||
|
||||
### 4.4. `MappingSet` и `MappingRule`
|
||||
|
||||
Допустимые методы:
|
||||
|
||||
- `fn is_empty(&self) -> bool`
|
||||
- `fn validate_paths(&self) -> Result<(), MappingError>`
|
||||
- `fn target_context(&self) -> MappingTargetContext`
|
||||
|
||||
Что не должно жить здесь:
|
||||
|
||||
- protocol adapter branching;
|
||||
- network execution;
|
||||
- persistence;
|
||||
- доступ к registry.
|
||||
|
||||
### 4.5. `ExecutionConfig`
|
||||
|
||||
Допустимые методы:
|
||||
|
||||
- `fn timeout(&self) -> Duration`
|
||||
- `fn has_auth(&self) -> bool`
|
||||
- `fn protocol_options(&self) -> Option<&ProtocolOptions>`
|
||||
|
||||
Что не должно жить здесь:
|
||||
|
||||
- secret resolution;
|
||||
- создание HTTP headers из env;
|
||||
- динамическое чтение конфигов приложения.
|
||||
|
||||
## 5. Что нужно выносить в `trait`
|
||||
|
||||
`Trait` нужен там, где появляется внешний контракт, который имеет несколько реализаций или зависит от инфраструктуры.
|
||||
|
||||
Правильные кандидаты:
|
||||
|
||||
- `OperationRepository`
|
||||
- `PublishedOperationRepository`
|
||||
- `ProtocolAdapter`
|
||||
- `SecretResolver`
|
||||
- `DescriptorStore`
|
||||
- `ArtifactStore`
|
||||
- `YamlCodec`
|
||||
- `DraftGenerator`
|
||||
|
||||
### Пример
|
||||
|
||||
```rust
|
||||
pub trait OperationRepository {
|
||||
async fn get(&self, id: &OperationId) -> Result<OperationRecord, RepoError>;
|
||||
async fn create_version(&self, cmd: CreateVersion) -> Result<OperationVersionRef, RepoError>;
|
||||
async fn publish(&self, id: &OperationId, version: u32) -> Result<(), RepoError>;
|
||||
}
|
||||
```
|
||||
|
||||
Почему это `trait`, а не метод на `Operation`:
|
||||
|
||||
- потому что операция сама не должна знать, как она хранится;
|
||||
- потому что хранение - инфраструктурная зависимость;
|
||||
- потому что это boundary между доменом и storage.
|
||||
|
||||
## 6. Что нужно выносить в service/use-case слой
|
||||
|
||||
Если логика:
|
||||
|
||||
- координирует несколько сущностей;
|
||||
- использует `trait`-зависимости;
|
||||
- меняет состояние нескольких aggregate boundaries;
|
||||
- имеет бизнес-шаги;
|
||||
|
||||
то это `service`, а не `impl` на структуре.
|
||||
|
||||
Кандидаты:
|
||||
|
||||
- `CreateOperationService`
|
||||
- `CreateOperationVersionService`
|
||||
- `PublishOperationService`
|
||||
- `GenerateDraftService`
|
||||
- `TestOperationService`
|
||||
- `ImportOperationYamlService`
|
||||
- `ExportOperationYamlService`
|
||||
- `ListPublishedToolsService`
|
||||
- `OperationExecutor`
|
||||
|
||||
## 7. Рекомендуемое распределение поведения
|
||||
|
||||
### Domain `impl`
|
||||
|
||||
Хранит:
|
||||
|
||||
- локальную валидацию;
|
||||
- derived methods;
|
||||
- простые status checks;
|
||||
- инварианты одной сущности.
|
||||
|
||||
### `trait`
|
||||
|
||||
Хранит:
|
||||
|
||||
- внешние контракты;
|
||||
- infrastructure boundaries;
|
||||
- replaceable dependencies.
|
||||
|
||||
### `service`
|
||||
|
||||
Хранит:
|
||||
|
||||
- orchestration;
|
||||
- use case sequence;
|
||||
- transaction boundaries;
|
||||
- вызовы нескольких зависимостей.
|
||||
|
||||
## 8. Пример правильного разделения
|
||||
|
||||
### Плохо
|
||||
|
||||
```rust
|
||||
impl Operation {
|
||||
pub async fn publish(
|
||||
&mut self,
|
||||
repo: &SqlOperationRepository,
|
||||
cache: &RuntimeCache,
|
||||
secret_resolver: &EnvSecretResolver,
|
||||
) -> Result<(), Error> {
|
||||
self.validate()?;
|
||||
repo.save(self).await?;
|
||||
cache.reload().await?;
|
||||
let _ = secret_resolver.resolve(...)?;
|
||||
self.status = Status::Published;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Почему плохо:
|
||||
|
||||
- доменная сущность знает про SQL;
|
||||
- знает про кэш;
|
||||
- знает про secret resolver;
|
||||
- меняет себя и внешний мир одновременно;
|
||||
- содержит orchestration.
|
||||
|
||||
### Правильно
|
||||
|
||||
```rust
|
||||
impl Operation {
|
||||
pub fn can_be_published(&self) -> bool {
|
||||
matches!(self.status, Status::Draft | Status::Testing)
|
||||
&& !self.input_mapping.rules.is_empty()
|
||||
&& !self.output_mapping.rules.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PublishOperationService<R> {
|
||||
repo: R,
|
||||
}
|
||||
|
||||
impl<R> PublishOperationService<R>
|
||||
where
|
||||
R: OperationRepository,
|
||||
{
|
||||
pub async fn execute(
|
||||
&self,
|
||||
operation_id: &OperationId,
|
||||
version: u32,
|
||||
) -> Result<(), PublishError> {
|
||||
let op = self.repo.get_version(operation_id, version).await?;
|
||||
if !op.can_be_published() {
|
||||
return Err(PublishError::InvalidState);
|
||||
}
|
||||
self.repo.publish(operation_id, version).await
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Признаки `god-struct`
|
||||
|
||||
Если у структуры:
|
||||
|
||||
- слишком много полей из разных bounded contexts;
|
||||
- методы и на schema, и на DB, и на adapters, и на YAML;
|
||||
- методы с кучей зависимостей в аргументах;
|
||||
- методы длиннее, чем небольшой локальный инвариант;
|
||||
- много `match protocol` прямо внутри доменной модели;
|
||||
|
||||
то это уже `god-struct`.
|
||||
|
||||
Особенно опасные кандидаты:
|
||||
|
||||
- `Operation`
|
||||
- `AppState`
|
||||
- `OperationExecutor`
|
||||
- `AdminService`
|
||||
- `ProtocolAdapter`
|
||||
|
||||
## 10. Как не допустить `god-struct`
|
||||
|
||||
### 10.1. Для `Operation`
|
||||
|
||||
Не добавлять туда:
|
||||
|
||||
- repo methods;
|
||||
- transport methods;
|
||||
- import/export;
|
||||
- publish flow;
|
||||
- sample upload handling.
|
||||
|
||||
### 10.2. Для `AppState`
|
||||
|
||||
Не складывать все зависимости в один плоский объект на 20 полей.
|
||||
|
||||
Лучше:
|
||||
|
||||
- `RegistryServices`
|
||||
- `RuntimeServices`
|
||||
- `ArtifactServices`
|
||||
- `AuthServices`
|
||||
|
||||
### 10.3. Для `OperationExecutor`
|
||||
|
||||
Он может быть orchestration root, но не должен становиться монолитом.
|
||||
|
||||
Нужно выделять:
|
||||
|
||||
- `InputPrepare`
|
||||
- `AdapterDispatch`
|
||||
- `OutputFinalize`
|
||||
- `ExecutionContextFactory`
|
||||
|
||||
## 11. Рекомендуемые `impl`-блоки по проекту
|
||||
|
||||
### В `mcpaas-core`
|
||||
|
||||
- маленькие `impl` на domain types;
|
||||
- status helpers;
|
||||
- derived metadata methods.
|
||||
|
||||
### В `mcpaas-schema`
|
||||
|
||||
- schema validation;
|
||||
- field traversal;
|
||||
- shape helpers.
|
||||
|
||||
### В `mcpaas-mapping`
|
||||
|
||||
- JSONPath validation;
|
||||
- mapping rule helpers;
|
||||
- execution helpers.
|
||||
|
||||
### В `mcpaas-proto`
|
||||
|
||||
- metadata conversion helpers;
|
||||
- descriptor lookup helpers.
|
||||
|
||||
### В `mcpaas-registry`
|
||||
|
||||
- service methods, а не методы на доменных структурах;
|
||||
- repository implementations.
|
||||
|
||||
### В `mcpaas-runtime`
|
||||
|
||||
- orchestration services;
|
||||
- adapter dispatch;
|
||||
- runtime context management.
|
||||
|
||||
## 12. Что лучше описывать не как методы структур
|
||||
|
||||
Следующие вещи лучше описывать отдельными сервисами даже если технически их можно записать как `impl`:
|
||||
|
||||
- `publish`
|
||||
- `create_version`
|
||||
- `import_yaml`
|
||||
- `export_yaml`
|
||||
- `generate_draft`
|
||||
- `test_run`
|
||||
- `reload_published_tools`
|
||||
|
||||
## 13. Практический итог
|
||||
|
||||
Для этого проекта хорошее правило такое:
|
||||
|
||||
- `struct` знает только себя;
|
||||
- `trait` знает границу;
|
||||
- `service` знает сценарий;
|
||||
- `adapter` знает протокол;
|
||||
- `repository` знает storage.
|
||||
|
||||
Если придерживаться этой схемы, то Rust-код останется модульным, а `Operation` и связанные типы не превратятся в `god-struct` с разнородной логикой.
|
||||
@@ -0,0 +1,131 @@
|
||||
# Стратегия тестирования
|
||||
|
||||
## 1. Назначение документа
|
||||
|
||||
Этот документ фиксирует, как проект должен тестироваться с самого начала разработки, чтобы архитектура не осталась "только на бумаге".
|
||||
|
||||
Цель:
|
||||
|
||||
- проверять доменную модель отдельно от транспорта;
|
||||
- ловить регрессии в mapping;
|
||||
- не дать адаптерам начать вести себя по-разному;
|
||||
- обеспечить воспроизводимость для дипломной демонстрации.
|
||||
|
||||
## 2. Уровни тестов
|
||||
|
||||
### 2.1. Unit tests
|
||||
|
||||
Покрывают:
|
||||
|
||||
- `mcpaas-schema`
|
||||
- `mcpaas-mapping`
|
||||
- `mcpaas-proto`
|
||||
- небольшие части `mcpaas-core`
|
||||
|
||||
Что проверять:
|
||||
|
||||
- валидацию схем;
|
||||
- `JSONPath` parsing;
|
||||
- применение input/output mapping;
|
||||
- генерацию чернового mapping;
|
||||
- protobuf -> schema normalization;
|
||||
- JSON -> protobuf и protobuf -> JSON conversion.
|
||||
|
||||
### 2.2. Integration tests
|
||||
|
||||
Покрывают:
|
||||
|
||||
- `mcpaas-registry` с реальной БД;
|
||||
- `mcpaas-runtime` с реальными adapter contracts;
|
||||
- `admin-api` на поднятом приложении;
|
||||
- publish flow и YAML import/export.
|
||||
|
||||
Что проверять:
|
||||
|
||||
- создание operation и новой version;
|
||||
- publish и reload published tools;
|
||||
- тестовый вызов draft;
|
||||
- экспорт в YAML и повторный импорт;
|
||||
- связность БД между `operations`, `operation_versions`, `published_operations`.
|
||||
|
||||
### 2.3. Adapter tests
|
||||
|
||||
Отдельно для каждого протокола:
|
||||
|
||||
- REST adapter;
|
||||
- GraphQL adapter;
|
||||
- gRPC unary adapter.
|
||||
|
||||
Что проверять:
|
||||
|
||||
- сборку request;
|
||||
- нормализацию response;
|
||||
- обработку ошибок;
|
||||
- стабильность mapping context.
|
||||
|
||||
### 2.4. End-to-end tests
|
||||
|
||||
Минимально нужны сценарии:
|
||||
|
||||
- создать REST operation -> протестировать -> опубликовать -> вызвать как MCP tool;
|
||||
- создать GraphQL operation -> протестировать -> опубликовать -> вызвать как MCP tool;
|
||||
- загрузить `.proto` или descriptor set -> создать gRPC unary operation -> протестировать -> опубликовать -> вызвать как MCP tool.
|
||||
|
||||
## 3. Что должно быть покрыто обязательно
|
||||
|
||||
### Обязательно с первого этапа
|
||||
|
||||
- schema validation;
|
||||
- mapping execution;
|
||||
- YAML import/export roundtrip;
|
||||
- versioning logic registry;
|
||||
- publish flow.
|
||||
|
||||
### Обязательно до первого демо
|
||||
|
||||
- хотя бы один end-to-end сценарий для каждого из трех протоколов;
|
||||
- negative tests на invalid `JSONPath`;
|
||||
- negative tests на invalid protobuf descriptor;
|
||||
- negative tests на GraphQL errors при HTTP `200`.
|
||||
|
||||
## 4. Формат тестовых данных
|
||||
|
||||
Рекомендуется использовать:
|
||||
|
||||
- JSON fixtures для sample input/output;
|
||||
- YAML golden files для export/import;
|
||||
- `.proto` и descriptor fixtures для gRPC;
|
||||
- snapshot tests для generated draft.
|
||||
|
||||
## 5. Техническая стратегия
|
||||
|
||||
Для Rust-части:
|
||||
|
||||
- unit/integration tests через `cargo test`;
|
||||
- тестовые фикстуры в `tests/fixtures/`;
|
||||
- golden files для YAML;
|
||||
- отдельные integration suites для registry и admin-api.
|
||||
|
||||
Для frontend:
|
||||
|
||||
- unit tests для form helpers и schema rendering;
|
||||
- integration tests для critical user flows;
|
||||
- отдельная проверка mapping editor и sample upload flows.
|
||||
|
||||
## 6. Что нельзя оставлять без тестов
|
||||
|
||||
- version increment logic;
|
||||
- publish semantics;
|
||||
- YAML import как `create|upsert`;
|
||||
- auth profile resolution;
|
||||
- generated draft application;
|
||||
- MCP tool execution path.
|
||||
|
||||
## 7. Практический итог
|
||||
|
||||
Перед активной разработкой проект должен исходить из правила:
|
||||
|
||||
- доменная логика тестируется отдельно;
|
||||
- adapters тестируются отдельно;
|
||||
- registry и admin-api тестируются на реальной БД;
|
||||
- минимум один полный end-to-end сценарий должен быть воспроизводим автоматически.
|
||||
@@ -0,0 +1,20 @@
|
||||
fmt:
|
||||
cargo fmt --all
|
||||
|
||||
fmt-check:
|
||||
cargo fmt --all --check
|
||||
|
||||
check:
|
||||
cargo check --workspace
|
||||
|
||||
clippy:
|
||||
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
|
||||
test:
|
||||
cargo test --workspace --all-targets
|
||||
|
||||
verify:
|
||||
just fmt-check
|
||||
just clippy
|
||||
just test
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
components = ["clippy", "rustfmt"]
|
||||
|
||||
Reference in New Issue
Block a user