diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..bba013b7 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,34 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/microsoft/vscode-dev-containers/blob/main/containers/docker-from-docker-compose/.devcontainer/devcontainer.json +{ + "name": "MCPServer", + "dockerComposeFile": "docker-compose-local.yml", + "service": "mcp-server", + "runServices": ["mcp-server","db"], + + "workspaceFolder": "/modelcontextprotocol/servers", + + // Set *default* container specific settings.json values on container create. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "dbaeumer.vscode-eslint", + "ms-azuretools.vscode-docker" + ] + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + //"forwardPorts": [3000], + + // "mounts": [ + // "source=/etc/hosts,target=/etc/hosts,type=bind,consistency=cached" + // ] + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "apt update && apt install -y git postgresql-client docker.io", + + // Comment out to connect as root instead. + "remoteUser": "root" +} diff --git a/.devcontainer/docker-compose-local.yml b/.devcontainer/docker-compose-local.yml new file mode 100644 index 00000000..1945bea8 --- /dev/null +++ b/.devcontainer/docker-compose-local.yml @@ -0,0 +1,24 @@ +version: '3.7' + +services: + mcp-server: + image: "node:22" + #command: ["host.docker.internal:1521/freepdb1"] + #build: + # context: .. + # dockerfile: src/oracle/Dockerfile + # platforms: + # - "linux/arm64" + command: tail -f /dev/null + environment: + - ORACLE_USER=hr + - ORACLE_PASSWORD=hr_2025 + volumes: + - ..:/modelcontextprotocol/servers + - /var/run/docker.sock:/var/run/docker.sock + db: + image: postgres:latest + environment: + POSTGRES_USER: hr + POSTGRES_PASSWORD: hr_2025 + POSTGRES_DB: hr \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 12482a9e..1f4f3faf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@modelcontextprotocol/server-filesystem": "*", "@modelcontextprotocol/server-gdrive": "*", "@modelcontextprotocol/server-memory": "*", + "@modelcontextprotocol/server-oracle": "*", "@modelcontextprotocol/server-postgres": "*", "@modelcontextprotocol/server-puppeteer": "*", "@modelcontextprotocol/server-sequential-thinking": "*", @@ -855,6 +856,10 @@ "resolved": "src/memory", "link": true }, + "node_modules/@modelcontextprotocol/server-oracle": { + "resolved": "src/oracle", + "link": true + }, "node_modules/@modelcontextprotocol/server-postgres": { "resolved": "src/postgres", "link": true @@ -923,6 +928,65 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", + "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/@smithy/abort-controller": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.8.tgz", @@ -1591,6 +1655,16 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/oracledb": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/@types/oracledb/-/oracledb-6.5.3.tgz", + "integrity": "sha512-qrcKBezSXi8MltjKtYOQTN+tk7YjeiLjLO4SgXuzGvUGmLMkDLWTRJ4OtM3YoW9dzi/tQeUdgatTmOOE3nSAVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/pg": { "version": "8.11.10", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz", @@ -1614,6 +1688,15 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "dev": true }, + "node_modules/@types/redis": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@types/redis/-/redis-4.0.10.tgz", + "integrity": "sha512-7CLy5b5fzzEGVcOccgZjoMlNpPhX6d10jEeRy2YWbFuaMNrSPc9ExRsMYsd+0VxvEHucf4EWx24Ja7cSU1FGUA==", + "license": "MIT", + "dependencies": { + "redis": "*" + } + }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", @@ -1761,9 +1844,10 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz", - "integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==", + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", + "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -2048,6 +2132,15 @@ "node": ">=12" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2792,6 +2885,15 @@ "node": ">=14" } }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -3635,6 +3737,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/oracledb": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/oracledb/-/oracledb-6.8.0.tgz", + "integrity": "sha512-A4ds4n4xtjPTzk1gwrHWuMeWsEcrScF0GFgVebGrhNpHSAzn6eDwMKcSbakZODKfFcI099iqhmqWsgko8D+7Ww==", + "hasInstallScript": true, + "license": "(Apache-2.0 OR UPL-1.0)", + "engines": { + "node": ">=14.6" + } + }, "node_modules/pac-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", @@ -4172,6 +4284,10 @@ "node": ">= 0.10" } }, + "node_modules/redis": { + "resolved": "src/redis", + "link": true + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -4759,10 +4875,11 @@ "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==" }, "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "devOptional": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4967,6 +5084,12 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -5476,6 +5599,34 @@ "zod": "^3.23.8" } }, + "src/oracle": { + "name": "@modelcontextprotocol/server-oracle", + "version": "0.6.2", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "1.0.1", + "oracledb": "^6.1.0" + }, + "bin": { + "mcp-server-oracle": "dist/index.js" + }, + "devDependencies": { + "@types/oracledb": "^6.1.0", + "shx": "^0.3.4", + "typescript": "^5.0.0" + } + }, + "src/oracle/node_modules/@modelcontextprotocol/sdk": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.0.1.tgz", + "integrity": "sha512-slLdFaxQJ9AlRg+hw28iiTtGvShAOgOKXcD0F91nUcRYiOMuS9ZBYjcdNZRXW9G5JQ511GRTdUy1zQVZDpJ+4w==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.23.8" + } + }, "src/postgres": { "name": "@modelcontextprotocol/server-postgres", "version": "0.6.2", @@ -5529,6 +5680,50 @@ "zod": "^3.23.8" } }, + "src/redis": { + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@modelcontextprotocol/sdk": "^0.4.0", + "@types/redis": "^4.0.10", + "redis": "^4.7.0" + }, + "bin": { + "redis": "build/index.js" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "typescript": "^5.7.2" + } + }, + "src/redis/node_modules/@modelcontextprotocol/sdk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.4.0.tgz", + "integrity": "sha512-79gx8xh4o9YzdbtqMukOe5WKzvEZpvBA1x8PAgJWL7J5k06+vJx8NK2kWzOazPgqnfDego7cNEO8tjai/nOPAA==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.23.8" + } + }, + "src/redis/node_modules/redis": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz", + "integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==", + "license": "MIT", + "workspaces": [ + "./packages/*" + ], + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.6.0", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" + } + }, "src/sequentialthinking": { "name": "@modelcontextprotocol/server-sequential-thinking", "version": "0.6.2", diff --git a/package.json b/package.json index 9d5e5ee2..35bc1838 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@modelcontextprotocol/server-everything": "*", "@modelcontextprotocol/server-gdrive": "*", "@modelcontextprotocol/server-postgres": "*", + "@modelcontextprotocol/server-oracle": "*", "@modelcontextprotocol/server-puppeteer": "*", "@modelcontextprotocol/server-slack": "*", "@modelcontextprotocol/server-brave-search": "*", diff --git a/src/oracle/Dockerfile b/src/oracle/Dockerfile new file mode 100644 index 00000000..8f70e3a0 --- /dev/null +++ b/src/oracle/Dockerfile @@ -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"] \ No newline at end of file diff --git a/src/oracle/README.md b/src/oracle/README.md new file mode 100644 index 00000000..fafa744f --- /dev/null +++ b/src/oracle/README.md @@ -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:////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. diff --git a/src/oracle/index.ts b/src/oracle/index.ts new file mode 100644 index 00000000..030ddc53 --- /dev/null +++ b/src/oracle/index.ts @@ -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); diff --git a/src/oracle/package.json b/src/oracle/package.json new file mode 100644 index 00000000..cd2e28dd --- /dev/null +++ b/src/oracle/package.json @@ -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" + } +} \ No newline at end of file diff --git a/src/oracle/tsconfig.json b/src/oracle/tsconfig.json new file mode 100644 index 00000000..ec5da158 --- /dev/null +++ b/src/oracle/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "." + }, + "include": [ + "./**/*.ts" + ] +}