Packaging Node.js and TypeScript AI agents for distribution via npm. Use when the user mentions: npm, npx, node agent, typescript agent, npm publish, package.json, bin field, npm package, agent distribution, node distribution, vercel ai sdk, npm install, npm registry, npx agent, global install, scoped package, npm provenance, tsconfig, esbuild, tsup, bundling agent, npm ci, prepublishOnly, monorepo agent, npm workspace, agent cli
npm is the right distribution channel when:
npx command, zero prior setup.npm gives you: a global registry, dependency resolution, semantic versioning, provenance attestation, and the npx zero-install runner. No other JavaScript distribution method matches this combination.
Every field matters. Here is the annotated structure for a distributable AI agent.
{
"name": "@your-org/agent-name",
"version": "1.0.0",
"description": "One-line description of what this agent does",
"type": "module",
"bin": {
"agent-name": "./dist/cli.js"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist/"
],
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"start": "node dist/cli.js",
"inspect": "npx @modelcontextprotocol/inspector node dist/cli.js",
"lint": "eslint src/",
"test": "vitest run",
"prepublishOnly": "npm run build && npm run test"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.7.0",
"vitest": "^3.0.0"
},
"engines": {
"node": ">=18.0.0"
},
"keywords": [
"ai-agent",
"mcp",
"mcp-server",
"cli",
"automation"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/your-org/agent-name"
}
}
name -- Use a scoped name (@your-org/agent-name). Scoped packages avoid name collisions on the npm registry and signal organizational ownership. Unscoped names are fine for personal projects but become a liability at scale.
version -- Follow semantic versioning strictly. For AI agents, consider: MAJOR = breaking changes to tool schemas or CLI interface, MINOR = new tools or capabilities, PATCH = bug fixes and prompt improvements.
description -- One sentence, no jargon. This appears in npm search results and registry listings. Front-load the action: "Searches GitHub issues via MCP" beats "An MCP server for GitHub."
type: "module" -- Declare ESM. Modern Node.js, the MCP SDK, and most AI libraries use ES modules. If you omit this, Node.js defaults to CommonJS, and you will fight import/require mismatches.
bin -- This is what makes your package executable. When a user runs npx -y @your-org/agent-name, npm looks up the bin field, downloads the package, and runs the specified file. The key in the bin object becomes the command name. The value must point to a compiled .js file in your dist/ directory, never to a .ts source file.
main -- Entry point for programmatic import. Allows other packages to import your agent's functionality without using the CLI.
types -- TypeScript declaration file. Enables type checking and IDE autocompletion for consumers who import your package.
files -- Whitelist of files to include in the published package. Only dist/ should ship. This excludes src/, tests/, .github/, node_modules/, and everything else. Always verify with npm pack --dry-run.
engines -- Declare the minimum Node.js version. The MCP SDK requires Node 18+. If you use fetch without a polyfill, you need Node 18+. If you use node: protocol imports, you need Node 16+. State this explicitly so npm warns users on incompatible versions.
keywords -- Used by the npm registry search and by MCP registries for auto-discovery. Always include mcp and mcp-server if your agent is an MCP server.
scripts.inspect -- MCP-specific. Launches the MCP Inspector for interactive testing. Not relevant for non-MCP agents but essential for MCP servers.
scripts.prepublishOnly -- Runs automatically before npm publish. Build and test in this hook so you never publish stale or broken code.
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}
Key settings explained:
target: "ES2022" -- Enables top-level await, which MCP servers and modern agents rely on.module: "Node16" / moduleResolution: "Node16" -- Correct ESM resolution for Node.js. Do not use "bundler" unless you are actually bundling.declaration: true -- Generates .d.ts files so consumers get type information.outDir: "./dist" -- Compiled output goes to dist/, matching the files field in package.json.rootDir: "./src" -- Preserves directory structure in output. src/index.ts becomes dist/index.ts.TypeScript strips shebangs during compilation. You need the shebang in the compiled output for npx to work. Three approaches:
Option A: Banner plugin (recommended for tsc users)
Add a postbuild script that prepends the shebang:
{
"scripts": {
"build": "tsc && node -e \"const f='dist/cli.js';const c=require('fs').readFileSync(f,'utf8');if(!c.startsWith('#!'))require('fs').writeFileSync(f,'#!/usr/bin/env node\\n'+c)\""
}
}
Option B: Use tsup or esbuild (handles it natively)
// tsup.config.ts
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/cli.ts"],
format: ["esm"],
banner: { js: "#!/usr/bin/env node" },
outDir: "dist",
clean: true,
});
Option C: Keep the shebang as a comment that survives compilation
Place #!/usr/bin/env node as the first line of your .ts file. TypeScript preserves it if it is literally the first line (before any imports). Verify this works with your TypeScript version.
#!/usr/bin/env node
This line must be the very first line of your CLI entry point (dist/cli.js). It tells the operating system to execute the file using Node.js.
Without it:
npx @your-org/agent fails on macOS/Linux with "Permission denied" or attempts to run the file as a shell script..cmd wrapper), but the shebang is still best practice.After building, always verify:
head -1 dist/cli.js
# Should output: #!/usr/bin/env node
The gold standard for agent distribution. The user runs one command and your agent executes immediately.
npx -y @your-org/agent-name
How it works:
@your-org/agent-name is installed locally or globally.bin field in package.json.The -y flag auto-confirms the download prompt, enabling non-interactive usage. This is critical for MCP server configs, where Claude Desktop spawns the process without user interaction.
npx -y @your-org/agent-name --port 3000 --verbose
Everything after the package name is passed as command-line arguments to your entry point. Parse them with a library like commander, yargs, or Node's built-in util.parseArgs.
npx -y @your-org/agent-name@2.1.0
Users can pin to a specific version. For MCP server configs, this prevents surprise breaking changes:
{
"command": "npx",
"args": ["-y", "@your-org/agent-name@2.1.0"]
}
For agents that users run frequently, a global install avoids re-downloading on every invocation.
npm install -g @your-org/agent-name
agent-name --help
The command name comes from the bin field key in package.json. After global install, it is available system-wide.
Trade-offs vs. npx:
npm update -g @your-org/agent-name).Recommendation: Document both patterns. Lead with npx for quick starts, mention global install for power users.
When you maintain multiple agents in a single repository, use npm workspaces.
my-agents/
package.json # Root with workspaces config
packages/
agent-search/
package.json # @your-org/agent-search
src/
agent-deploy/
package.json # @your-org/agent-deploy
src/
shared/
package.json # @your-org/agent-shared (internal)
src/
Root package.json:
{
"name": "my-agents",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"build": "npm run build --workspaces",
"publish-all": "npm publish --workspaces --access public"
}
}
Each agent package has its own package.json, bin, files, and version. They can depend on each other using workspace protocol ("@your-org/agent-shared": "workspace:*") during development. npm resolves these to real versions at publish time.
// Good: check at startup, fail with clear message
const apiKey = process.env.API_KEY;
if (!apiKey) {
console.error("Error: API_KEY environment variable is required.");
console.error("");
console.error("Set it in your MCP config:");
console.error(' "env": { "API_KEY": "your-key-here" }');
console.error("");
console.error("Or export it in your shell:");
console.error(" export API_KEY=your-key-here");
process.exit(1);
}
import "dotenv/config"; // Loads .env file if present
const apiKey = process.env.API_KEY;
Include dotenv as a regular dependency (not devDependency) if your agent supports .env files. But document that MCP server configs should use the env field instead.
// NEVER do this
const API_KEY = "sk-abc123secretkey";
// ALWAYS do this
const API_KEY = process.env.API_KEY;
Hardcoded keys end up in the npm registry, in Git history, and in your users' node_modules. There is no way to fully retract a published npm package.
Bundle your agent into a single file when:
Do NOT bundle when:
import individual modules from your package.{
"scripts": {
"build": "esbuild src/cli.ts --bundle --platform=node --target=node18 --outfile=dist/cli.js --format=esm --banner:js='#!/usr/bin/env node'"
}
}
// tsup.config.ts
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/cli.ts"],
format: ["esm"],
target: "node18",
platform: "node",
banner: { js: "#!/usr/bin/env node" },
outDir: "dist",
clean: true,
// Mark native modules as external
external: ["better-sqlite3"],
});
# Check file size
ls -lh dist/cli.js
# Verify it runs standalone
node dist/cli.js --help
# Verify shebang
head -1 dist/cli.js
# Create an npm account (if you don't have one)
npm adduser
# Login
npm login
# Create your organization (for scoped packages)
# Do this at https://www.npmjs.com/org/create
# 1. Verify package contents
npm pack --dry-run
# 2. Check for sensitive files
# Look for .env, credentials, secrets in the file list
# 3. Bump version
npm version patch # 1.0.0 -> 1.0.1
npm version minor # 1.0.0 -> 1.1.0
npm version major # 1.0.0 -> 2.0.0
# 4. Publish (prepublishOnly runs build + test automatically)
npm publish --access public
# 5. Verify
npx -y @your-org/agent-name@latest --help
Scoped packages (@org/name) require --access public on first publish. After that, subsequent publishes default to the access level of the previous version.
Provenance attestation creates a verifiable link between your published package and the source code that produced it. Enable it in CI:
npm publish --provenance --access public
Requirements:
Users see a "Provenance" badge on npmjs.com, proving the package was built from the linked source.
name: Publish to npm
on:
push:
tags:
- "v*"
permissions:
contents: read
id-token: write # Required for provenance
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm run build
- run: npm test
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Workflow:
git tag v1.2.0git push origin v1.2.0Your README is your landing page. Structure it for scanning.
# @your-org/agent-name
One-sentence description of what this agent does.
## Quick Start
\`\`\`bash
npx -y @your-org/agent-name
\`\`\`
## Configuration
### For Claude Desktop
Add to your `claude_desktop_config.json`:
\`\`\`json
{
"mcpServers": {
"agent-name": {
"command": "npx",
"args": ["-y", "@your-org/agent-name"],
"env": {
"API_KEY": "your-api-key"
}
}
}
}
\`\`\`
### For Claude Code
\`\`\`bash
claude mcp add agent-name -- npx -y @your-org/agent-name
\`\`\`
### Environment Variables
| Variable | Required | Description |
|-----------|----------|----------------------|
| API_KEY | Yes | Your API key |
## Tools
| Tool | Description |
|----------------|---------------------------------|
| search_items | Search items by query |
| create_item | Create a new item |
## License
MIT
Install command goes first. Before description, before badges, before the table of contents. The user's first question is "how do I use this?" Answer it in the first 5 lines.
Symptom: npx @your-org/agent downloads the package but does nothing, or throws "command not found."
Fix: Add a bin field to package.json mapping a command name to your compiled entry point.
Symptom: "Permission denied" or "exec format error" on macOS/Linux.
Fix: Ensure #!/usr/bin/env node is the first line of the file referenced by bin. Verify after TypeScript compilation.
Symptom: Package size is 5x larger than expected. Users see TypeScript files in node_modules. Potential compilation errors on users' machines.
Fix: Set "files": ["dist/"] in package.json. Run npm pack --dry-run and inspect the file list before every publish.
Symptom: Your agent crashes on Node 14 with a cryptic syntax error because you used top-level await or optional chaining.
Fix: Add "engines": { "node": ">=18.0.0" } to package.json. npm warns users if their Node version is incompatible.
Symptom: npx @your-org/agent --help takes 8 seconds because it loads a 50MB ML model on import.
Fix: Use dynamic import() for heavy dependencies. Load them only when the specific tool or command that needs them is invoked.
// Bad: loads on every invocation
import { HeavyModel } from "heavy-ml-lib";
// Good: loads only when needed
async function runAnalysis() {
const { HeavyModel } = await import("heavy-ml-lib");
// ...
}
Symptom: Tests, GitHub workflows, .env.example, and other dev files ship to users.
Fix: Use the files whitelist in package.json (preferred over .npmignore). The files field is an allowlist; only listed paths are included.
Symptom: Users report they are running an old version even after you published an update.
Fix: npx caches packages. Users can force a fresh download with npx -y @your-org/agent@latest. Document this in your troubleshooting section.
Symptom: You publish a package with stale dist/ from a previous build that does not match the current source.
Fix: Add "prepublishOnly": "npm run build && npm run test" to scripts. This runs automatically before every npm publish, ensuring the build is fresh and tests pass.
Symptom: "Cannot use import statement outside a module" or "require is not defined in ES module scope."
Fix: Set "type": "module" in package.json for ESM. Ensure tsconfig uses "module": "Node16". If you must support both ESM and CJS, use tsup's dual format output (format: ["esm", "cjs"]) and configure package.json exports field.
Symptom: Package installs fine via npm install but fails via npx because the bin entry point has an error.
Fix: After every publish, run npx -y @your-org/agent@latest in a clean environment. Consider adding this as a post-publish CI step.
"type": "module" field and module resolution algorithm.npx skills add phazurlabs/npm-agent-packaging下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer