MCPDBWizard

Writing  · 

What an agent actually sees when your PL/SQL becomes tools

“Expose your procedures as MCP tools” sounds like a small job until you look at what PL/SQL is allowed to do. This post is about the gap between the two, because it is the part people underestimate and then abandon halfway through.

MCP tools are a narrow shape

An MCP tool has a name, a description, and a JSON Schema for its arguments. It takes a JSON object and returns a result. That is close enough to a function call in most languages that wrapping one is trivial.

PL/SQL is not most languages.

What a procedure can actually have

A stored procedure can take any number of OUT and IN OUT parameters. There is no single return value to map onto a result. A procedure with three OUT parameters is normal, and it has no analogue in a REST-shaped mental model.

Parameters can be records — including records nested inside records. They can be collections, %ROWTYPEs matching a table’s structure, REF CURSORs both strong and weak, package-level types, and overloads where the same name means several different signatures.

And then Oracle’s own data dictionary describes these things differently between versions. The view you query to find out a procedure’s arguments does not return the same rows on 12c as it does on 19c, because later versions stopped expanding nested types and left you to reconstruct them from other views. If you write your introspection against one version, it silently produces less on another — not an error, just fewer parameters than the procedure really has.

That last one is genuinely nasty, and it is why we regression-test the generator against six live Oracle instances from 12.1 to 26ai rather than one.

What the crossing looks like

Once it is derived, the mapping is unremarkable, which is the goal:

oracle:  PROCEDURE order_summary(p_customer IN  NUMBER,
                                 p_totals   OUT summary_rec,
                                 p_lines    OUT SYS_REFCURSOR)

tool:    order_summary { "p_customer": <number> }
      -> { "p_totals": { "orderCount": 12, "value": 4210.55 },
           "p_lines":  [ { "sku": "AB-1", "qty": 3 }, ... ] }

A record crosses as a JSON object. A REF CURSOR crosses as an array of row objects. A DATE becomes an ISO-8601 string, RAW and binary vectors become base64, a CLOB becomes text.

The important part is the schema. The tool advertises p_customer as a number because the procedure says it is one. The agent is told what the parameters are, rather than guessing from the tool’s name and a sentence of prose — which is the difference between a model calling your procedure correctly and a model calling it plausibly.

Why this is not a weekend project

You can hand-write a wrapper for one procedure in an afternoon. The trouble is the second hundred, and the fact that every one of them is a place where a nested record can be quietly flattened into an untyped blob and nobody notices until a caller binds all-nulls.

We generate them instead, from what the database says the signature is. The work is dull and there is a great deal of it, which is precisely the argument for not doing it by hand.

How do I get MCP DB Wizard?

It’s available from our GitHub repo as a Docker image. There is a worked example of generated output in the repo if you want to see what it emits before installing anything.