AgenticMail ships as five npm packages, all developed in a single monorepo. No Turborepo. No Nx. No Lerna. Just npm workspaces and a handful of conventions that keep everything manageable.
The five packages
@agenticmail/core: The foundation. Shared types, utility functions, configuration schemas, and the database layer. Every other package depends on core.
@agenticmail/api: The Express server with 107 endpoints. Depends on core for types and database access.
@agenticmail/mcp: The MCP server that exposes 62 tools over stdio. Communicates with the API over HTTP rather than importing it directly.
@agenticmail/openclaw: The OpenClaw plugin with 52 tools and lifecycle hooks. Also communicates with the API over HTTP.
agenticmail: The CLI package (no scope prefix, since it’s the user facing command). Depends on core for configuration and spawns the API server as a child process.
The dependency graph
The dependency relationships are intentional and strictly enforced.
Core sits at the bottom. It has zero internal dependencies. Everything it needs is either built in Node APIs or third party npm packages.
The API package depends on core. It imports types for request/response shapes, uses core’s database utilities, and relies on core’s configuration system. This is the only package that directly depends on core at the code level (besides the CLI).
The MCP and OpenClaw packages do not import core or API directly. They talk to the API server over HTTP. This is a deliberate architectural boundary. The MCP server can run on a completely different machine from the API server. The OpenClaw plugin can run inside an agent framework that has its own process model. Neither should need to bundle AgenticMail’s internals.
The CLI depends on core for reading and writing configuration files, but it treats the API as an opaque process. It spawns it, monitors it, and routes health checks to it, but never imports its route handlers or middleware.
This graph means you can update the API’s internal implementation without rebuilding MCP or OpenClaw. You can ship a new version of the MCP server without touching the API. The HTTP boundary between packages is a real isolation layer, not just a theoretical one.
npm workspaces
The package.json at the root declares the workspace:
{
"workspaces": ["packages/*"]
}
That’s it. npm resolves cross package dependencies, hoists shared dependencies, and lets you run scripts across all packages with npm run build --workspaces.
I evaluated Turborepo and Nx before choosing plain workspaces. Both add value for large teams with complex build graphs, but for a five package repo maintained by one person, they add more configuration overhead than they save. npm workspaces handle dependency resolution and script running. Anything more is unnecessary.
tsup for building
Every package uses tsup as its build tool. The configuration is nearly identical across all five:
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm", "cjs"],
dts: true,
clean: true,
})
Dual output (ESM and CJS) because the ecosystem is still split. Some consumers use import, others use require. Shipping both means AgenticMail works in either context without the consumer needing to configure anything.
tsup generates declaration files from the TypeScript source, so types ship alongside the JavaScript. No separate @types/agenticmail package to keep in sync.
The clean: true flag wipes the output directory before each build. This prevents stale artifacts from previous builds lingering and causing confusion. Every build starts fresh.
Vitest for testing
All five packages use Vitest. The choice was straightforward: Vitest understands TypeScript natively, runs tests in parallel, and its API is familiar to anyone who has used Jest.
Tests live alongside the source code they test. src/mail/send.ts has a corresponding src/mail/send.test.ts. This convention makes it easy to find tests and ensures they stay close to the code they cover.
The root package.json includes a test script that runs Vitest across all workspaces. CI runs the full suite on every push.
prepublishOnly hooks
Each package has a prepublishOnly script that runs the build before publishing. This prevents accidentally publishing stale output. If you run npm publish without building first, the hook catches it.
{
"scripts": {
"build": "tsup",
"prepublishOnly": "npm run build"
}
}
Simple, but it eliminates an entire category of “I published but forgot to build” mistakes.
Why this structure works
Five packages might seem like over engineering for what could be a single repo with a src/ directory. But the package boundaries map to real deployment boundaries. The API runs as a server. The MCP server runs as a stdio process. The OpenClaw plugin runs inside a framework. The CLI runs on a developer’s machine.
Each of those contexts has different dependencies, different entry points, and different consumers. Packaging them separately means each context only pulls in what it needs. An agent framework that uses the OpenClaw plugin doesn’t need Express or the CLI’s terminal UI library in its dependency tree.
The monorepo keeps development convenient: one clone, one npm install, one test command. The package boundaries keep deployment clean: each package ships exactly what its consumers need, nothing more.