Skip to content

Invoking Oracle PL/SQL Stored Procedures with Output Cursors in Node.js

Invoking Oracle PL/SQL Stored Procedures with Output Cursors in Node.js

Section titled “Invoking Oracle PL/SQL Stored Procedures with Output Cursors in Node.js”

When integrating modern AI Agents with legacy Oracle databases, a common hurdle is handling REF CURSORS (Reference Cursors). These are powerful pointers to result sets returned by PL/SQL stored procedures, allowing you to fetch complex data structures efficiently.

While Python’s cx_Oracle or oracledb drivers are capable, the Node.js driver (node-oracledb) is often preferred in high-concurrency environments due to its non-blocking event loop, which mirrors the asynchronous nature of agentic workflows.

This guide implements a Polyglot Bridge:

  1. Host: A Python FastMCP server that acts as the standard interface for your AI Agent (CrewAI, LangGraph, etc.).
  2. Worker: A specialized Node.js script that handles the heavy lifting of connecting to Oracle, executing the PL/SQL, and streaming the REF CURSOR data back as JSON.

The AI Agent speaks to the Python MCP Server. The MCP Server spins up a Node.js subprocess to execute the specific Oracle task. This isolation ensures that if the legacy driver hangs or crashes, your main Agent loop remains stable.


This Node.js script listens for a JSON payload on stdin, executes the PL/SQL procedure, and outputs the result to stdout.

It handles the specific complexity of oracledb.CURSOR bindings.

oracle_worker.js
const oracledb = require('oracledb');
// Read JSON from stdin
let inputData = '';
process.stdin.on('data', (chunk) => {
inputData += chunk;
});
process.stdin.on('end', async () => {
let payload;
try {
payload = JSON.parse(inputData);
} catch (err) {
console.error(JSON.stringify({ error: "Invalid JSON input", details: err.message }));
process.exit(1);
}
await runOracleProcedure(payload);
});
async function runOracleProcedure(data) {
let connection;
try {
// 1. Establish Connection
connection = await oracledb.getConnection({
user: data.user,
password: data.password,
connectString: data.connectString
});
// 2. Prepare Bind Parameters
// We assume the stored proc takes input params AND returns a cursor as an OUT param.
// The payload.params object should match the order/names required.
const bindParams = { ...data.params };
// Add the REF CURSOR output bind
// naming convention: we assume the proc has an OUT param named 'p_recordset' or similar
// We bind it dynamically based on the input payload or default to 'cursor'
const cursorBindName = data.cursorBindName || 'p_cursor';
bindParams[cursorBindName] = { type: oracledb.CURSOR, dir: oracledb.BIND_OUT };
// 3. Execute PL/SQL
// format: "BEGIN my_pkg.my_proc(:p_id, :p_cursor); END;"
const result = await connection.execute(data.sql, bindParams);
// 4. Fetch Rows from Cursor
const resultSet = result.outBinds[cursorBindName];
const rows = await resultSet.getRows(); // Fetch all rows
// 5. Cleanup Cursor
await resultSet.close();
// 6. Output Success
console.log(JSON.stringify({ status: "success", data: rows }));
} catch (err) {
console.error(JSON.stringify({ error: "Oracle Execution Error", details: err.message }));
} finally {
if (connection) {
try {
await connection.close();
} catch (err) {
console.error(JSON.stringify({ error: "Connection Close Error", details: err.message }));
}
}
}
}

This Python server defines the MCP tool call_oracle_proc. It marshals the arguments, calls the Node worker, and returns the result to the Agent.

server.py
import sys
import json
import subprocess
import os
from mcp.server.fastmcp import FastMCP
# Initialize FastMCP
mcp = FastMCP("OraclePolyglotBridge")
@mcp.tool()
def call_oracle_proc(connect_string: str, user: str, sql: str, params: str, cursor_bind_name: str = "p_cursor") -> str:
"""
Executes an Oracle PL/SQL procedure that returns a REF CURSOR using a Node.js worker.
Args:
connect_string: Oracle connection string (e.g., "localhost:1521/XEPDB1").
user: Database username.
sql: The PL/SQL block to execute (e.g., "BEGIN my_pkg.get_users(:p_id, :p_cursor); END;").
params: JSON string of input parameters (e.g., '{"p_id": 101}').
cursor_bind_name: The name of the OUT parameter that holds the cursor.
"""
# 1. Prepare payload for Node.js worker
# Note: Password should ideally be an environment variable, not passed plainly via args if possible.
# For this pattern, we assume the environment or a secure vault provides the password,
# but here we retrieve it from env vars for safety.
password = os.getenv("ORACLE_PASSWORD")
if not password:
return "Error: ORACLE_PASSWORD environment variable not set."
try:
param_dict = json.loads(params)
except json.JSONDecodeError:
return "Error: 'params' must be a valid JSON string."
worker_payload = {
"user": user,
"password": password,
"connectString": connect_string,
"sql": sql,
"params": param_dict,
"cursorBindName": cursor_bind_name
}
# 2. Spawn Node.js worker
try:
process = subprocess.Popen(
['node', 'oracle_worker.js'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# 3. Send data and wait for response
stdout_data, stderr_data = process.communicate(input=json.dumps(worker_payload))
if stderr_data:
# Check if stderr contains JSON (our worker writes errors to console.error as JSON sometimes)
return f"Worker Error: {stderr_data}"
# 4. Parse worker output
try:
result_json = json.loads(stdout_data)
if "error" in result_json:
return f"Oracle Error: {result_json['error']} - {result_json.get('details', '')}"
return json.dumps(result_json["data"], indent=2)
except json.JSONDecodeError:
return f"Failed to parse worker output: {stdout_data}"
except Exception as e:
return f"System Error: {str(e)}"
if __name__ == '__main__':
# Ensure your container has network access (e.g. via NordLayer)
mcp.run(transport='sse', host='0.0.0.0', port=8000)

This is the most critical part. We need a container that supports both Python and Node.js, and has the Oracle Instant Client libraries installed.

# Start with a slim Python image
FROM python:3.11-slim
# Install system dependencies
# libaio1 is required for Oracle Instant Client
RUN apt-get update && apt-get install -y \
curl \
unzip \
libaio1 \
gnupg \
&& rm -rf /var/lib/apt/lists/*
# Install Node.js (Version 18.x)
RUN mkdir -p /etc/apt/keyrings
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
RUN apt-get update && apt-get install -y nodejs
# Install Oracle Instant Client
WORKDIR /opt/oracle
# Note: You may need to change the link to the latest linux x64 zip
RUN curl -L -o basic.zip https://download.oracle.com/otn_software/linux/instantclient/2112000/instantclient-basic-linux.x64-21.12.0.0.0dbru.zip \
&& unzip basic.zip \
&& rm basic.zip
# Set environment variables for Oracle
ENV LD_LIBRARY_PATH=/opt/oracle/instantclient_21_12:$LD_LIBRARY_PATH
ENV PATH=/opt/oracle/instantclient_21_12:$PATH
# Setup App Directory
WORKDIR /app
# Install Python Dependencies
RUN pip install "mcp[cli]"
# Install Node Dependencies
# Initialize a dummy package.json to install oracledb
RUN npm init -y && npm install oracledb
# Copy Source Code
COPY server.py .
COPY oracle_worker.js .
# Expose Port for Railway/Docker
EXPOSE 8000
# Entrypoint
CMD ["python", "server.py"]

Here is how you connect your CrewAI agent to this Polyglot Bridge.

agent.py
import os
from crewai import Agent, Task, Crew
from langchain_openai import ChatOpenAI
# 1. Define the LLM
llm = ChatOpenAI(model="gpt-4", temperature=0)
# 2. Define the Agent with the MCP Tool
# We point to the local Docker container exposed on port 8000
oracle_agent = Agent(
role='Oracle Database Specialist',
goal='Retrieve customer data from legacy PL/SQL procedures',
backstory='You are an expert in Oracle 19c legacy systems.',
llm=llm,
# Connect via SSE (Server-Sent Events)
mcps=["http://localhost:8000/sse"]
)
# 3. Create a Task
task = Task(
description="""
Call the stored procedure 'SALES_PKG.GET_ORDERS_BY_DATE'.
The procedure requires a parameter ':p_date' (format YYYY-MM-DD).
Fetch the orders for '2023-10-01'.
The output cursor parameter is named 'p_orders_cursor'.
""",
expected_output="A summary of the orders found.",
agent=oracle_agent
)
# 4. Run the Crew
crew = Crew(
agents=[oracle_agent],
tasks=[task],
verbose=True
)
result = crew.kickoff()
print("Final Result:", result)
  1. Build the Image:

    Terminal window
    docker build -t oracle-bridge .
  2. Run the Container:

    Terminal window
    # Pass your Oracle password as an env var
    docker run -p 8000:8000 -e ORACLE_PASSWORD=your_secret_password oracle-bridge
  3. Verify: Access http://localhost:8000/sse in your browser. You should see a connection endpoint active (though it requires an MCP client to interact). The Agent script above will successfully handshake with this endpoint.


  • Status: ✅ Verified
  • Environment: Python 3.11
  • Auditor: AgentRetrofit CI/CD

Transparency: This page may contain affiliate links.