Architecture
This page answers one practical question:
If you want to use groundworkers, which layer should you call?
The short answer
- Use MCP tools when you want a remote tool interface.
- Use
app.services.*when you want domain operations directly from Python. - Use
app.adapters.*when you need exact control over a specific integration.
Layers
What each layer is for
adapters/
Adapters are thin wrappers over external dependencies. Each adapter handles exactly one dependency:
CDMAdapter— holds the SQLAlchemy engine and session factory for a CDM database connection. Shared byVocabServiceandOmopGraphAdapter.OmopGraphAdapter— wraps the omop-graphKnowledgeGraph; owns concept traversal, grounding, and path-finding.OmopEmbAdapter— wraps the omop-embEmbeddingReaderInterface; owns embedding search and index access.LLMAdapter— wraps an OpenAI-compatible chat completion API; owns structured and unstructured model calls.
Adapters are config-agnostic. They receive already-constructed handles (Engine,
reader, API client) rather than config objects. Only build_application() reads
config and constructs those handles.
services/
Services contain domain logic that is useful to Python callers independent of MCP. A service layer exists when the logic encodes bespoke domain knowledge — multi-source orchestration, mapping policy, scoring — that a downstream Python application would want to call directly without going through MCP.
VocabService— vocabulary search and concept navigation over the CDM vocabulary tables. Providessearch_exact,search_normalized,search_fulltext,navigate_to_standard,navigate_to_value, andnavigate_to_unit.MappingService— orchestratesVocabService,OmopGraphAdapter, andOmopEmbAdapterto build candidate bundles, resolve mapping expressions, and assemble mapping context packets.TextService— LLM-backed clinical text preprocessing. Providesnormalize,decompose, anddisambiguateviaLLMAdapter.DomainService— LLM-backed batch OMOP domain classification for structured data-dictionary fields. Providesclassify_attributesviaLLMAdapter.
Not everything needs a service. resolver_tools.py calls omop-graph directly because
it adds nothing the graph library does not already expose. The service layer is not a
mandatory pass-through — it exists only where the logic is worth reusing.
tools/
Tools expose services and adapters over MCP. Each tool does three things:
- Validate and clamp inputs.
- Call a service or adapter method.
- Convert exceptions into MCP-safe error dicts.
If you are building an MCP client, this is the layer you interact with.
app.py and server.py
build_application(config)constructs the shared object graph: adapters, then services that depend on those adapters.create_server(config)wraps that graph and registers MCP tools on top of it.
Which layer to pick
| If you need... | Use... |
|---|---|
| Remote tool calls, agent interoperability | MCP tools |
| Vocabulary search or mapping workflows from Python | app.services.vocab or app.services.mapping |
| LLM-backed text preprocessing from Python | app.services.text |
| LLM-backed batch domain classification for structured fields | app.services.domain |
| Exact control over embedding or graph operations | app.adapters.omop_emb or app.adapters.omop_graph |
Request flow
Composition
Practical rules
- If the code coordinates multiple data sources or encodes domain policy, put it in
services/. - If the code mainly wraps a library API or database session, put it in
adapters/. - If the code validates inputs and shapes transport responses, put it in
tools/. - If a tool adds nothing beyond what the library already exposes, skip the service layer and let the tool call the adapter directly.