feat: add Oracle MCP server with Docker support and configuration

This commit is contained in:
Marcelo Ochoa
2025-03-13 16:51:37 +00:00
parent 941266c05a
commit 9846ccc695
9 changed files with 691 additions and 6 deletions

24
src/oracle/Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
FROM node:22-alpine AS builder
COPY src/oracle /app
COPY tsconfig.json /tsconfig.json
WORKDIR /app
RUN --mount=type=cache,target=/root/.npm npm install
RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev
FROM node:22-alpine AS release
COPY --from=builder /app/dist /app/dist
COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/package-lock.json /app/package-lock.json
ENV NODE_ENV=production
WORKDIR /app
RUN npm ci --ignore-scripts --omit-dev
ENTRYPOINT ["node", "dist/index.js"]

81
src/oracle/README.md Normal file
View File

@@ -0,0 +1,81 @@
# Oracle
A Model Context Protocol server that provides read-only access to Oracle databases. This server enables LLMs to inspect database schemas and execute read-only queries.
## Components
### Tools
- **query**
- Execute read-only SQL queries against the connected database
- Input: `sql` (string): The SQL query to execute
- All queries are executed within a READ ONLY transaction
### Resources
The server provides schema information for each table in the database:
- **Table Schemas** (`oracle://<host>/<table>/schema`)
- JSON schema information for each table
- Includes column names and data types
- Automatically discovered from database metadata
## Usage with Claude Desktop
To use this server with the Claude Desktop app, add the following configuration to the "mcpServers" section of your `claude_desktop_config.json`:
### Docker
* when running docker on macos, use host.docker.internal if the server is running on the host network (eg localhost)
* username/password can be added to the oracle url with `oracle://host.docker.internal:1521/freepdb1`
```json
{
"mcpServers": {
"oracle": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"ORACLE_USER=hr",
"-e",
"ORACLE_PASSWORD=hr_2025",
"mcp/oracle",
"host.docker.internal:1521/freepdb1"]
}
}
}
```
### NPX
```json
{
"mcpServers": {
"oracle": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-oracle",
"host.docker.internal:1521/freepdb1"
]
}
}
}
```
Replace `/freepdb1` with your database name.
## Building
Docker:
```sh
docker build -t mcp/oracle -f src/oracle/Dockerfile .
```
## License
This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.

286
src/oracle/index.ts Normal file
View File

@@ -0,0 +1,286 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import oracledb from "oracledb";
const server = new Server(
{
name: "example-servers/oracle",
version: "0.1.0",
},
{
capabilities: {
resources: {},
tools: {},
prompts: {}, // Add this line to indicate support for prompts
},
},
);
const prompts = [
{ id: 1, text: "query select * from tabs" },
{ id: 2, text: "explain select * from tabs" }
];
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("Please provide an Oracle database connection string as a command-line argument");
process.exit(1);
}
const connectionString = args[0];
if (!process.env.ORACLE_USER) {
console.error("Error: Environment variable ORACLE_USER must be set.");
process.exit(1);
}
const resourceBaseUrl = new URL("oracle://" + process.env.ORACLE_USER.toUpperCase());
resourceBaseUrl.protocol = "oracle:";
resourceBaseUrl.password = "";
const SCHEMA_PATH = "schema";
// Initialize the pool outside of request handlers.
let pool: oracledb.Pool | undefined = undefined;
// Helper function to initialize the connection pool
async function initializePool(connectionString: string) {
const dbUser = process.env.ORACLE_USER;
const dbPassword = process.env.ORACLE_PASSWORD;
if (!dbUser || !dbPassword) {
console.error(
"Error: Environment variables ORACLE_USER and ORACLE_PASSWORD must be set.",
);
process.exit(1);
}
try {
//console.log("Initializing OracleDB connection pool...");
pool = await oracledb.createPool({
user: dbUser,
password: dbPassword,
connectionString,
poolMin: 4,
poolMax: 10,
poolIncrement: 1,
queueTimeout: 60000,
});
//console.log("OracleDB connection pool initialized successfully.");
} catch (err) {
console.error("connectionString:", connectionString);
console.error("Error initializing connection pool:", err);
process.exit(1);
}
}
server.setRequestHandler(ListResourcesRequestSchema, async () => {
if (!pool) {
throw new Error("Oracle connection pool not initialized.");
}
let connection;
try {
connection = await pool.getConnection();
const result = await connection.execute<{ TABLE_NAME: string }>(
`SELECT table_name as "TABLE_NAME" FROM user_tables`,
[], // binding parameters
{ outFormat: oracledb.OUT_FORMAT_OBJECT }
);
return {
resources: result.rows!.map((row) => ({
uri: new URL(`${row.TABLE_NAME}/${SCHEMA_PATH}`, resourceBaseUrl).href,
mimeType: "application/json",
name: `"${row.TABLE_NAME}" database schema`,
})),
};
} finally {
if (connection) {
try {
await connection.close();
} catch (err) {
console.error("Error closing Oracle connection:", err);
}
}
}
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
if (!pool) {
throw new Error("Oracle connection pool not initialized.");
}
const resourceUrl = new URL(request.params.uri);
const pathComponents = resourceUrl.pathname.split("/");
const schema = pathComponents.pop();
const tableName = pathComponents.pop();
if (schema !== SCHEMA_PATH) {
throw new Error("Invalid resource URI");
}
let connection;
try {
connection = await pool.getConnection();
const result = await connection.execute<{ COLUMN_NAME: string, DATA_TYPE: string }>(
`SELECT column_name as "COLUMN_NAME", data_type as "DATA_TYPE" FROM user_tab_columns WHERE table_name = UPPER(:tableName)`,
{ tableName },
);
return {
contents: [
{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(result.rows, null, 2),
},
],
};
} finally {
if (connection) {
try {
await connection.close();
} catch (err) {
console.error("Error closing Oracle connection:", err);
}
}
}
});
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "query",
description: "Run a read-only SQL query",
inputSchema: {
type: "object",
properties: {
sql: { type: "string" },
},
},
},
{
name: "explain",
description: "Explain Plan for SQL query",
inputSchema: {
type: "object",
properties: {
sql: { type: "string" },
},
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (!pool) {
throw new Error("Oracle connection pool not initialized.");
}
if (request.params.name === "query") {
const sql = request.params.arguments?.sql as string;
let connection;
try {
connection = await pool.getConnection();
await connection.execute("SET TRANSACTION READ ONLY");
const result = await connection.execute(sql, [], { outFormat: oracledb.OUT_FORMAT_OBJECT });
return {
content: [{ type: "text", text: JSON.stringify(result.rows, null, 2) }],
isError: false,
};
} catch (error) {
throw error;
} finally {
if (connection) {
try {
await connection.close();
} catch (err) {
console.warn("Could not close connection:", err);
}
}
}
}
if (request.params.name === "explain") {
const sql = request.params.arguments?.sql as string;
let connection;
try {
connection = await pool.getConnection();
await connection.execute("EXPLAIN PLAN FOR " + sql);
const result = await connection.execute("SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY(NULL, NULL, 'ALL'))", [], { outFormat: oracledb.OUT_FORMAT_OBJECT });
return {
content: [{ type: "text", text: JSON.stringify(result.rows, null, 2) }],
isError: false,
};
} catch (error) {
throw error;
} finally {
if (connection) {
try {
await connection.close();
} catch (err) {
console.warn("Could not close connection:", err);
}
}
}
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
import { z } from "zod";
const PromptsListRequestSchema = z.object({
method: z.literal("prompts/list"),
params: z.object({}),
});
let inactivityTimer: NodeJS.Timeout | null = null;
const INACTIVITY_TIMEOUT = 60000; // 1 minute
function resetInactivityTimer() {
if (inactivityTimer) {
clearTimeout(inactivityTimer);
}
inactivityTimer = setTimeout(() => {
// console.log("No activity detected for 30 seconds. Shutting down server...");
process.exit(0);
}, INACTIVITY_TIMEOUT);
}
server.setRequestHandler(PromptsListRequestSchema, async () => {
resetInactivityTimer();
return {
prompts,
};
});
process.on("SIGINT", async () => {
console.log("Received SIGINT. Shutting down server...");
process.exit(0);
});
process.on("SIGTERM", async () => {
console.log("Received SIGTERM. Shutting down server...");
process.exit(0);
});
async function runServer() {
await initializePool(connectionString); // Initialize the pool before starting the server
const transport = new StdioServerTransport();
await server.connect(transport);
resetInactivityTimer(); // Start the inactivity timer when the server starts
}
runServer().catch(console.error);

30
src/oracle/package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "@modelcontextprotocol/server-oracle",
"version": "0.6.2",
"description": "MCP server for interacting with Oracle databases",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
"homepage": "https://modelcontextprotocol.io",
"bugs": "https://github.com/marcelo-ochoa/servers/issues",
"type": "module",
"bin": {
"mcp-server-oracle": "dist/index.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"prepare": "npm run build",
"watch": "tsc --watch"
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.0.1",
"oracledb": "^6.1.0"
},
"devDependencies": {
"@types/oracledb": "^6.1.0",
"shx": "^0.3.4",
"typescript": "^5.0.0"
}
}

10
src/oracle/tsconfig.json Normal file
View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "."
},
"include": [
"./**/*.ts"
]
}