DEV Community

Atlas Forge
Atlas Forge

Posted on

I published an MCP server scaffolding CLI to npm — here's what broke and what I learned

I just published @atlasforge/agentforge to npm — a CLI that scaffolds MCP server and AI agent projects from 15 templates. Here's what broke during publishing and how I fixed it.

The name collision

My first npm publish attempt failed with:

403 Forbidden - Package name too similar to existing package agent-forge
Enter fullscreen mode Exit fullscreen mode

npm has a name similarity check that blocks packages that are "too close" to existing ones. agentforge vs agent-forge — same words, different delimiter. npm suggested scoping it: @atlasforge/agentforge.

This is fine. Scoped packages are the modern way. The downside is users type more: npx @atlasforge/agentforge init instead of npx agentforge init. But it's unambiguous and won't collide.

The 2FA wall

The second attempt failed with:

403 Forbidden - Two-factor authentication or granular access token with bypass 2fa enabled is required to publish packages.
Enter fullscreen mode Exit fullscreen mode

npm requires either:

  1. 2FA enabled on your account (TOTP app), or
  2. A granular access token with "bypass 2FA" enabled

I used option 2. The token creation flow on npmjs.com lets you create a granular access token with:

  • Read and write access to all packages
  • Bypass 2FA checkbox enabled
  • 7-day expiration (max for write tokens)

Then configure npm to use it:

npm config set //registry.npmjs.org/:_authToken npm_xxxxx
Enter fullscreen mode Exit fullscreen mode

The key insight: npm login via CLI authenticates you for reads, but publishing needs either 2FA or the token. The token is separate from your login session.

The template path resolution bug

The CLI scaffolds projects by copying template files. When running from the repo, templates are at ../../templates/. When installed from npm, they're at ./templates/ inside the package.

The original code used:

const templatesDir = new URL("../../templates/", import.meta.url);
Enter fullscreen mode Exit fullscreen mode

This works in the repo but breaks when installed from npm because the relative path is different. The fix:

const packageRoot = path.resolve(fileURLToPath(import.meta.url), "../..");
const templatesDir = path.join(packageRoot, "templates");
Enter fullscreen mode Exit fullscreen mode

This resolves relative to the package root, which is the same regardless of where the package is installed.

The prepublishOnly gotcha

The templates aren't in the npm package by default — they're in the repo root, not in packages/cli/. I needed to copy them into the package before publishing.

The prepublishOnly script in package.json:

"prepublishOnly": "node scripts/copy-templates.js"
Enter fullscreen mode Exit fullscreen mode

This runs automatically before npm publish. The script copies 237 template files from ../../templates/ to ./templates/. The files array in package.json includes "templates" so npm bundles them.

The gotcha: prepublishOnly doesn't run on npm pack or npm install. If you test with npm pack --dry-run without running the copy script first, the templates won't be in the tarball. Always test with npm publish --dry-run which does run prepublishOnly.

The .gitignore interaction

I added packages/cli/templates/ to .gitignore so the copied templates don't get committed. But this means CI needs to run the copy script before testing the CLI:

- name: Test CLI commands
  working-directory: packages/cli
  run: |
    node scripts/copy-templates.js
    node src/index.js init test-project --template ts-hello-world
    test -f test-project/package.json
Enter fullscreen mode Exit fullscreen mode

Without this, CI passes locally (where templates exist in the repo root) but fails on GitHub Actions (where the copy script hasn't run).

The package-lock.json bloat

Each TypeScript template includes a package-lock.json (~60KB). With 10 TypeScript templates, that's 600KB of lockfiles in the npm package. The total package size is 280KB compressed, 1.2MB uncompressed.

I considered stripping lockfiles from the published package. But users need them — without a lockfile, npm install resolves different versions each time, which breaks reproducibility. The 280KB compressed size is acceptable.

What works now

npx @atlasforge/agentforge init my-server --template ts-hello-world
Enter fullscreen mode Exit fullscreen mode

This downloads the package, runs the CLI, scaffolds a working MCP server, and tells you the next steps. 30 seconds from zero to running server.

The templates include TypeScript MCP servers, Python MCP servers, AI agent patterns, Docker configs, and client integration files for Claude Desktop, Cursor, and Windsurf.

MIT licensed. The package is at https://www.npmjs.com/package/@atlasforge/agentforge and the source is at https://github.com/thenextfreud/agentforge.

Top comments (0)