From fb302b2a2c7fa387d3e12ad774e690dd3fd58206 Mon Sep 17 00:00:00 2001 From: "a.tolmachev" Date: Wed, 25 Mar 2026 12:20:42 +0300 Subject: [PATCH] Initialize project scaffold and domain model --- .gitignore | 11 + AGENTS.md | 68 +++ Cargo.lock | 346 +++++++++++ Cargo.toml | 29 + README.md | 74 +++ TASKS.md | 45 ++ apps/admin-api/Cargo.toml | 11 + apps/admin-api/src/main.rs | 1 + apps/mcp-server/Cargo.toml | 11 + apps/mcp-server/src/main.rs | 1 + apps/ui/README.md | 3 + crates/mcpaas-adapter-graphql/Cargo.toml | 13 + crates/mcpaas-adapter-graphql/src/lib.rs | 3 + crates/mcpaas-adapter-grpc/Cargo.toml | 14 + crates/mcpaas-adapter-grpc/src/lib.rs | 3 + crates/mcpaas-adapter-rest/Cargo.toml | 13 + crates/mcpaas-adapter-rest/src/lib.rs | 3 + crates/mcpaas-core/Cargo.toml | 14 + crates/mcpaas-core/src/auth.rs | 59 ++ crates/mcpaas-core/src/ids.rs | 42 ++ crates/mcpaas-core/src/lib.rs | 16 + crates/mcpaas-core/src/operation.rs | 363 ++++++++++++ crates/mcpaas-core/src/protocol.rs | 42 ++ crates/mcpaas-mapping/Cargo.toml | 15 + crates/mcpaas-mapping/src/lib.rs | 129 +++++ crates/mcpaas-proto/Cargo.toml | 13 + crates/mcpaas-proto/src/lib.rs | 3 + crates/mcpaas-registry/Cargo.toml | 15 + crates/mcpaas-registry/src/lib.rs | 3 + crates/mcpaas-runtime/Cargo.toml | 18 + crates/mcpaas-runtime/src/lib.rs | 3 + crates/mcpaas-schema/Cargo.toml | 15 + crates/mcpaas-schema/src/lib.rs | 164 ++++++ docs/admin-api.md | 451 ++++++++++++++ docs/architecture.md | 517 +++++++++++++++++ docs/data-model.md | 696 ++++++++++++++++++++++ docs/database-schema.md | 355 ++++++++++++ docs/development-rules.md | 251 ++++++++ docs/diagrams.md | 386 ++++++++++++ docs/implementation-plan.md | 399 +++++++++++++ docs/mcp-interface.md | 162 ++++++ docs/module-decomposition.md | 709 +++++++++++++++++++++++ docs/protocols/graphql.md | 107 ++++ docs/protocols/grpc.md | 107 ++++ docs/protocols/rest.md | 112 ++++ docs/runtime-config.md | 127 ++++ docs/rust-code-rules.md | 316 ++++++++++ docs/rust-design.md | 402 +++++++++++++ docs/testing-strategy.md | 131 +++++ justfile | 20 + rust-toolchain.toml | 4 + 51 files changed, 6815 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 TASKS.md create mode 100644 apps/admin-api/Cargo.toml create mode 100644 apps/admin-api/src/main.rs create mode 100644 apps/mcp-server/Cargo.toml create mode 100644 apps/mcp-server/src/main.rs create mode 100644 apps/ui/README.md create mode 100644 crates/mcpaas-adapter-graphql/Cargo.toml create mode 100644 crates/mcpaas-adapter-graphql/src/lib.rs create mode 100644 crates/mcpaas-adapter-grpc/Cargo.toml create mode 100644 crates/mcpaas-adapter-grpc/src/lib.rs create mode 100644 crates/mcpaas-adapter-rest/Cargo.toml create mode 100644 crates/mcpaas-adapter-rest/src/lib.rs create mode 100644 crates/mcpaas-core/Cargo.toml create mode 100644 crates/mcpaas-core/src/auth.rs create mode 100644 crates/mcpaas-core/src/ids.rs create mode 100644 crates/mcpaas-core/src/lib.rs create mode 100644 crates/mcpaas-core/src/operation.rs create mode 100644 crates/mcpaas-core/src/protocol.rs create mode 100644 crates/mcpaas-mapping/Cargo.toml create mode 100644 crates/mcpaas-mapping/src/lib.rs create mode 100644 crates/mcpaas-proto/Cargo.toml create mode 100644 crates/mcpaas-proto/src/lib.rs create mode 100644 crates/mcpaas-registry/Cargo.toml create mode 100644 crates/mcpaas-registry/src/lib.rs create mode 100644 crates/mcpaas-runtime/Cargo.toml create mode 100644 crates/mcpaas-runtime/src/lib.rs create mode 100644 crates/mcpaas-schema/Cargo.toml create mode 100644 crates/mcpaas-schema/src/lib.rs create mode 100644 docs/admin-api.md create mode 100644 docs/architecture.md create mode 100644 docs/data-model.md create mode 100644 docs/database-schema.md create mode 100644 docs/development-rules.md create mode 100644 docs/diagrams.md create mode 100644 docs/implementation-plan.md create mode 100644 docs/mcp-interface.md create mode 100644 docs/module-decomposition.md create mode 100644 docs/protocols/graphql.md create mode 100644 docs/protocols/grpc.md create mode 100644 docs/protocols/rest.md create mode 100644 docs/runtime-config.md create mode 100644 docs/rust-code-rules.md create mode 100644 docs/rust-design.md create mode 100644 docs/testing-strategy.md create mode 100644 justfile create mode 100644 rust-toolchain.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39bb821 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +/target +/.idea +/.vscode +/.DS_Store +/node_modules +/dist +/coverage +/.env +/var +*.log + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..47371d5 --- /dev/null +++ b/AGENTS.md @@ -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/`. +- 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. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..33d2385 --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0dea7d5 --- /dev/null +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md new file mode 100644 index 0000000..a09d485 --- /dev/null +++ b/README.md @@ -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. Для первой версии это слишком большой отдельный пласт сложности. diff --git a/TASKS.md b/TASKS.md new file mode 100644 index 0000000..8476257 --- /dev/null +++ b/TASKS.md @@ -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` diff --git a/apps/admin-api/Cargo.toml b/apps/admin-api/Cargo.toml new file mode 100644 index 0000000..51b01b7 --- /dev/null +++ b/apps/admin-api/Cargo.toml @@ -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 + diff --git a/apps/admin-api/src/main.rs b/apps/admin-api/src/main.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/apps/admin-api/src/main.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/apps/mcp-server/Cargo.toml b/apps/mcp-server/Cargo.toml new file mode 100644 index 0000000..b8a2d0b --- /dev/null +++ b/apps/mcp-server/Cargo.toml @@ -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 + diff --git a/apps/mcp-server/src/main.rs b/apps/mcp-server/src/main.rs new file mode 100644 index 0000000..f328e4d --- /dev/null +++ b/apps/mcp-server/src/main.rs @@ -0,0 +1 @@ +fn main() {} diff --git a/apps/ui/README.md b/apps/ui/README.md new file mode 100644 index 0000000..1c1a2d4 --- /dev/null +++ b/apps/ui/README.md @@ -0,0 +1,3 @@ +# UI + +The UI app is planned as a separate TypeScript project and is intentionally kept outside the Cargo workspace. diff --git a/crates/mcpaas-adapter-graphql/Cargo.toml b/crates/mcpaas-adapter-graphql/Cargo.toml new file mode 100644 index 0000000..9fc9f90 --- /dev/null +++ b/crates/mcpaas-adapter-graphql/Cargo.toml @@ -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 + diff --git a/crates/mcpaas-adapter-graphql/src/lib.rs b/crates/mcpaas-adapter-graphql/src/lib.rs new file mode 100644 index 0000000..33c9ca1 --- /dev/null +++ b/crates/mcpaas-adapter-graphql/src/lib.rs @@ -0,0 +1,3 @@ +pub fn crate_name() -> &'static str { + "mcpaas-adapter-graphql" +} diff --git a/crates/mcpaas-adapter-grpc/Cargo.toml b/crates/mcpaas-adapter-grpc/Cargo.toml new file mode 100644 index 0000000..ced060b --- /dev/null +++ b/crates/mcpaas-adapter-grpc/Cargo.toml @@ -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 + diff --git a/crates/mcpaas-adapter-grpc/src/lib.rs b/crates/mcpaas-adapter-grpc/src/lib.rs new file mode 100644 index 0000000..8523de6 --- /dev/null +++ b/crates/mcpaas-adapter-grpc/src/lib.rs @@ -0,0 +1,3 @@ +pub fn crate_name() -> &'static str { + "mcpaas-adapter-grpc" +} diff --git a/crates/mcpaas-adapter-rest/Cargo.toml b/crates/mcpaas-adapter-rest/Cargo.toml new file mode 100644 index 0000000..cbabb78 --- /dev/null +++ b/crates/mcpaas-adapter-rest/Cargo.toml @@ -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 + diff --git a/crates/mcpaas-adapter-rest/src/lib.rs b/crates/mcpaas-adapter-rest/src/lib.rs new file mode 100644 index 0000000..15bc142 --- /dev/null +++ b/crates/mcpaas-adapter-rest/src/lib.rs @@ -0,0 +1,3 @@ +pub fn crate_name() -> &'static str { + "mcpaas-adapter-rest" +} diff --git a/crates/mcpaas-core/Cargo.toml b/crates/mcpaas-core/Cargo.toml new file mode 100644 index 0000000..4ab229f --- /dev/null +++ b/crates/mcpaas-core/Cargo.toml @@ -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 diff --git a/crates/mcpaas-core/src/auth.rs b/crates/mcpaas-core/src/auth.rs new file mode 100644 index 0000000..060ebb8 --- /dev/null +++ b/crates/mcpaas-core/src/auth.rs @@ -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) -> 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, +} diff --git a/crates/mcpaas-core/src/ids.rs b/crates/mcpaas-core/src/ids.rs new file mode 100644 index 0000000..6d96792 --- /dev/null +++ b/crates/mcpaas-core/src/ids.rs @@ -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) -> Self { + Self(value.into()) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + } + + impl From 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 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); diff --git a/crates/mcpaas-core/src/lib.rs b/crates/mcpaas-core/src/lib.rs new file mode 100644 index 0000000..99cba1a --- /dev/null +++ b/crates/mcpaas-core/src/lib.rs @@ -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}; diff --git a/crates/mcpaas-core/src/operation.rs b/crates/mcpaas-core/src/operation.rs new file mode 100644 index 0000000..6a05526 --- /dev/null +++ b/crates/mcpaas-core/src/operation.rs @@ -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, +} + +#[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, +} + +#[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, + #[serde(skip_serializing_if = "Option::is_none")] + pub auth_profile_ref: Option, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub headers: BTreeMap, + #[serde(skip_serializing_if = "Option::is_none")] + pub protocol_options: Option, +} + +#[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, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub examples: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct Samples { + #[serde(skip_serializing_if = "Option::is_none")] + pub input_json_sample_ref: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub output_json_sample_ref: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub proto_file_ref: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub descriptor_ref: Option, +} + +#[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, + #[serde(skip_serializing_if = "Option::is_none")] + pub generated_at: Option, + 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, +} + +#[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 { + 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, + #[serde(skip_serializing_if = "Option::is_none")] + pub generated_draft: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub config_export: Option, + pub created_at: String, + pub updated_at: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub published_at: Option, +} + +impl Operation { + 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_yaml::from_str(&yaml).unwrap(); + + assert!(yaml.contains("protocol: rest")); + assert!(yaml.contains("export_mode: portable")); + assert_eq!(restored, operation); + } +} diff --git a/crates/mcpaas-core/src/protocol.rs b/crates/mcpaas-core/src/protocol.rs new file mode 100644 index 0000000..2258ce6 --- /dev/null +++ b/crates/mcpaas-core/src/protocol.rs @@ -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, +} diff --git a/crates/mcpaas-mapping/Cargo.toml b/crates/mcpaas-mapping/Cargo.toml new file mode 100644 index 0000000..8c3fc5e --- /dev/null +++ b/crates/mcpaas-mapping/Cargo.toml @@ -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 diff --git a/crates/mcpaas-mapping/src/lib.rs b/crates/mcpaas-mapping/src/lib.rs new file mode 100644 index 0000000..893031e --- /dev/null +++ b/crates/mcpaas-mapping/src/lib.rs @@ -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, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub transform: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub condition: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub notes: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)] +pub struct MappingSet { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub rules: Vec, +} + +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); + } +} diff --git a/crates/mcpaas-proto/Cargo.toml b/crates/mcpaas-proto/Cargo.toml new file mode 100644 index 0000000..f08b19d --- /dev/null +++ b/crates/mcpaas-proto/Cargo.toml @@ -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 + diff --git a/crates/mcpaas-proto/src/lib.rs b/crates/mcpaas-proto/src/lib.rs new file mode 100644 index 0000000..d914202 --- /dev/null +++ b/crates/mcpaas-proto/src/lib.rs @@ -0,0 +1,3 @@ +pub fn crate_name() -> &'static str { + "mcpaas-proto" +} diff --git a/crates/mcpaas-registry/Cargo.toml b/crates/mcpaas-registry/Cargo.toml new file mode 100644 index 0000000..47eccbe --- /dev/null +++ b/crates/mcpaas-registry/Cargo.toml @@ -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 + diff --git a/crates/mcpaas-registry/src/lib.rs b/crates/mcpaas-registry/src/lib.rs new file mode 100644 index 0000000..dee68c0 --- /dev/null +++ b/crates/mcpaas-registry/src/lib.rs @@ -0,0 +1,3 @@ +pub fn crate_name() -> &'static str { + "mcpaas-registry" +} diff --git a/crates/mcpaas-runtime/Cargo.toml b/crates/mcpaas-runtime/Cargo.toml new file mode 100644 index 0000000..b8a18f4 --- /dev/null +++ b/crates/mcpaas-runtime/Cargo.toml @@ -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 + diff --git a/crates/mcpaas-runtime/src/lib.rs b/crates/mcpaas-runtime/src/lib.rs new file mode 100644 index 0000000..d8cd3a1 --- /dev/null +++ b/crates/mcpaas-runtime/src/lib.rs @@ -0,0 +1,3 @@ +pub fn crate_name() -> &'static str { + "mcpaas-runtime" +} diff --git a/crates/mcpaas-schema/Cargo.toml b/crates/mcpaas-schema/Cargo.toml new file mode 100644 index 0000000..1c73ecd --- /dev/null +++ b/crates/mcpaas-schema/Cargo.toml @@ -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 diff --git a/crates/mcpaas-schema/src/lib.rs b/crates/mcpaas-schema/src/lib.rs new file mode 100644 index 0000000..8d9a4de --- /dev/null +++ b/crates/mcpaas-schema/src/lib.rs @@ -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, + #[serde(default)] + pub required: bool, + #[serde(default)] + pub nullable: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub default_value: Option, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub fields: BTreeMap, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub items: Option>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub enum_values: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub variants: Vec, +} + +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); + } +} diff --git a/docs/admin-api.md b/docs/admin-api.md new file mode 100644 index 0000000..a18dae2 --- /dev/null +++ b/docs/admin-api.md @@ -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. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..ece87db --- /dev/null +++ b/docs/architecture.md @@ -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 слоя, отдельной схемной модели и отдельного адаптера. diff --git a/docs/data-model.md b/docs/data-model.md new file mode 100644 index 0000000..c1e1a51 --- /dev/null +++ b/docs/data-model.md @@ -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`. + +Следующим логическим шагом после этого документа должна стать схема БД, в которой эти сущности будут разложены по таблицам и связям. diff --git a/docs/database-schema.md b/docs/database-schema.md new file mode 100644 index 0000000..c983e36 --- /dev/null +++ b/docs/database-schema.md @@ -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 модели домена. diff --git a/docs/development-rules.md b/docs/development-rules.md new file mode 100644 index 0000000..764a9fa --- /dev/null +++ b/docs/development-rules.md @@ -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/`. + +Примеры: + +- `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/`; +- пушим атомарно и периодически; +- пишем commit messages и неизбежные code comments только на английском; +- стараемся вообще обходиться без комментариев в коде за счет самоописывающегося дизайна; +- не допускаем временных архитектурных компромиссов, которые потом невозможно разгрести. diff --git a/docs/diagrams.md b/docs/diagrams.md new file mode 100644 index 0000000..4d3a301 --- /dev/null +++ b/docs/diagrams.md @@ -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 уже описаны. diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md new file mode 100644 index 0000000..849f718 --- /dev/null +++ b/docs/implementation-plan.md @@ -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. + +Такой порядок минимизирует архитектурный риск и дает ранний рабочий результат. diff --git a/docs/mcp-interface.md b/docs/mcp-interface.md new file mode 100644 index 0000000..02515f1 --- /dev/null +++ b/docs/mcp-interface.md @@ -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 слое. diff --git a/docs/module-decomposition.md b/docs/module-decomposition.md new file mode 100644 index 0000000..acdd833 --- /dev/null +++ b/docs/module-decomposition.md @@ -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` останутся тонкими входными слоями; +- добавление нового протокола не потребует переписывать половину проекта. + +Это и есть целевая архитектурная дисциплина проекта: отдельные слои, отдельные модели, минимально необходимая публичность и отсутствие "универсальных" структур, в которые со временем начинает стекаться вся система. diff --git a/docs/protocols/graphql.md b/docs/protocols/graphql.md new file mode 100644 index 0000000..a6b11cc --- /dev/null +++ b/docs/protocols/graphql.md @@ -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 в системе должен иметь отдельный адаптер и отдельную конфигурационную модель. diff --git a/docs/protocols/grpc.md b/docs/protocols/grpc.md new file mode 100644 index 0000000..efdfe57 --- /dev/null +++ b/docs/protocols/grpc.md @@ -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-платформой. diff --git a/docs/protocols/rest.md b/docs/protocols/rest.md new file mode 100644 index 0000000..d42b01b --- /dev/null +++ b/docs/protocols/rest.md @@ -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. diff --git a/docs/runtime-config.md b/docs/runtime-config.md new file mode 100644 index 0000000..f03f18f --- /dev/null +++ b/docs/runtime-config.md @@ -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. diff --git a/docs/rust-code-rules.md b/docs/rust-code-rules.md new file mode 100644 index 0000000..c790e83 --- /dev/null +++ b/docs/rust-code-rules.md @@ -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>` как универсальный контейнер состояния; +- 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; +- код читается за счет имен и декомпозиции, а не за счет комментариев. diff --git a/docs/rust-design.md b/docs/rust-design.md new file mode 100644 index 0000000..b72a4e8 --- /dev/null +++ b/docs/rust-design.md @@ -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; + async fn create_version(&self, cmd: CreateVersion) -> Result; + 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 { + repo: R, +} + +impl PublishOperationService +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` с разнородной логикой. diff --git a/docs/testing-strategy.md b/docs/testing-strategy.md new file mode 100644 index 0000000..5321ab0 --- /dev/null +++ b/docs/testing-strategy.md @@ -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 сценарий должен быть воспроизводим автоматически. diff --git a/justfile b/justfile new file mode 100644 index 0000000..3171a2e --- /dev/null +++ b/justfile @@ -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 + diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..8bb1f26 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "stable" +components = ["clippy", "rustfmt"] +