How one team organized a 118-tool MCP server across 19 files
Eugen details the architecture behind a 118-tool MCP server for a product called PaperLink, explaining how domain-based file splitting, OAuth-scope-aligned permissions, consistent verb naming, and Zod schemas tamed the complexity after three rewrites.
Score breakdown
Teams building large MCP servers can adopt this domain-plus-permission file structure and seven-verb naming convention to keep tool sets predictable for both developers and AI models as the tool count scales.
- 01The PaperLink MCP server grew from 25 tools to 118 tools across three rewrites before settling on its current architecture.
- 02All 118 tools are organized across 19 TypeScript files in a `tools/` directory, each grouped by domain and permission level.
- 03Each file exports a single `ToolRegistrar` function; the entry point calls all 19 registrars with no plugin system or config registry.
Eugen's post walks through the architecture of a 118-tool MCP server for PaperLink, arrived at after three rewrites starting from an initial 25 tools. The final structure places every tool in one of 19 TypeScript files under a `tools/` directory, each grouped by domain (e.g., `accountingReadTools.ts`, `documentLifecycleTools.ts`, `recurringTransactionTools.ts`) plus a shared `types.ts`. Each file exports a single function typed as `ToolRegistrar`, which accepts an `McpServer` instance and an `authInfo` object. The server entry point calls all 19 registrars in sequence — adding a new domain requires only one new file and one new line in the entry point.
The key driver for splitting files by permission level rather than just by domain was OAuth scope granularity.
The key driver for splitting files by permission level rather than just by domain was OAuth scope granularity. The system exposes 25 scopes (e.g., `accounting:read`, `accounting:write`, `accounting:delete`, `invoices:read`, `sharing:write`) so users can grant an AI assistant read-only access to some resources while allowing full write access to others. When all tools for a domain lived in one file, scope checks were scattered with no clear boundary; splitting by permission level made each file internally consistent. Smaller domains like categories and currencies keep read and write together until file length or permission complexity justifies a split.
Naming consistency is treated as a first-class concern: seven core verbs (`list`, `get`, `create`, `update`, `archive`, `restore`, `delete`) cover the standard CRUD lifecycle, with a small set of domain-specific verbs (e.g., `change-invoice-status`, `convert-estimate-to-invoice`, `pause-recurring-transaction`) for operations that don't fit the model. Batch operations use a plural noun (`create-transactions`) rather than a suffix like `-batch`. The rationale is that AI models pattern-match on tool names, so consistent conventions let a model predict tool names it hasn't encountered yet. Every tool's input is defined as a Zod schema, which simultaneously handles runtime validation, TypeScript typing inside the handler, and JSON Schema generation for MCP clients — eliminating the need for separate OpenAPI specs or manual JSON Schema definitions.
Key facts
- 01The PaperLink MCP server grew from 25 tools to 118 tools across three rewrites before settling on its current architecture.
- 02All 118 tools are organized across 19 TypeScript files in a `tools/` directory, each grouped by domain and permission level.
- 03Each file exports a single `ToolRegistrar` function; the entry point calls all 19 registrars with no plugin system or config registry.