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”The “Polyglot Bridge” Pattern
Section titled “The “Polyglot Bridge” Pattern”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:
- Host: A Python FastMCP server that acts as the standard interface for your AI Agent (CrewAI, LangGraph, etc.).
- 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.
Architecture
Section titled “Architecture”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.
1. The Worker: oracle_worker.js
Section titled “1. The Worker: oracle_worker.js”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.
const oracledb = require('oracledb');
// Read JSON from stdinlet 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 })); } } }}2. The Host: server.py
Section titled “2. The Host: server.py”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.
import sysimport jsonimport subprocessimport osfrom mcp.server.fastmcp import FastMCP
# Initialize FastMCPmcp = 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)3. Dockerfile
Section titled “3. Dockerfile”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 imageFROM python:3.11-slim
# Install system dependencies# libaio1 is required for Oracle Instant ClientRUN 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/keyringsRUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpgRUN 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.listRUN apt-get update && apt-get install -y nodejs
# Install Oracle Instant ClientWORKDIR /opt/oracle# Note: You may need to change the link to the latest linux x64 zipRUN 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 OracleENV LD_LIBRARY_PATH=/opt/oracle/instantclient_21_12:$LD_LIBRARY_PATHENV PATH=/opt/oracle/instantclient_21_12:$PATH
# Setup App DirectoryWORKDIR /app
# Install Python DependenciesRUN pip install "mcp[cli]"
# Install Node Dependencies# Initialize a dummy package.json to install oracledbRUN npm init -y && npm install oracledb
# Copy Source CodeCOPY server.py .COPY oracle_worker.js .
# Expose Port for Railway/DockerEXPOSE 8000
# EntrypointCMD ["python", "server.py"]4. Client Connectivity (CrewAI Example)
Section titled “4. Client Connectivity (CrewAI Example)”Here is how you connect your CrewAI agent to this Polyglot Bridge.
import osfrom crewai import Agent, Task, Crewfrom langchain_openai import ChatOpenAI
# 1. Define the LLMllm = ChatOpenAI(model="gpt-4", temperature=0)
# 2. Define the Agent with the MCP Tool# We point to the local Docker container exposed on port 8000oracle_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 Tasktask = 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 Crewcrew = Crew( agents=[oracle_agent], tasks=[task], verbose=True)
result = crew.kickoff()print("Final Result:", result)Testing the Deployment
Section titled “Testing the Deployment”-
Build the Image:
Terminal window docker build -t oracle-bridge . -
Run the Container:
Terminal window # Pass your Oracle password as an env vardocker run -p 8000:8000 -e ORACLE_PASSWORD=your_secret_password oracle-bridge -
Verify: Access
http://localhost:8000/ssein 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.
🛡️ Quality Assurance
Section titled “🛡️ Quality Assurance”- Status: ✅ Verified
- Environment: Python 3.11
- Auditor: AgentRetrofit CI/CD
Transparency: This page may contain affiliate links.