·7 min read

Eight months of an MCP server for Db2 for i

In January the server looked up table definitions so the AI would stop guessing. At 2.12 it checks its SQL against the IBM i, reads your business definitions, and lets colleagues sign in as themselves.

In January I wrote about an MCP server I built because I was tired of pasting table definitions into AI chats. The fix was obvious once I saw it. The information already lives in the system, so let the AI look it up. Seven tools, all read-only, one job.

It worked. The model stopped inventing column names. Then it started getting something else wrong, and that's what the next eight months were about.

The column was right, the answer was wrong

Take the question "how many open orders do we have for customer 1001?" With the schema in hand, the model finds ORDERHDR, finds the STATUS column, and writes a clean query. It runs. It returns a number.

The number is plausible and wrong. At this company an open order excludes the ones on credit hold, and STATUS = 'R' means released to the warehouse, which still counts as open. None of that is in the catalog. It lives in the heads of the people who built the reports, and in the Excel workbooks that query the ERP over ODBC.

Knowing the columns fixes syntax. It does nothing for meaning. And a wrong number that looks right is worse than an error, because nobody goes looking for it.

Let the IBM i check the work

The syntax side improved first, and the tool that helped most was the IBM i itself.

validate_query sends a statement to the IBM i to be parsed and checks every table and column name against the catalog, without running it. A generic SQL parser can't do that. It doesn't know your catalog, and it doesn't know every corner of Db2 for i syntax either. The system knows both.

So the loop changed from "run, fail, guess, run again" to "check, fix, run once". When Db2 does reject something, the error now carries the cause and recovery text from the message's second-level help, the same text you'd see pressing F1 on a green screen. Models fix a query much faster when they're told why it failed in Db2 for i's own words, instead of reading a bare SQL0204.

Write down what "open" means, once

For meaning, the answer turned out to be low-tech: a YAML file.

Next to each table you write short notes in business words. What the table is ("sales order header, one row per order"). What the codes mean ("O is open, R is released, C is closed"). What belongs together ("the lines live in ORDERLINES, matched on ORDERNO"). The model reads these every time it touches the table.

For the questions that matter most, you can go a step further and take the SQL away from the model entirely. A query the business already trusts, say the one behind the open orders workbook, becomes a named tool. The model fills in the customer number and runs your query exactly as written. The answer matches the workbook because it is the workbook's query.

This has become my favorite part of the project. The reports and spreadsheets a company already has hold years of careful decisions about what the numbers mean. They aren't something for an AI to replace. They're the best context it could ask for.

Read-only, in layers

In January, "read-only" meant a check that refused UPDATE, DELETE and INSERT. It caught the obvious cases. Today it's four separate things, and each one would stop a write on its own:

  • The connection opens read-only. Db2 itself refuses a write that slipped past everything else.
  • A SQL validator parses every statement into a syntax tree and walks it, including functions with side effects tucked inside a SELECT.
  • A library allowlist (QUERY_ALLOWED_SCHEMAS) rejects any query that touches a library you didn't list, and any SQL the parser can't read.
  • QUERY_TIMEOUT cancels long queries on the IBM i, not only in the client. The IBM i keeps running a statement after its client disconnects, so "the client gave up" never meant "the system stopped".

I try not to call any of this "secure". What I can say precisely is what's enforced, and where.

One execute_query call from the agent to Db2 for i and back: OAuth token to IBM i user over HTTP, SQL validator, library allowlist, PARSE_STATEMENT on the IBM i, FETCH FIRST n ROWS ONLY, the SELECT on a read-only connection with QUERY_TIMEOUT, column masking and audit log, then rows as JSON or a CSV or XLSX link

No Java, and still nothing on the IBM i

The most common complaint about setup was fair. Node, plus a Java runtime, plus a JDBC driver, is a lot to ask of a tool you want to try in five minutes. Someone on r/IBMi compared it with MCP servers you just install and run, and they had a point.

The IBM i Access ODBC driver is now the default, so the machine running the server needs no Java. JT400 still works if you prefer it. For systems where only SSH is open to the network, there's a Mapepire driver that starts its server inside the SSH session. I had dropped Mapepire earlier because it needed a server installed on the IBM i. Its SSH mode changed that trade-off, so it came back. (It does need sshd and Java on the IBM i, and the first query in a session takes a few seconds while the JVM starts.)

Across all three drivers, the rule from January holds. Nothing for an administrator to install on the IBM i.

Colleagues sign in as themselves

The first version was one person on one laptop. The useful cases have more people in them.

With OAuth turned on, the server runs its own sign-in. A colleague adds one URL as a connector in Claude, Cursor or Claude Code. A sign-in page opens, and they log in with their own IBM i user profile. Queries run with their authority, so object authority and exit programs apply as usual. They see what they could already see, and nothing more. Someone sets the server up once. Everyone else adds a URL.

claude.ai connects from Anthropic's side, so the server has to be reachable over HTTPS. That puts a sign-in page on the internet, and it deserves an IP allowlist in front of it. The docs include an example.

The whole list, not a summary

Some questions need every row: all open orders for one customer, every invoice more than 60 days overdue. A chat answer can only summarize those.

export_query writes the full result to a CSV or Excel file. Over stdio that's a path on disk. Over HTTP it's a download link that expires after 15 minutes. The model only sees the row count and a few sample rows, and the file comes from your own server with masked columns still masked.

Where the ideas came from

Reading back through the changelog, I'm struck by how few of these ideas started with me. The ODBC default came from that Reddit complaint. A port clash spotted on r/IBMi shaped the Mapepire rewrite. People on the IBMiOSS forum asked the questions that set the order of the early releases.

In the first week, someone also pointed me to IBM's own MCP server for IBM i. It's a different starting point: it runs on the Mapepire server on the IBM i and ships ready-made SQL tool sets, agent examples and a CLI. If that fits your setup, it's worth a look. This one stays focused on needing nothing on the IBM i and on reads only.

What eight months changed

In January I thought the job was to let the AI look things up instead of guessing. That turned out to be half of it.

The other half is giving it something worth looking up. A system that checks its SQL before anything runs. Notes from the people who know what "open" means. Queries the business already trusts. The model didn't get much smarter about Db2 for i in eight months. It got better at asking the system, and the people, who already knew.

It's at version 2.12 now, with 20 tools. Still MIT licensed, still free.

npx mcp-server-db2i@latest

Links

Share
Roni Ström

Written by

Roni Ström

Founder

More from the blog

See all