Prerequisites
- Node.js 22 or higher (required for
--env-fileflag) - unixODBC and the IBM i Access ODBC Driver for the default
odbcdriver (see Database Drivers) - JDK 11 or higher, optional. Only needed to build and run the
jt400driver:npm installbuilds its Java bridge when a JDK is present and skips it otherwise. The tests mock both drivers, butnpm run typecheckneeds 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
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 intests/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.
Code Style
The project uses ESLint with TypeScript rules. Format code before committing:Conventions
- Use TypeScript strict mode
- Prefer
async/awaitover callbacks - Use structured logging with
createChildLogger - Document public functions with JSDoc comments
- Keep functions focused and testable
Adding a New Tool
- Create the tool function in
src/tools/:
-
Add the name to
TOOL_NAMESinsrc/config.ts, soMCP_TOOLS_ENABLEDandMCP_TOOLS_DISABLEDaccept it. -
Register the tool in
createServer()insrc/server.ts.withToolHandlerresolves the target system, applies the rate limit, writes the audit line, and formats the result:
- Add tests in
tests/:
Database Layer
Connection Pool
Thedb/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 aDbError (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
&1placeholders. The first-level message inerrorhas 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.
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 insrc/db/queries.ts:
HTTP Transport
Adding Endpoints
Add routes insrc/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
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 #123in 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:- Squash-merged conventional commits on
mainare analyzed - A release PR is automatically created/updated with the version bump and
CHANGELOG.md - Merging the release PR tags
vX.Y.Z, creates the GitHub release, runs CI on the tagged commit, and publishesmcp-server-db2ito npm via OIDC trusted publishing. Other pushes tomainonly update the release PR: branch protection has already built and tested them - 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.
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