Wednesday, July 8, 2026

How to Connect Oracle AI Private Agent Factory to Oracle EBS Using SQLcl MCP Server over SSE

How to Connect Oracle AI Private Agent Factory to Oracle EBS Using SQLcl MCP Server over SSE


Over the past few weeks, I have been hands-on with Oracle AI Private Agent Factory (PAF) on OCI, and one of the most interesting challenges I faced was connecting PAF to a classic Oracle EBS (E-Business Suite) database that sits on a private subnet. There was no documentation that covered this end-to-end, and I had to piece things together through trial, error, and a fair amount of troubleshooting.

In this blog, I am going to walk you through everything I did — from installing SQLcl on Oracle Linux 9.7, understanding why a bridge is needed between SQLcl MCP and PAF, dealing with SELinux, cross-VCN networking on OCI, and finally getting PAF to talk to your EBS database through the MCP server. If you are doing something similar, this is the guide I wish I had.


Understanding the Architecture
Before we dive into commands, let me explain what we are building and why each component exists. This is the flow:
PAF (VCN 1, Private Subnet)  →  SQLcl MCP VM (VCN 2, Private Subnet)  →  EBS Database (VCN 2, Private Subnet)

Oracle AI Private Agent Factory is an agentic AI platform that orchestrates AI agents to perform tasks — including querying databases. It communicates with external tools using the Model Context Protocol (MCP), but it expects MCP servers to be reachable over HTTP/SSE (Server-Sent Events).

SQLcl is Oracle's command-line interface for Oracle Database. From version 25.2 onwards, it ships with a built-in MCP server that you can activate with a single flag — sql -mcp. However, SQLcl's MCP server speaks stdio (standard input/output), not HTTP. This is the core challenge.


๐Ÿ’ก  PAF speaks HTTP/SSE. SQLcl MCP speaks stdio. You need a bridge between them — that bridge is mcp-proxy running on the same VM as SQLcl.


The mcp-proxy tool (Python) wraps the SQLcl stdio process and exposes it as an HTTP/SSE endpoint on port 8080. Once that endpoint is live, PAF can discover and call all five SQLcl MCP tools as if they were a native HTTP service.

The following are the OS details in my OCI environment,
OS - Oracle Linux 9.7 (OL9) OR Oracle Linux
Architecture - x86_64
RAM - Minimum 4 GB 
Disk - Boot volume at least 100 GB
Network - Private subnet 
User - oracle user with sudo access

Software Prerequisites
Java 17 or higher (JRE/JDK) — SQLcl is Java-based
SQLcl 25.2 or higher — includes the built-in MCP server
Python 3.11 or higher — required for mcp-proxy
mcp-proxy Python package — the stdio-to-SSE bridge

Section 1: Installing Java 17 on Oracle Linux 9

The first thing you need is Java 17 or higher. Do not touch the system Python or any system-managed Java — install a clean version alongside what is already on the system.

# Install OpenJDK 17
sudo dnf install java-17-openjdk -y

# Set JAVA_HOME
echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk' >> ~/.bashrc
echo 'export PATH=$JAVA_HOME/bin:$PATH' >> ~/.bashrc
source ~/.bashrc

# Verify
java -version

๐Ÿ’ก  On OL9, OpenJDK 17 is available directly from the default dnf repositories — no third-party repo needed.


Section 2: Installing SQLcl

Download SQLcl 25.2 or later from Oracle's official download page (requires an Oracle account). Once you have the zip file, extract it to a standard location.

# Create the install directory
mkdir -p /home/oracle/sqlcl

# Unzip (replace with your actual downloaded filename)
unzip sqlcl-25.x.x.zip -d /home/oracle/sqlcl/

# The binary is called 'sql' — not 'sqlcl'
# This is a common gotcha!
export PATH=/home/oracle/sqlcl/bin:$PATH

# Verify
sql -version

๐Ÿ’ก  The binary is named 'sql', not 'sqlcl'. This trips up almost everyone the first time. All commands going forward use 'sql', including sql -mcp.


Section 3: Saving the EBS Database Connection

The SQLcl MCP server relies on connections saved in its credential store at ~/.dbtools. You must save your EBS connection with the -savepwd flag before the MCP server can auto-connect to it.

# Launch SQLcl
/home/oracle/sqlcl/bin/sql /nolog

# Inside SQLcl — save EBS connection with password
SQL> conn -save ebs_mcp -savepwd apps/yourpassword@//ebs-db-hostname:1521/EBSDB

# Verify it saved correctly
SQL> connmgr list

SQL> exit

If your EBS database uses a tnsnames.ora entry, set TNS_ADMIN first:
export TNS_ADMIN=/home/oracle/network/admin

sql /nolog
SQL> conn -save ebs_mcp -savepwd apps/yourpassword@EBSDB



Section 4: Installing Python 3.11 and mcp-proxy

Your Oracle Linux 9 ships with Python 3.9, which is tied to system tools like dnf. Do not upgrade or replace it. Instead, install Python 3.11 alongside it — they coexist cleanly on OL9.

# Install Python 3.11
sudo dnf install python3.11 python3.11-pip -y

# Verify
python3.11 --version

# Install mcp-proxy using Python 3.11
python3.11 -m pip install --user mcp-proxy

# Add local bin to PATH
echo 'export PATH=$HOME/.local/bin:$PATH' >> ~/.bashrc
source ~/.bashrc

# Verify
mcp-proxy --version

๐Ÿ’ก  mcp-proxy requires Python 3.10 or higher. If you try to install it with the system Python 3.9, it will fail with 'No matching distribution found'. Always use python3.11 -m pip install for this package.


Why mcp-proxy and What It Does

Let me explain this clearly because I got confused about this myself. There are several bridge tools in the MCP ecosystem — mcp-remote, mcp-proxy, supergateway — and they all sound similar but do different things.

For our use case — wrapping SQLcl MCP (stdio) and exposing it as HTTP/SSE for PAF — mcp-proxy (Python) is the correct tool. It spawns sql -mcp as a child process, handles stdio communication internally, and exposes a clean SSE endpoint on port 8080 that PAF can register and call.

Section 5: Creating the Wrapper Script and systemd Service

The Wrapper Script
Create a shell script that sets all required environment variables and launches mcp-proxy with the correct arguments. Pay attention to using absolute paths — systemd does not load your .bashrc, so relative paths and ~ expansions will not work.

cat > /usr/local/bin/sqlmcpserver.sh << 'EOF'
#!/bin/bash
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk
export TNS_ADMIN=/home/oracle/network/admin
export PATH=/home/oracle/.local/bin:/usr/lib/jvm/java-17-openjdk/bin:/home/oracle/sqlcl/bin:$PATH

exec /home/oracle/.local/bin/mcp-proxy \
  --port=8080 \
  --host=0.0.0.0 \
  -- /home/oracle/sqlcl/bin/sql -mcp
EOF

chmod +x /usr/local/bin/sqlmcpserver.sh


๐Ÿ’ก  Keep your scripts in /usr/local/bin rather than /home/oracle. Oracle Linux 9 runs SELinux in Enforcing mode by default, and scripts in home directories carry the user_home_t context which systemd refuses to execute (you get status=203/EXEC). Scripts in /usr/local/bin automatically get the bin_t context that systemd accepts.


The systemd Service
Create a systemd unit file to manage the service lifecycle — auto-start on boot, auto-restart on crash, and proper logging via journald.

sudo vi /etc/systemd/system/sqlcl-mcp.service
[Unit]
Description=SQLcl MCP HTTP/SSE Server for PAF
After=network.target

[Service]
Type=simple
User=oracle
ExecStart=/usr/local/bin/sqlmcpserver.sh
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable sqlcl-mcp
sudo systemctl start sqlcl-mcp
sudo systemctl status sqlcl-mcp

A healthy service output looks like this:
Active: active (running) since Wed 2026-07-01 06:20:07 GMT
Main PID: 318054 (mcp-proxy)
INFO: Uvicorn running on http://0.0.0.0:8080
INFO: Application startup complete.

๐Ÿ’ก  Watch for 'http://0.0.0.0:8080' specifically. If you see '127.0.0.1:8080', add --host=0.0.0.0 to your mcp-proxy command — otherwise PAF cannot reach it from outside the VM.

Section 6: SELinux — The Silent Blocker

This deserves its own section because it cost me a significant amount of troubleshooting time. Oracle Linux 9 runs SELinux in Enforcing mode by default. If your script is in /home/oracle, systemd will refuse to execute it with status=203/EXEC — even if the file has execute permissions and a valid shebang.

The quick way to diagnose this:
getenforce
# Enforcing

ls -lZ /home/oracle/sqlmcpserver.sh
# -rwxr-xr-x. oracle oinstall unconfined_u:object_r:user_home_t:s0

The context user_home_t is the problem. The solution is simply to move your script to /usr/local/bin where it automatically gets the bin_t context that systemd trusts. Alternatively, if you must keep it in the home directory, you can relabel it:

sudo dnf install policycoreutils-python-utils -y
sudo semanage fcontext -a -t bin_t "/home/oracle/sqlmcpserver.sh"
sudo restorecon -v /home/oracle/sqlmcpserver.sh

๐Ÿ’ก  Never disable SELinux permanently to work around this. It is there for a reason — especially on a VM that holds database credentials. Fix the context, not the policy.

OS Firewall on SQLcl VM
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --reload
sudo firewall-cmd --list-ports

Verify Connectivity End-to-End
# From SQLcl VM — confirm EBS DB is reachable
nc -zv <ebs-db-private-ip> 1521

# From PAF VM — confirm SQLcl MCP SSE endpoint is reachable
curl -N http://<sqlcl-vm-private-ip>:8080/sse

Section 7: Registering SQLcl MCP in Oracle AI Private Agent Factory

Once the SSE endpoint is confirmed reachable from the PAF VM, registering it in PAF is straightforward.
Open PAF UI → Settings → MCP Servers → Add
Name: SQLcl-EBS-MCP
Endpoint URL: http://<sqlcl-vm-private-ip>:8080/sse
Transport: SSE
Authentication: None (for private subnet deployments)
Click Connect — status should change to Connected

Once connected, PAF will automatically discover all five SQLcl MCP tools:



Thanks & Regards,
Chandan Tanwani

Saturday, April 11, 2026

Why I Couldn’t SSH Into My OCI VM Using Mobile Hotspot (And How I Finally Fixed It)

Why I Couldn’t SSH Into My OCI VM Using Mobile Hotspot (And How I Finally Fixed It)

There are moments in cloud troubleshooting where everything looks perfectly configured… and yet, nothing works. This was one of those moments.

I had set up an Oracle Cloud (OCI) virtual machine, configured the networking correctly, added my public IP to the security list, and confidently tried to SSH into the server.

And then… it failed. Not once. Not twice. Every single time.

If you’ve ever faced a similar issue while working on a mobile hotspot, this article will save you hours of confusion.

๐Ÿ” The Setup — Everything Looked Correct

Let me first explain the setup so you can relate.

I had:
A compute instance in OCI with a public IP
A security rule allowing SSH (port 22) from my IP
My laptop connected via a mobile hotspot

From a configuration standpoint, this is textbook correct. In a typical home or office broadband setup, this should work instantly.

But here’s the catch — mobile networks behave very differently.

๐Ÿงช The First Suspicion — IP Mismatch

When SSH didn’t work, I did what most of us do — I verified my IP address. From the browser, I checked using an IP lookup website.
Then from terminal, I ran: curl ifconfig.me

To my surprise, both showed different results. At first glance, this might look like a minor inconsistency. But in reality, this is the starting point of the entire problem.

⚠️ Understanding the Real Problem


After digging deeper, I discovered that two things were happening simultaneously — and both are very common when using mobile hotspots.
๐ŸŒ 1. IPv6 vs IPv4 Mismatch

The IP returned from the terminal looked something like:

2509:50c2:104d:df9b:9e6b:732d:a081:d7e2

This is clearly an IPv6 address. However, most OCI environments (unless explicitly configured) operate using IPv4:
Public IP assigned to VM → IPv4
Security rules → IPv4 CIDR ranges
SSH access → typically IPv4

So what was happening?

๐Ÿ‘‰ My laptop was trying to connect using IPv6
๐Ÿ‘‰ My OCI VM was expecting IPv4 traffic

These two simply don’t talk to each other unless specifically configured.

๐Ÿ“ก 2. Carrier-Grade NAT (CGNAT)

Now comes the second and more subtle issue.

Mobile network providers don’t assign a unique public IPv4 address to every user. Instead, they use something called Carrier-Grade NAT (CGNAT).
Let me simplify this.

Instead of:

Your Laptop → Internet → OCI VM

The actual flow looks like:

Your Laptop → Mobile Network NAT → Shared Public IP → OCI VM

This means:
  • Your device does not directly own a public IPv4 address
  • Multiple users share the same external IP
  • The IP you see may not be the one OCI sees
  • The IP can change frequently
So even if you whitelist an IP in OCI, the request might come from a completely different IP.

๐Ÿšซ Why SSH Failed Despite Correct Configuration

At this point, everything started to make sense.
I had configured:
Source: <my-ip>/32
Port: 22

But OCI evaluates the actual incoming IP, not what I think my IP is. Due to CGNAT and IPv6 behavior:
My outgoing request did not match the whitelisted IP. OCI security rules blocked the connection.
Result → SSH timeout

This is why the issue feels so confusing — because the configuration is technically correct, but the network behavior breaks the assumption.

๐Ÿงช Confirming the Diagnosis
To validate this, I ran:
curl ifconfig.me
curl -4 ifconfig.me
curl -6 ifconfig.me

At this point, it was clear that this was not an OCI issue — it was a network limitation.

✅ What Actually Works (Practical Solutions)

Once you understand the root cause, the solution becomes much clearer.

๐Ÿฅ‡ Solution 1 — Use OCI Bastion (Recommended Approach)

The most effective and professional way to handle this is by using
OCI Bastion

Instead of exposing your VM directly to the internet, Bastion acts as a secure entry point.

Why this works so well:
  • It does not rely on your public IP being stable
  • It works even if your IP changes frequently
  • It eliminates the need to open SSH to the world
  • It aligns with security best practices
In simple terms, you connect to Bastion, and Bastion connects to your VM internally. This completely avoids the problems caused by CGNAT and IP mismatch.

๐Ÿฅˆ Solution 2 — Forcing IPv4 (Temporary Workaround)

If you still want to try direct SSH, you can attempt to force IPv4:
curl -4 ifconfig.me

If you get a valid IPv4 address:

Add that IP in OCI security rules
Use ssh -4 to force IPv4 connection

However, this is not reliable because:
  • The IP may change at any time
  • CGNAT may still interfere
  • You may lose access intermittently
This is more of a quick test rather than a long-term solution.

Conclusion

This issue wasn’t really about OCI configuration — it was about how mobile networks behave. With CGNAT, dynamic IPs, and IPv6 in play, the IP you whitelist is often not the one OCI actually sees, which leads to SSH failures.

Instead of trying to chase changing IPs, the better approach is to use solutions designed for such environments, like OCI Bastion or OCI Cloud Shell.

๐Ÿ‘‰ The key takeaway: when working from a mobile hotspot, don’t rely on IP-based access — use network-independent access methods for a stable and secure connection.


Thanks & Regards,
Chandan Tanwani

Friday, February 13, 2026

Shrinking Storage in Shared Autonomous Database on OCI

Shrinking Storage in Shared Autonomous Database on OCI

In one of our projects, we had deleted a huge volume of historical data from our Autonomous Database. Naturally, the expectation was simple:

Data is deleted → storage should reduce → cloud cost should go down.

But when we checked OCI metrics, the storage size was still the same. This created confusion in the team and raised an important question: “If the data is gone, why is storage still allocated?” If you have faced this, you are not alone. This is one of the most common and misunderstood behaviours in the Shared Autonomous Database on OCI.

This exact scenario is something I see very often across customers and projects using Shared Autonomous Database on OCI. 

First, Let’s Understand How Storage Works in Autonomous Database

One of the biggest advantages of Autonomous Database is automatic storage scaling.

This means:
-> As your data grows → storage automatically increases.
-> You never need to worry about provisioning disks.
-> Performance remains consistent.
However, here is the important part many people don’t realize: When data is deleted, storage does NOT shrink automatically.

Yes — that surprises many people.

Why Storage Does Not Automatically Reduce

This behavior is actually by design.Oracle keeps the allocated storage to ensure:
-> Stable performance
-> No frequent resizing overhead
-> Faster future data growth handling

So when you delete data:
✔ The space becomes free inside the database
❌ But OCI still considers it allocated storage

And that means you continue paying for that storage unless you reclaim it. This is why understanding storage shrinking is very important for cloud cost optimization.

When Should You Shrink Storage?

Based on my real project experience, you should consider shrinking storage after:
  • Large data purging activities
  • Archiving historical records
  • Cleaning up staging tables
  • Dropping big tables or partitions
  • Post-migration cleanup
  • Temporary ETL data removal
Basically, whenever a significant amount of data is removed.

Check Before Shrinking Storage

One important point to understand is that Oracle Autonomous Database does not add multiple datafiles to the DATA tablespace. Instead, it uses a single large datafile (bigfile tablespace) and automatically increases its size of that big datafile as storage grows.

Because of this architecture, shrinking storage can take significant time, since the database must reorganize and reclaim space within a very large datafile.

Run the following query to assess the potential storage savings before initiating the shrink operation. All columns in the output are expressed in MB.

select file_name,
       ceil( (nvl(hwm,1)*&&blksize)/1024/1024 ) sm,
       ceil( blocks*&&blksize/1024/1024) currsize,
       ceil( blocks*&&blksize/1024/1024) -
       ceil( (nvl(hwm,1)*&&blksize)/1024/1024 ) savings
from dba_data_files a,
     ( select file_id, max(block_id+blocks-1) hwm
         from dba_extents where tablespace_name='DATA' 
        group by file_id ) b
where a.file_id = b.file_id(+) and a.tablespace_name='DATA';

You should proceed with storage shrinking only when the savings column indicates a significant amount of reclaimable space. If the expected savings are minimal, it is not recommended to run this operation, as it is time-consuming and resource-intensive with limited practical benefit.

Steps to Shrink Storage in Autonomous Database

Step 1 — Log in to the OCI Console

Navigate to:
OCI Console → Autonomous Database → Select your database

Step 2 — Open Resource Allocation

Click the More actions drop-down menu and select Manage resource allocation.


Step 3 — Review Storage Details

In the Storage section, review:
"Allocated storage" — Total storage currently reserved
"Approximate used storage" — Actual space consumed by data


You can also run the query below to identify space usage by schemas/objects.

select file_name,
ceil( (nvl(hwm,1)*&&blksize)/1024/1024 ) sm,
ceil( blocks*&&blksize/1024/1024) currsize,
ceil( blocks*&&blksize/1024/1024) -
ceil( (nvl(hwm,1)*&&blksize)/1024/1024 ) savings
from dba_data_files a,
( select file_id, max(block_id+blocks-1) hwm
from dba_extents
group by file_id ) b
where a.file_id = b.file_id(+)

If you observe a significant difference between allocated and used storage, you may be able to reclaim space. For example, in my case there was approximately 4 TB of reclaimable storage between allocated and actual usage.

Step 4 — Click the "Shrink" Button

Click Shrink to initiate the storage reduction process.

Preconditions for Shrink Operation
The Shrink option is available only when all of the following conditions are met:

- Storage auto-scaling is enabled
- Allocated storage is greater than base (minimum) storage
- Allocated storage − Used storage > 100 GB

If these conditions are not satisfied and you click Shrink, Autonomous Database displays an “Action unavailable” message.

Considerations and Challenges with the Shrinking Process

There are a few important aspects to be aware of before initiating storage shrink:

1. No visible progress indicator
Once the shrinking process starts, there is no direct way in the console or database views to monitor its progress. The only practical approach is to raise an SR with Oracle Support and request periodic updates on the completion percentage.

2. Potentially long execution time
The duration largely depends on the size of the data file.
In my case, the data file was about 9 TB, and the shrink process took more than 28 hours to complete.

NOTE: 
๐Ÿ‘‰  The shrink operation runs an alter table... move online operation hence it is taking long time to complete it.
๐Ÿ‘‰ Once your data deletion operation is complete, wait at least 1–2 hours before initiating the shrink process. Autonomous Database needs time to recalculate storage usage, and the updated values may take some time to appear in the OCI Console.
Because of this delay, the Shrink operation may occasionally be unavailable or fail immediately after large deletions, as the console has not yet reflected the updated storage metrics.

3. ECPU allocation impacts duration
The time required for shrinking also depends on the number of ECPUs allocated to the Autonomous Database. Higher ECPU allocation provides more processing power for the reorganization work.

๐Ÿ‘‰ Therefore, it is recommended to scale up ECPUs before starting the shrink operation.
This helps in two ways:
-> Reduces overall shrink duration
-> Minimizes impact on your ongoing workload

My final conclusion

๐Ÿ‘‰ Storage will NOT shrink automatically
๐Ÿ‘‰ You must reclaim it manually via console

This small step can save significant cloud costs.



Thanks & Regards,
Chandan Tanwani