10. Project-Specific Commands

Run all commands from the repository root. This project uses pnpm version 9.12.1.

Environment Setup

# Set the Node.js version (per .nvmrc)
nvm use

# Install dependencies
pnpm install

# Install from the lockfile
pnpm install --frozen-lockfile   # CI

Local Development

# Build (tsup — 3 entries: index / server / cli)
pnpm build

# Dev mode (watch)
pnpm dev

# Clean (remove dist, coverage, doc_build, node_modules)
pnpm clean

# Lint
pnpm lint

# Format
pnpm format
pnpm format:check

Type Checking

pnpm typecheck        # tsc --noEmit

Documentation

README.md and skills/<bin>/SKILL.md are hand-written — there is no generation command. Editing a tool means editing both files by hand. See 08-file-creation-rules for the checklist.

Documentation Site (Rspress)

# Start the local docs dev server
pnpm docs:dev

# Static build of the docs site (outputs to doc_build/)
pnpm docs:build

The docs site is driven by the root rspress.config.ts. Static docs are the .md files under docs/, and the tools API page (/api) is generated by readme-docs-plugin from the root README.md.

Tests

# Full test run (agent tests are skipped automatically)
pnpm test

# Agent tests only (requires an LLM and network access)
pnpm test:agent

Coverage reports are written to coverage/.

Verification (same order as CI)

# 1. Lint
pnpm lint

# 2. Format check
pnpm format:check

# 3. Type check
pnpm typecheck

# 4. Test
pnpm test

# 5. Build
pnpm build

Releases are handled by release-please. There is no local release dry-run command — merging the release PR triggers the real release.

Running the MCP Server

# Run directly with npx (published version)
npx @julong/mcp-kit

# Run the local build — the same thing, as a script
pnpm build && pnpm start

# Run one tool from the local build
pnpm start:cli nowPlayingMoviesTool

# Debug with MCP Inspector
pnpm inspect

# MCP Client Config
# {
#   "mcpServers": {
#     "@julong/mcp-kit": {
#       "command": "npx",
#       "args": ["-y", "@julong/mcp-kit"],
#       "env": { "TMDB_API_KEY": "<your-key>" }
#     }
#   }
# }

pnpm start is a stdio server: it waits on stdin and prints nothing on its own. That is the normal state — an MCP client drives it. To see it answer by hand, pipe JSON-RPC frames in:

printf '%s\n%s\n%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' \
  '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
  | pnpm -s start

Use the WHITE_FN / BLACK_FN env vars to restrict which tools are exposed (comma-separated, matched by tool name). A non-empty WHITE_FN acts as an allowlist.

Where Credentials Come From

The server and the CLI read TMDB_API_KEY / TMDB_ACCESS_TOKEN from the process environment, in this order of precedence:

  1. Variables already set in the environment — the env block of an MCP client config, or an inline prefix such as TMDB_API_KEY=... pnpm start.
  2. A .env file in the current working directory, loaded at startup.
  3. A credential embedded into the bundle at build time (see below).

A value already present in the environment always wins over .env, and either of them wins over the embedded credential. The environment is taken as a whole: as soon as it supplies TMDB_API_KEY or TMDB_ACCESS_TOKEN, the embedded pair is ignored entirely rather than filling in the other half — a bearer token is preferred over an API key, so mixing the two sources would silently discard the key you passed in. When no source supplies a credential, every movie tool answers with a single Error: ... line naming both variables.

Building a Bundle That Carries the Credential

pnpm build reads TMDB_API_KEY / TMDB_ACCESS_TOKEN from the build environment and writes the value into the bundle, so the resulting server needs no credential to start:

# From .env in the repository root
pnpm build

# Or for one build only
TMDB_API_KEY=<your-key> pnpm build

# Now the movie tools work with nothing in the environment
node dist/cli.js nowPlayingMoviesTool

The build prints which variables it embedded:

TMDB credentials embedded in the bundle (sealed, not secret): TMDB_API_KEY
TMDB credentials not embedded — the bundle reads the environment at run time

The value is sealed with AES-256-GCM rather than written in plain text, so it is not a greppable string in dist/. The secret that opens it ships in the same bundle, which makes this obfuscation and not encryption — anyone holding the bundle can recover the key. Treat any build made this way as carrying a secret: do not commit it, do not attach it to a release, and do not npm publish it. To rotate or remove the value, rebuild without the variable set.

In CI the same two variables come from repository secrets, wired into the Test step of .github/workflows/ci.yml:

- name: Test
  run: pnpm test
  env:
    TMDB_API_KEY: ${{ secrets.TMDB_API_KEY }}
    TMDB_ACCESS_TOKEN: ${{ secrets.TMDB_ACCESS_TOKEN }}

Add the secret under Settings → Secrets and variables → Actions. With it present, pnpm test also exercises the tests that call TMDB for real; without it those tests skip and the rest of the suite still passes. Pull requests opened from a fork never receive secrets, so they take the skip path. Set SKIP_TMDB_LIVE_TESTS=1 to skip them even when a credential is available.

The Build steps in both workflows receive the same two secrets, because a credential handed to the build is written into the bundle. In release.yml that is deliberate: the package published to npm carries the credential, so npx @julong/mcp-kit needs nothing in the client config. The consequence is that every install can recover the key — see Build-Time TMDB Credentials for what that means for which key belongs in the secret.

An MCP client spawns the server with a working directory you do not control, so .env is only reliable when you start the server yourself. For a client config, use its env block.

Connecting from LM Studio

LM Studio reads the same mcpServers shape (Program → Install → Edit mcp.json). Point it at the built entry with an absolute path and pass the credential through env:

{
  "mcpServers": {
    "mcp-kit": {
      "command": "node",
      "args": ["/absolute/path/to/mcp-kit/dist/server.js"],
      "env": { "TMDB_API_KEY": "<your-key>" }
    }
  }
}

Run pnpm build first — the path above is the build output, not the source.

Running CLI Tools

# List available tools
npx mcp-kit-cli

# Run an exchange-rate tool — requires `npx playwright install chromium` once
npx mcp-kit-cli exchangeRatesTool
npx mcp-kit-cli exchangeRatesTool '["naver","daum"]' '["CNY","JPY"]'
npx mcp-kit-cli exchangeRateTool google EUR

# Run the CLI from the local build
node dist/cli.js exchangeRateTool naver CNY

Running the Movie Tools

The TMDB tools need a credential — TMDB_API_KEY (v3 API key) or TMDB_ACCESS_TOKEN (read access token). Issue either at https://www.themoviedb.org/settings/api. The examples below pass it in the environment; a build that already carries one (see "Building a Bundle That Carries the Credential") runs the same commands with no prefix. No browser is involved, so Playwright is not needed.

# Build once, then run the local CLI
pnpm build

# Now playing / upcoming (defaults: language=ko-KR, region=KR)
TMDB_API_KEY=<your-key> node dist/cli.js nowPlayingMoviesTool
TMDB_API_KEY=<your-key> node dist/cli.js upcomingMoviesTool

# Movies beyond the Korean release calendar
TMDB_API_KEY=<your-key> node dist/cli.js nowPlayingMoviesTool en-US US

# Recommendations from a reference movie (arguments: title, movieId, genre, ...)
TMDB_API_KEY=<your-key> node dist/cli.js movieRecommendationsTool 인터스텔라
TMDB_API_KEY=<your-key> node dist/cli.js movieRecommendationsTool null 157336

# Recommendations with no reference movie — already released, popularity first
TMDB_API_KEY=<your-key> node dist/cli.js movieRecommendationsTool null null 액션

Passing null keeps a later argument positional — the CLI maps arguments to schema fields in order. Without a credential every movie tool answers with a single Error: ... line instead of throwing.

# Drive the MCP server itself over stdio
TMDB_API_KEY=<your-key> npx @modelcontextprotocol/inspector node dist/server.js

Agent Integration Test (MCP + LLM)

Runs the MCP server under an LLM agent (deepagents + langchain) and checks the tool results end to end. The whole cycle is written to src/common/.log/<taskId>.log.

# 1. Copy the env template and fill in your LLM credentials
cp .env.example .env

# 2. Build the MCP server bundle the agent will spawn
pnpm build

# 3. Run the agent as assertions
pnpm test:agent

# 4. Read the full run log
less src/common/.log/rstest-agent-all.log

pnpm test:agent runs src/agent.test.ts, which asserts that the MCP server exposes both tools, that the LLM actually invoked one of them, that the tool arguments match the request scope, and that the numbers in the final answer came from the tool result rather than from the model. Plain pnpm test skips the file unless RUN_AGENT_TESTS is set.

The test is the only place that wires an agent: @/common/agent supplies the model, the logging and nodeMcpServer(), while the test supplies the MCP server to spawn and the prompts. The published bundle stays a pure tool provider and never imports langchain.

Env varPurpose
SILICONFLOW_URLOpenAI-compatible base URL (e.g. https://api.siliconflow.cn/v1)
SILICONFLOW_MODELModel name (e.g. Qwen/Qwen3-8B)
OPENAI_API_KEYAPI key — also accepts SILICONFLOW_API_KEY or API_KEY

The agent needs network access, a valid API key, and Playwright's browser (npx playwright install chromium). It is not part of pnpm test.

Git Commits

# feat: new feature (minor release)
git commit -m "feat(scope): add new feature"

# fix: bug fix (patch release)
git commit -m "fix(scope): resolve null reference"

# BREAKING CHANGE (major release)
git commit -m "feat(scope)!: rename public API
"
# or declare BREAKING CHANGE in the footer
git commit -m "feat(scope): rename public API

BREAKING CHANGE: oldName has been renamed to newName"

# Docs/config changes (no release)
git commit -m "docs: update README"
git commit -m "chore: update dependencies"
git commit -m "refactor(scope): restructure module"

Publishing

# 1. Push the work branch and open a PR against main
git push -u origin <branch>
gh pr create --base main

# 2. CI runs on the PR — merge once it is green

# 3. The merge triggers release-please, which opens/updates a release PR
#    carrying the version bump + CHANGELOG

# 4. Merging that release PR performs the actual release:
#    a tag (v<version>) and GitHub Release are created, then the publish job
#    runs `pnpm publish`

Bundle Inspection

# Inspect the built bundle
ls dist/

# Inspect the skill docs
ls skills/

# Inspect the docs site build output
ls doc_build/

Troubleshooting

# pnpm store issues
pnpm store prune
rm -rf node_modules
pnpm install

# Full clean
pnpm clean && pnpm install && pnpm build

# Exchange lookups failing because the Playwright browser is missing
npx playwright install chromium

MCP error -32000: Connection closed

This error means the client's stdio pipe to the server is gone — the server process exited. The server writes every log line to stderr, so read the client's MCP log for this server first.

Line in stderrWhat it means
(nothing at all)The process never started. Check that npx is on the PATH the client launches with, and that Node is 20 or newer.
[mcp-kit] ready on stdio (v…, node …) missingStartup failed. The [mcp-kit] server error: line right after it carries the cause.
[mcp-kit] uncaughtException: / unhandledRejection:An error escaped a tool handler. The server stays up and keeps serving; the line names the cause.
[mcp-kit] <tool> start with no matching done in …msThe process died mid-call. That call is the one to reproduce — it is the last thing the server was doing.
[mcp-kit] stdin closed by client — shutting downNormal shutdown — the client closed the pipe.
[mcp-kit] no tools registeredWHITE_FN / BLACK_FN filtered every tool out.

Reproduce the handshake outside the client to separate a client problem from a server problem:

echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}' \
  | npx -y @julong/mcp-kit

One call to exchange_rates is budgeted at min(timeoutMs × (currencies + 1), 45s) and returns whatever it has read when the budget runs out — currencies it could not reach in time come back as null. The 45-second cap is deliberate: an MCP client waits 60 seconds for one request by default, and a call that runs past that returns no partial result at all. Adding a currency lengthens a call but can no longer push it over that line.