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