2. Tech Stack
Framework & Language
Core Dependencies
Which
package.jsonfield a new package goes in. Everything here exceptplaywrightis bundled, so it is only needed to build — it belongs indevDependencies.dependenciesis reserved for packages that stayexternalintsup.config.tsand therefore have to be installed alongside the published bundle;playwrightis the only one. Putting a bundled package independenciesstill works, but every consumer then downloads a copy they never load.
Documentation Site
scripts/readme-docs-plugin.ts — Custom Rspress plugin that renders the generated root README as the /api documentation page
Build & Bundling
Bundling config (tsup.config.ts):
- ESM + CJS format
- Code splitting with
splitting: true, andtreeshake: trueso rollup performs the CJS conversion - Every dependency is inlined except
playwright(noExternal: [/^(?!playwright)/]) playwrightstays external — it resolves browser drivers from its own package directory at runtime and cannot be bundled- The ESM output carries a
requireshim in its banner (esbuildOptions) — see below - Minify enabled
defineinjectspackage.json'sversion(read bysrc/common/constants.ts, reported in the MCPinitializeresponse) and the build environment's TMDB credentials (see below)- 3 entry points:
src/index.ts,src/server.ts,src/cli.ts - Post-build: shebangs added to the
server/clibundles and empty chunks removed. The build never touchesREADME.mdorskills/<bin>/SKILL.md— those are hand-written
Why treeshake: true Is Not Optional
Without it, tsup converts the CJS output by running sucrase over the already-minified esbuild
bundle. Sucrase rewrites return(await x)?.y ?? z — the shape minification produces — into
returnawait _asyncNullishCoalesce(...), with the space dropped, and every dist/*.cjs fails to
parse. treeshake: true hands CJS code splitting to rollup instead and the sucrase pass never
runs. The Smoke test the built bundles step in CI loads the output so this cannot ship unnoticed.
Why the ESM Output Needs a require Shim
noExternal inlines every dependency, and some of them ship as CommonJS — systeminformation
calls require('os') and require('child_process') inside its own modules. esbuild leaves those
calls as "use require if the runtime has one, otherwise throw", and ESM has none, so importing the
bundle died on Dynamic require of "os" is not supported before any tool ran. esbuildOptions adds
a banner to the ESM format only that builds a require from node:module:
The CJS output already has a real require, and declaring another one there would collide, which is
why the banner is keyed off context.format. Marking the builtins external does not help —
noExternal: [/^(?!playwright)/] matches bare names like os too and wins over external.
Build-Time TMDB Credentials
tsup.config.ts reads TMDB_API_KEY / TMDB_ACCESS_TOKEN from the build environment (and from a
local .env) and replaces the __TMDB_API_KEY__ / __TMDB_ACCESS_TOKEN__ / __TMDB_SEAL_SECRET__
identifiers in src/tmdb/embedded.ts with string literals. A build made with a credential runs the
movie tools without one being supplied at startup; a build made without one behaves exactly as
before and reads the environment at call time. The build prints which variables it embedded.
The value is sealed with AES-256-GCM under a secret generated fresh for each build, and
src/tmdb/embedded.ts owns both halves of the format so the build and the runtime cannot drift
apart. This is obfuscation, not encryption — the secret ships in the same bundle, so anyone
holding the bundle can open the value. What it buys is narrow and worth stating plainly:
- the key is not a greppable string in
dist/, so automated secret scanners and casual inspection do not surface it - pasting a fragment of the bundle into an issue or a log does not leak the credential
It does not make the credential safe to distribute.
The Build step in .github/workflows/release.yml passes the repository secrets, so the package
published to npm carries a TMDB credential and npx @julong/mcp-kit works with nothing in the
client config. Everyone who installs the package can recover that key, so the secret behind it must
be a dedicated key issued for public use — not a personal one, and not one shared with anything
else. If it is abused, issue a new one at TMDB and replace the repository secret; the next release
picks it up. A Verify the credential made it into the bundle step fails the release if the secret
is missing, so a keyless package cannot be published by accident.
ci.yml passes the same secrets so the embedding path is exercised on every run, but it asserts
nothing — pull requests from a fork receive no secrets and correctly produce a build without one.
Code Quality & Testing
Agent Tests (optional)
src/agent.test.ts calls a real LLM and the real portals. It is skipped by the default pnpm test and runs only via pnpm test:agent.
CI/CD
Release Rules (release-please-config.json + .release-please-manifest.json)
Versioning is driven by Conventional Commits. Instead of releasing immediately on push to main, release-please opens/updates a release PR with version bumps and CHANGELOG entries; merging that PR creates the tag, GitHub Release, and triggers npm publish. The current version is tracked in .release-please-manifest.json.
Key release-please-config.json options: release-type: "node", include-component-in-tag: false, include-v-in-tag: true (tags use the v<version> format, e.g. v1.0.0), with a packages map covering only the repository root ({ ".": {} }).
Currently Unused
- Monorepo tooling: No Turborepo or pnpm workspace (single repository, single package)
- UI framework: React, Next.js, etc. not used (MCP server only)
- Database: Not used
- HTTP server: Not used (MCP stdio transport only)