Skip to main content
This guide covers setting up a development environment and contributing to mcp-server-db2i.

Prerequisites

  • Node.js 22 or higher (required for --env-file flag)
  • unixODBC and the IBM i Access ODBC Driver for the default odbc driver (see Database Drivers)
  • JDK 11 or higher, optional. Only needed to build and run the jt400 driver: npm install builds its Java bridge when a JDK is present and skips it otherwise. The tests mock both drivers, but npm run typecheck needs both packages installed, and CI builds both
  • npm or yarn
  • Access to an IBM i system (for integration testing)

Getting Started

1. Clone the Repository

2. Install Dependencies

nvm use reads .nvmrc and switches to Node 22. fnm and mise read the same file. .npmrc sets engine-strict, so npm install fails on an older Node instead of building the native modules for the wrong version. If you switch Node versions later, run npm rebuild so the native module matches.

3. Configure Environment

Create a .env file:

4. Run in Development Mode

This uses tsx to run TypeScript directly. It does not restart on changes.

Available Scripts

Project Structure

Testing

Run All Tests

Watch Mode

Test Coverage

Integration Tests

The tests in tests/integration/ run the MCP server end to end over an in-memory transport, with the database driver mocked. They need no IBM i connection and run as part of npm test. To run only them:

Validating tool files

validate-tools runs the startup checks on YAML tool files and then exits. It does not open a database connection and does not need DB2I_HOSTNAME.
QUERY_ALLOWED_SCHEMAS and DB2I_SCHEMA are applied when they are set. --connect also runs each statement through QSYS2.PARSE_STATEMENT on ibmi.example.com (or whichever host DB2I_HOSTNAME names). That path needs credentials. A missing PARSE_STATEMENT is a failure.
A CI job can run the check with no secrets:
The command exits 0 when every file passes and 1 when any file fails.

Code Style

The project uses ESLint with TypeScript rules. Format code before committing:

Conventions

  • Use TypeScript strict mode
  • Prefer async/await over callbacks
  • Use structured logging with createChildLogger
  • Document public functions with JSDoc comments
  • Keep functions focused and testable

Adding a New Tool

  1. Create the tool function in src/tools/:
  1. Add the name to TOOL_NAMES in src/config.ts, so MCP_TOOLS_ENABLED and MCP_TOOLS_DISABLED accept it.
  2. Register the tool in createServer() in src/server.ts. withToolHandler resolves the target system, applies the rate limit, writes the audit line, and formats the result:
  1. Add tests in tests/:

Database Layer

Connection Pool

The db/connection.ts module manages connection pools. It does not know which driver it uses: db/driver.ts defines the DbPool and DbDriver interfaces, and db/drivers/jt400.ts, db/drivers/odbc.ts and db/drivers/mapepire.ts implement them. The driver module is imported on first use, and a pool connects on its first query. tests/db/drivers.contract.test.ts runs every implementation against fakes. The mapepire driver keeps its own job pool (JobPool), which gets jobs from a JobFactory. Only the SSH factory exists today. A daemon transport would add a second factory and reuse the pool. tests/db/mapepirePool.test.ts tests the pool against a fake factory, and db/drivers/sshHostKey.ts holds the host key check.

Driver Errors

When Db2 rejects a statement, a driver throws a DbError (db/driver.ts) instead of a plain Error. Its message keeps the [SQLSTATE] text shape, and it carries sqlstate and sqlcode: Other failures, such as a lost connection or a QueryTimeoutError, stay plain errors. db/connection.ts passes a DbError to explainSqlError in db/sqlErrorInfo.ts. It looks the SQLCODE up with SYSTOOLS.SQLCODE_INFO on the same pool, splits the second-level text into cause and recovery, and throws a DatabaseQueryError with those in details:
  • Each system and SQLCODE is looked up once and cached for the life of the process. A failed lookup is not cached, except when the function does not exist on that system; then the system is not asked again.
  • The lookup has a 5 second limit. If it fails for any reason, the error is reported as the driver gave it.
  • The second-level text keeps its &1 placeholders. The first-level message in error has the values.
  • With the JDBC option errors=full, JT400 and Mapepire put the second-level text in the message, with the values filled in. That text is split instead, and no lookup runs.
Tools spread sqlErrorFields(error) into their error result, and withToolHandler in server.ts returns sqlstate, sqlcode, cause and recovery in structuredContent, with cause and recovery also in the text. Rejections by the SQL validator, the schema allowlist or masking do not come from Db2 and carry none of these fields.

Pools

  • Global pool: For stdio transport
  • Session pools: For HTTP transport (per-authenticated user)

Adding Queries

Add new query functions in src/db/queries.ts:

HTTP Transport

Adding Endpoints

Add routes in src/transports/http.ts:

Authentication

HTTP endpoints use Bearer token authentication:

Debugging

Enable Debug Logging

VS Code Launch Configuration

Create .vscode/launch.json:

Contributing

1. Fork the Repository

Fork on GitHub and clone your fork:

2. Create a Branch

3. Make Changes

  • Write code
  • Add tests
  • Update documentation if needed
  • Run lint and tests

4. Commit

Follow conventional commit format:

5. Push and Create PR

Then create a Pull Request on GitHub.

Pull Request Guidelines

  • Use a Conventional Commits PR title (feat:, fix:, ci:, …). That title becomes the squash-commit subject.
  • Squash-merge only. Merge commits make Release Please list the same change twice in CHANGELOG.md.
  • Describe the changes clearly
  • Reference any related issues (Fixes #123 in the PR body)
  • Ensure all tests pass
  • Update documentation as needed
  • Keep changes focused and atomic

Release Process

Releases are automated via GitHub Actions using Release Please:
  1. Squash-merged conventional commits on main are analyzed
  2. A release PR is automatically created/updated with the version bump and CHANGELOG.md
  3. Merging the release PR tags vX.Y.Z, creates the GitHub release, runs CI on the tagged commit, and publishes mcp-server-db2i to npm via OIDC trusted publishing. Other pushes to main only update the release PR: branch protection has already built and tested them
  4. CI and npm publish both run on Node 22. Publish installs the latest npm so trusted publishing works. The registry publish retries up to 20 times, 30 seconds apart (about 10 minutes), because a just-published npm version can still 404.
To retry publishing an already-tagged release (for example after an npm outage), run the Release workflow with workflow_dispatch and set tag to vX.Y.Z. That path skips Release Please and republishes the existing tag. ci: commits appear under CI/CD in the next version’s changelog but do not bump the version by themselves.

Getting Help

  • Open an issue for bugs or feature requests
  • Check existing issues before creating new ones
  • Provide reproduction steps for bugs
  • Include relevant logs and configuration