-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
src/
index.ts Entry point: construct and start, nothing else
ApplicationFactory.ts Composition root: the only file that wires things
types/ Interfaces and type aliases, one file per concern
errors/ Named error classes
domain/ Engine catalog, ConnectionTarget, ConnectionProfile
config/ EnvironmentConfigLoader, PackageVersionLoader
connections/ URL parser, target factory, registry, manager
drivers/ DatabaseDriver strategy, registry, cache, provider
sql/ MySQL, PostgreSQL, SQLite (+ worker), SQL Server, ClickHouse
document/ MongoDB, stage allowlist, schema sampler
keyvalue/ Redis, COMMAND INFO flags guard
search/ Elasticsearch and OpenSearch
validation/
sql/ Dialects, skeletonizer, validator, rules/
document/ MongoDB operator guard
keyvalue/ Redis command allowlist
search/ Search body allowlist
names/ Per-engine naming policies
formatting/ ToolResponse, RowFormatter, JsonSerializer
logging/ Optional call log: CallLogger, Redactor, formatters, sinks
tools/ BaseTool, DatabaseScopedTool, and one folder per group
server/ McpDbServer
flowchart TD
I["index.ts"] --> AF["ApplicationFactory<br/><i>composition root</i>"]
AF --> SRV["server/<br/>McpDbServer"]
AF --> CFG["config/<br/>EnvironmentConfigLoader"]
AF --> DRV["drivers/<br/>DriverRegistry, DriverCache"]
SRV --> T["tools/"]
T --> CX["connections/<br/>Registry, Manager"]
T --> DP["drivers/<br/>DriverProvider"]
T --> VAL["validation/"]
T --> FMT["formatting/"]
DP --> DRV
CX --> DOM["domain/<br/>EngineCatalog, ConnectionTarget"]
DRV --> DOM
CFG --> DOM
VAL --> DOM
style AF fill:#eef,stroke:#66a
style DOM fill:#efe,stroke:#6a6
| Class | Responsibility | Holds state? |
|---|---|---|
ApplicationFactory |
Build the object graph, register one driver factory per engine | No |
McpDbServer |
Register tools, run, shut down | Shutdown flag |
EnvironmentConfigLoader |
Read configuration, report problems | No |
EngineCatalog |
Static facts about each engine and scheme | No |
ConnectionUrlParser |
Turn a URL into target fields, for any engine | No |
ConnectionTargetFactory |
Build targets from URLs and legacy MySQL configuration | No |
ConnectionRegistry |
Known profiles, the active connection | Yes |
ConnectionManager |
Change the active connection safely | No |
DriverRegistry |
Which factory builds which engine's driver | Registered factories |
DriverCache |
One driver per connection, LRU | Yes |
DriverProvider |
Resolve a call's target and hand back a driver of the right family | No |
| Each driver | Talk to one engine, enforce its read-only layer two | Its lazily opened client |
ReadOnlyQueryValidator |
Decide if a SQL statement may run, per dialect | No |
| Other validators | The same, for MongoDB, Redis and search bodies | No |
BaseTool subclasses |
One tool each | No |
A run_query call against PostgreSQL:
sequenceDiagram
autonumber
participant C as MCP client
participant T as RunQueryTool
participant P as DriverProvider
participant V as Validator (postgres)
participant D as PostgresDriver
participant S as PostgreSQL
C->>T: tools/call run_query
Note over T: BaseTool.invoke wraps<br/>everything in try/catch
T->>P: requireActiveTarget, requireFamily("sql")
P-->>T: target (or EngineMismatchError)
T->>P: acquireSql(target)
P-->>T: driver (cached, not yet connected)
T->>V: validate(sql) with driver.dialect
alt a rule objects
V-->>T: invalid
T-->>C: isError, no connection used
else allowed
T->>D: query(sql)
D->>S: BEGIN READ ONLY#59; SET LOCAL ...
D->>S: the statement, extended protocol
S-->>D: rows
D->>S: ROLLBACK
D-->>T: rows
T-->>C: text, truncated at 100 rows
end
Acquiring a driver does no I/O, so a rejected query still costs no connection. The dialect comes from the driver, which is why validation happens after acquiring it.
Two fields on ConnectionRegistry:
private activeName: string | null = null;
private activeTarget: ConnectionTarget | null = null;Changing them changes where every subsequent call goes, including onto a different engine. There is no reconnect step: DriverCache keys its drivers by ConnectionTarget.key(), so pointing at a different database or server selects a different driver, and pointing back reuses the original.
It looks simpler but makes a pool lie. A pool holds several connections and USE affects only the one it ran on, so a later query served by a different connection would silently read the old database. Keying drivers by target means every connection a driver opens was pointed at the right database from the start. It also generalises: PostgreSQL has no USE at all, since a database there is a connection parameter.
The active connection is process-wide, so two concurrently handled tool calls share it. The mitigation is the optional database argument on every reading tool, which resolves its own target and ignores the shared state. Scoping the connection per request would need a session identifier MCP does not carry.
MCP fixes the tool list at handshake, while the active engine changes whenever the user switches. Hiding run_query while connected to MongoDB would mean the list is wrong the moment they switch back. So all eighteen tools are always listed, and a family-specific tool called against the wrong engine fails with EngineMismatchError, which names the tools that do fit.
Nothing constructs its own dependencies, which means nothing can be tested in isolation unless something assembles them. ApplicationFactory is that something, and keeping it to one file means the wiring is reviewable in one place. It is also the only file that names a concrete driver class, which is what keeps connections/ and tools/ free of engine knowledge.
Stdio means the client owns the process lifetime, there is no port to bind, nothing to authenticate, and nothing is reachable from outside the machine. For a process holding database credentials, not listening on a socket is a feature.
The one exception is opt-in: the live log viewer (Call Logging) listens on DB_LOG_PORT when it is set. It serves only the call log, read-only, and nothing else; the MCP protocol itself stays on stdio. The cost is that stdout is sacred: it carries the JSON-RPC stream, so every diagnostic goes through the injected logger to stderr, and the SQLite worker process has its stdout disconnected entirely.
User guide
Developer guide
- Architecture
- Design Patterns
- Domain and Configuration
- Drivers
- Read Only Enforcement
- Tools internals
- Call Logging internals
- Server Lifecycle
- Testing
- Release Process
Links