diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..bba013b7da --- /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 0000000000..1945bea8ed --- /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/.gemini/styleguide.md b/.gemini/styleguide.md new file mode 120000 index 0000000000..ff80726687 --- /dev/null +++ b/.gemini/styleguide.md @@ -0,0 +1 @@ +../.github/copilot-instructions.md \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..ffa069c41d --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,122 @@ +--- +description: A Model Context Protocol server that provides read-only access to Oracle, MySQL, PostgreSQL, QNAP NAS and MikroTik RouterOS +applyTo: "src/{oracle,mysql,postgres,qnap,mikrotik}/**" +--- +# AI Agent Project Instructions: MCP Servers + +## Overview +This project is part of a modular AI Agent system, with each module providing a specific capability. The `src/` directory implements various MCP servers, enabling the AI Agent to interact with databases (Oracle, MySQL, PostgreSQL), storage (QNAP NAS), and network devices (MikroTik RouterOS). + +## Technology Stack + +- **Programming Language:** TypeScript (Node.js) +- **Package Management:** npm (Node Package Manager) +- **Build System:** TypeScript compiler (`tsc`) +- **Containerization:** Docker (see `Dockerfile`) +- **Configuration:** Project-specific configuration files (e.g., `tsconfig.json`, `package.json`, `server.json`) + +## TypeScript Guidelines +- Use TypeScript for all new code +- Follow functional programming principles where possible +- Use interfaces for data structures and type definitions +- Prefer immutable data (const, readonly) +- Use optional chaining (?.) and nullish coalescing (??) operators + +### Key Files + +#### Oracle Server (`src/oracle/`) +- `index.ts`: Main entry point for the Oracle server logic +- `server.ts`: Server initialization and MCP protocol setup +- `db.ts`: Database connection and query execution logic +- `handlers.ts`: Request handlers for tools and resources +- `tools.ts`: Tool definitions (query, explain, stats, connect, awr) +- `tools/`: Individual tool implementations + +#### MySQL Server (`src/mysql/`) +- `index.ts`: Main entry point for the MySQL server logic +- `server.ts`: Server initialization and MCP protocol setup +- `db.ts`: Database connection and query execution logic +- `handlers.ts`: Request handlers for tools and resources +- `tools.ts`: Tool definitions (mysql-query, mysql-explain, mysql-stats, mysql-connect, mysql-awr) +- `tools/`: Individual tool implementations + +#### PostgreSQL Server (`src/postgres/`) +- `index.ts`: Main entry point for the PostgreSQL server logic +- `server.ts`: Server initialization and MCP protocol setup +- `db.ts`: Database connection and query execution logic +- `handlers.ts`: Request handlers for tools and resources +- `tools.ts`: Tool definitions (pg-query, pg-explain, pg-stats, pg-connect, pg-awr) +- `tools/`: Individual tool implementations + +#### QNAP Server (`src/qnap/`) +- `index.ts`: Main entry point for the QNAP server logic +- `server.ts`: Server initialization and MCP protocol setup +- `handlers.ts`: Request handlers for tools and resources +- `tools.ts`: Tool definitions (qnap-connect, qnap-report, qnap-dir, qnap-file-info) +- `tools/`: Individual tool implementations + +#### MikroTik Server (`src/mikrotik/`) +- `index.ts`: Main entry point for the MikroTik server logic +- `server.ts`: Server initialization and MCP protocol setup +- `handlers.ts`: Request handlers for tools and resources +- `tools.ts`: Tool definitions (mk-connect, mk-report, mk-get, mk-awr) +- `tools/`: Individual tool implementations + +## How It Works +- The servers are written in TypeScript and are designed to be run as Node.js applications. +- They can be built and run locally or inside a Docker container for deployment. +- Each server uses the Model Context Protocol (MCP) to expose tools and resources to AI Agents. + +## Development Workflow +1. **Install Dependencies:** + ```sh + cd src/[module] + npm install + ``` +2. **Build the Project:** + ```sh + npm run build + ``` +3. **Run the Server (Locally):** + ```sh + npx -y @marcelo-ochoa/server-oracle localhost:1521/freepdb1 + npx -y @marcelo-ochoa/server-mysql localhost:3306/mydb + npx -y @marcelo-ochoa/server-postgres localhost:5432/postgres + npx -y @marcelo-ochoa/server-qnap http://nas-ip:8080 + npx -y @marcelo-ochoa/server-mikrotik 192.168.88.1 + ``` +4. **Build with Docker:** + Build the Docker image and run the container: + ```sh + docker build -t mochoa/mcp-oracle -f src/oracle/Dockerfile . + docker build -t mochoa/mcp-mysql -f src/mysql/Dockerfile . + docker build -t mochoa/mcp-postgres -f src/postgres/Dockerfile . + docker build -t mochoa/mcp-qnap -f src/qnap/Dockerfile . + docker build -t mochoa/mcp-mikrotik -f src/mikrotik/Dockerfile . + ``` +5. **Publish to ModelContextProtocol:** + ```sh + mcp-publisher login github + mcp-publisher publish + ``` +6. **Publish to npm:** + ```sh + npm login + npm publish + ``` +7. **Test with Antigravity Code Editor:** + Build with Docker as is described in step 4. + ```sh + docker build -t mochoa/mcp-oracle -f src/oracle/Dockerfile . + # manually wait until user reloads MCP servers + ``` + If antigravity is running, MCP Servers need to be reloaded. + +## Notes +- Ensure you have Node.js and npm installed for local development. +- For Docker-based workflows, ensure Docker is installed and running. +- Each server may require specific environment variables for credentials (e.g., `QNAP_USER`, `MK_PASSWORD`, `PG_PASSWORD`). + +## Additional Resources +- See the `README.md` in each module directory for specific usage, configuration, and advanced options. +- Refer to the root project `README.md` for information about the overall system. diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000000..7d8a3e867c --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,62 @@ +name: ci + +on: + push: + branches: + - "main" + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push oracle + uses: docker/build-push-action@v5 + with: + context: . + file: ./src/oracle/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: mochoa/mcp-oracle:latest + - name: Build and push postgres + uses: docker/build-push-action@v5 + with: + context: . + file: ./src/postgres/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: mochoa/mcp-postgres:latest + - name: Build and push mysql + uses: docker/build-push-action@v5 + with: + context: . + file: ./src/mysql/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: mochoa/mcp-mysql:latest + - name: Build and push Mikrotik + uses: docker/build-push-action@v5 + with: + context: . + file: ./src/mikrotik/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: mochoa/mcp-mikrotik:latest + - name: Build and push QNAP + uses: docker/build-push-action@v5 + with: + context: . + file: ./src/qnap/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: mochoa/mcp-qnap:latest diff --git a/.gitignore b/.gitignore index 7c924cfbc9..0827ee7612 100644 --- a/.gitignore +++ b/.gitignore @@ -303,3 +303,7 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ .claude/settings.local.json + +# mcp-publisher +.mcpregistry_github_token +.mcpregistry_registry_token diff --git a/package-lock.json b/package-lock.json index 1845571736..8e3bdc59cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,31 @@ "src/*" ], "dependencies": { + "@marcelo-ochoa/server-mikrotik": "*", + "@marcelo-ochoa/server-mysql": "*", + "@marcelo-ochoa/server-oracle": "*", + "@marcelo-ochoa/server-postgres": "*", + "@marcelo-ochoa/server-qnap": "*", "@modelcontextprotocol/server-everything": "*", "@modelcontextprotocol/server-filesystem": "*", "@modelcontextprotocol/server-memory": "*", "@modelcontextprotocol/server-sequential-thinking": "*" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -124,6 +143,45 @@ "hono": "^4" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -152,6 +210,26 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@marcelo-ochoa/server-mikrotik": { + "resolved": "src/mikrotik", + "link": true + }, + "node_modules/@marcelo-ochoa/server-mysql": { + "resolved": "src/mysql", + "link": true + }, + "node_modules/@marcelo-ochoa/server-oracle": { + "resolved": "src/oracle", + "link": true + }, + "node_modules/@marcelo-ochoa/server-postgres": { + "resolved": "src/postgres", + "link": true + }, + "node_modules/@marcelo-ochoa/server-qnap": { + "resolved": "src/qnap", + "link": true + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", @@ -208,6 +286,23 @@ "resolved": "src/sequentialthinking", "link": true }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", @@ -275,6 +370,17 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", @@ -539,150 +645,527 @@ "dev": true, "license": "MIT" }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.5.tgz", + "integrity": "sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.5.tgz", + "integrity": "sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } + "os": [ + "android" + ] }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.5.tgz", + "integrity": "sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.5.tgz", + "integrity": "sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.5.tgz", + "integrity": "sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.5.tgz", + "integrity": "sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.5.tgz", + "integrity": "sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/diff": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.3.tgz", - "integrity": "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.5.tgz", + "integrity": "sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.5.tgz", + "integrity": "sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/express": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", - "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.5.tgz", + "integrity": "sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "^2" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/express-serve-static-core": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", - "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.5.tgz", + "integrity": "sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.5.tgz", + "integrity": "sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw==", + "cpu": [ + "loong64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.5.tgz", + "integrity": "sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/node": { - "version": "22.19.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.21.tgz", - "integrity": "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==", + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.5.tgz", + "integrity": "sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.5.tgz", + "integrity": "sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.5.tgz", + "integrity": "sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "MIT" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.5.tgz", + "integrity": "sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.5.tgz", + "integrity": "sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.5.tgz", + "integrity": "sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.5.tgz", + "integrity": "sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.5.tgz", + "integrity": "sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.5.tgz", + "integrity": "sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.5.tgz", + "integrity": "sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.5.tgz", + "integrity": "sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.5.tgz", + "integrity": "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@toon-format/toon": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@toon-format/toon/-/toon-1.4.0.tgz", + "integrity": "sha512-bjdhhIPjnX2oVk+pKy/nD3bwuESDLX/5fwW0TxwpV7Q4PVNkiRSv1S0sPeuy9TI4PfAlulow1HShdmMTnYvoLg==", + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/diff": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.3.tgz", + "integrity": "sha512-K0Oqlrq3kQMaO2RhfrNQX5trmt+XLyom88zS0u84nnIcLvFnRUMRRHmrGny5GSM+kNO9IZLARsdQHDzkhAgmrQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.21.tgz", + "integrity": "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/oracledb": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/@types/oracledb/-/oracledb-6.10.4.tgz", + "integrity": "sha512-VztLwr8PQsStRD5MEr6FVRv9LN+JeeTrTGVUFenAmTP/8X+mfNYwz02qvMc8B41CVeDdR3enQhaWmT9iBgNJSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", @@ -911,28 +1394,63 @@ } } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", "dev": true, "license": "MIT", "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/ast-v8-to-istanbul": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz", - "integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==", + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz", + "integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -1013,6 +1531,16 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1064,6 +1592,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -1169,6 +1707,13 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -1270,6 +1815,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1312,12 +1867,26 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -1693,6 +2262,36 @@ "url": "https://opencollective.com/express" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1711,6 +2310,13 @@ "node": ">= 0.8" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1735,6 +2341,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1907,9 +2522,9 @@ } }, "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -1928,6 +2543,18 @@ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -2073,6 +2700,21 @@ "node": ">=10" } }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", @@ -2087,6 +2729,22 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jose": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", @@ -2397,6 +3055,19 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "11.5.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", @@ -2406,6 +3077,21 @@ "node": "20 || >=22" } }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -2576,6 +3262,39 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mysql2": { + "version": "3.23.4", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.23.4.tgz", + "integrity": "sha512-J1Rgl8Oy5iw3mOBjKeTMQ3cJNjMYtJYavQVXShsMtSj9rKV8Q1+QaGKNJqDVQFcNINqYYp6Vxd90r69cCoBxBA==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.3", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.5.1" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.16", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", @@ -2690,6 +3409,16 @@ "wrappy": "1" } }, + "node_modules/oracledb": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/oracledb/-/oracledb-6.10.0.tgz", + "integrity": "sha512-kGUumXmrEWbSpBuKJyb9Ip3rXcNgKK6grunI3/cLPzrRvboZ6ZoLi9JQ+z6M/RIG924tY8BLflihL4CKKQAYMA==", + "hasInstallScript": true, + "license": "(Apache-2.0 OR UPL-1.0)", + "engines": { + "node": ">=14.17" + } + }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -2700,6 +3429,13 @@ "node": ">=4" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -2715,6 +3451,16 @@ "node": ">= 0.8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2764,6 +3510,105 @@ "dev": true, "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pg": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", + "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2822,6 +3667,45 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prettier": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", @@ -3040,6 +3924,52 @@ "@rolldown/binding-win32-x64-msvc": "1.0.3" } }, + "node_modules/rollup": { + "version": "4.62.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.5.tgz", + "integrity": "sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.5", + "@rollup/rollup-android-arm64": "4.62.5", + "@rollup/rollup-darwin-arm64": "4.62.5", + "@rollup/rollup-darwin-x64": "4.62.5", + "@rollup/rollup-freebsd-arm64": "4.62.5", + "@rollup/rollup-freebsd-x64": "4.62.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.5", + "@rollup/rollup-linux-arm-musleabihf": "4.62.5", + "@rollup/rollup-linux-arm64-gnu": "4.62.5", + "@rollup/rollup-linux-arm64-musl": "4.62.5", + "@rollup/rollup-linux-loong64-gnu": "4.62.5", + "@rollup/rollup-linux-loong64-musl": "4.62.5", + "@rollup/rollup-linux-ppc64-gnu": "4.62.5", + "@rollup/rollup-linux-ppc64-musl": "4.62.5", + "@rollup/rollup-linux-riscv64-gnu": "4.62.5", + "@rollup/rollup-linux-riscv64-musl": "4.62.5", + "@rollup/rollup-linux-s390x-gnu": "4.62.5", + "@rollup/rollup-linux-x64-gnu": "4.62.5", + "@rollup/rollup-linux-x64-musl": "4.62.5", + "@rollup/rollup-openbsd-x64": "4.62.5", + "@rollup/rollup-openharmony-arm64": "4.62.5", + "@rollup/rollup-win32-arm64-msvc": "4.62.5", + "@rollup/rollup-win32-ia32-msvc": "4.62.5", + "@rollup/rollup-win32-x64-gnu": "4.62.5", + "@rollup/rollup-win32-x64-msvc": "4.62.5", + "fsevents": "~2.3.2" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -3315,6 +4245,30 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sql-escaper": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.5.1.tgz", + "integrity": "sha512-4toX5E1fQbBrpfXidaHnF0669nkAdETeIPTs2SUjxxD7RRIs9ICG4gtpmfc68JCEKehsdwLFqBu9VlQqZ1P1gg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -3347,9 +4301,113 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", "dev": true, "license": "MIT", @@ -3383,6 +4441,100 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/test-exclude/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -3417,6 +4569,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, "node_modules/tinyrainbow": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", @@ -3427,6 +4589,16 @@ "node": ">=14.0.0" } }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3506,7 +4678,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -3611,292 +4782,2225 @@ } } }, - "node_modules/vitest": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", - "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.8", - "@vitest/mocker": "4.1.8", - "@vitest/pretty-format": "4.1.8", - "@vitest/runner": "4.1.8", - "@vitest/snapshot": "4.1.8", - "@vitest/spy": "4.1.8", - "@vitest/utils": "4.1.8", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" }, "bin": { - "vitest": "vitest.mjs" + "vite-node": "vite-node.mjs" }, "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.8", - "@vitest/browser-preview": "4.1.8", - "@vitest/browser-webdriverio": "4.1.8", - "@vitest/coverage-istanbul": "4.1.8", - "@vitest/coverage-v8": "4.1.8", - "@vitest/ui": "4.1.8", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, + "node_modules/vite-node/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">= 8" + "node": ">=12" } }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "node_modules/vite-node/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite-node/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "src/everything": { + "name": "@modelcontextprotocol/server-everything", + "version": "2.0.0", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.30.0", + "cors": "^2.8.5", + "express": "^5.2.1", + "jszip": "^3.10.1", + "zod": "^4.0.0" + }, + "bin": { + "mcp-server-everything": "dist/index.js" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@vitest/coverage-v8": "^4.1.8", + "prettier": "^2.8.8", + "shx": "^0.4.0", + "typescript": "^5.6.2", + "vitest": "^4.1.8" + } + }, + "src/filesystem": { + "name": "@modelcontextprotocol/server-filesystem", + "version": "0.6.3", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.30.0", + "diff": "^8.0.3", + "glob": "^13.0.6", + "minimatch": "^10.0.1" + }, + "bin": { + "mcp-server-filesystem": "dist/index.js" + }, + "devDependencies": { + "@types/diff": "^5.0.9", + "@types/minimatch": "^5.1.2", + "@types/node": "^22", + "@vitest/coverage-v8": "^4.1.8", + "shx": "^0.4.0", + "typescript": "^5.8.2", + "vitest": "^4.1.8" + } + }, + "src/memory": { + "name": "@modelcontextprotocol/server-memory", + "version": "0.6.3", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.30.0" + }, + "bin": { + "mcp-server-memory": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^22", + "@vitest/coverage-v8": "^4.1.8", + "shx": "^0.4.0", + "typescript": "^5.6.2", + "vitest": "^4.1.8" + } + }, + "src/mikrotik": { + "name": "@marcelo-ochoa/server-mikrotik", + "version": "1.0.6", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.24.2" + }, + "bin": { + "mcp-server-mikrotik": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^22", + "shx": "^0.3.4", + "typescript": "^5.6.2" + } + }, + "src/mikrotik/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "src/mikrotik/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "src/mikrotik/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "src/mikrotik/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "src/mikrotik/node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "src/mikrotik/node_modules/shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.3", + "shelljs": "^0.8.5" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "src/mysql": { + "name": "@marcelo-ochoa/server-mysql", + "version": "1.0.7", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.24.2", + "@toon-format/toon": "^1.0.0", + "mysql2": "^3.11.5" + }, + "bin": { + "mcp-server-mysql": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^22.10.1", + "shx": "^0.3.4", + "typescript": "^5.6.2" + } + }, + "src/mysql/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "src/mysql/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "src/mysql/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "src/mysql/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "src/mysql/node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "src/mysql/node_modules/shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.3", + "shelljs": "^0.8.5" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "src/oracle": { + "name": "@marcelo-ochoa/server-oracle", + "version": "1.0.7", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.24.2", + "@toon-format/toon": "^1.0.0", + "oracledb": "^6.1.0" + }, + "bin": { + "mcp-server-oracle": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^22", + "@types/oracledb": "^6.5.1", + "@vitest/coverage-v8": "^2.1.8", + "shx": "^0.3.4", + "typescript": "^5.6.2", + "vitest": "^2.1.8" + } + }, + "src/oracle/node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "src/oracle/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "src/oracle/node_modules/@vitest/coverage-v8": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", + "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.7", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.12", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "2.1.9", + "vitest": "2.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "src/oracle/node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "src/oracle/node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "src/oracle/node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "src/oracle/node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "src/oracle/node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "src/oracle/node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "src/oracle/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "src/oracle/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "src/oracle/node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "src/oracle/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "src/oracle/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "src/oracle/node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "src/oracle/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "src/oracle/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "src/oracle/node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "src/oracle/node_modules/shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.3", + "shelljs": "^0.8.5" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "src/oracle/node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "src/oracle/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "src/oracle/node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "src/oracle/node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "src/oracle/node_modules/vitest/node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", "dev": true, "license": "MIT", "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "src/oracle/node_modules/vitest/node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { - "why-is-node-running": "cli.js" + "vite": "bin/vite.js" }, "engines": { - "node": ">=8" + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } } }, - "node_modules/wrappy": { + "src/postgres": { + "name": "@marcelo-ochoa/server-postgres", + "version": "1.0.8", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.24.2", + "@toon-format/toon": "^1.0.0", + "pg": "^8.13.0" + }, + "bin": { + "mcp-server-postgres": "dist/index.js" + }, + "devDependencies": { + "@types/pg": "^8.11.10", + "shx": "^0.3.4", + "typescript": "^5.6.2" + } + }, + "src/postgres/node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" + "src/postgres/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", + "src/postgres/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=12" + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "src/postgres/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, "engines": { - "node": ">=12" + "node": "*" } }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", + "src/postgres/node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "src/postgres/node_modules/shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "minimist": "^1.2.3", + "shelljs": "^0.8.5" + }, + "bin": { + "shx": "lib/cli.js" }, "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "src/qnap": { + "name": "@marcelo-ochoa/server-qnap", + "version": "1.0.8", "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "@modelcontextprotocol/sdk": "^1.24.2", + "@toon-format/toon": "^1.0.10" }, - "engines": { - "node": ">=8" + "bin": { + "mcp-server-qnap": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^22", + "shx": "^0.3.4", + "typescript": "^5.6.2" } }, - "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "src/qnap/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "src/qnap/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "src/qnap/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "src/everything": { - "name": "@modelcontextprotocol/server-everything", - "version": "2.0.0", - "license": "SEE LICENSE IN LICENSE", + "src/qnap/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", "dependencies": { - "@modelcontextprotocol/sdk": "^1.30.0", - "cors": "^2.8.5", - "express": "^5.2.1", - "jszip": "^3.10.1", - "zod": "^4.0.0" - }, - "bin": { - "mcp-server-everything": "dist/index.js" + "brace-expansion": "^1.1.7" }, - "devDependencies": { - "@types/cors": "^2.8.19", - "@types/express": "^5.0.6", - "@vitest/coverage-v8": "^4.1.8", - "prettier": "^2.8.8", - "shx": "^0.4.0", - "typescript": "^5.6.2", - "vitest": "^4.1.8" + "engines": { + "node": "*" } }, - "src/filesystem": { - "name": "@modelcontextprotocol/server-filesystem", - "version": "0.6.3", - "license": "SEE LICENSE IN LICENSE", + "src/qnap/node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@modelcontextprotocol/sdk": "^1.30.0", - "diff": "^8.0.3", - "glob": "^13.0.6", - "minimatch": "^10.0.1" + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" }, "bin": { - "mcp-server-filesystem": "dist/index.js" + "shjs": "bin/shjs" }, - "devDependencies": { - "@types/diff": "^5.0.9", - "@types/minimatch": "^5.1.2", - "@types/node": "^22", - "@vitest/coverage-v8": "^4.1.8", - "shx": "^0.4.0", - "typescript": "^5.8.2", - "vitest": "^4.1.8" + "engines": { + "node": ">=4" } }, - "src/memory": { - "name": "@modelcontextprotocol/server-memory", - "version": "0.6.3", - "license": "SEE LICENSE IN LICENSE", + "src/qnap/node_modules/shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "dev": true, + "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^1.30.0" + "minimist": "^1.2.3", + "shelljs": "^0.8.5" }, "bin": { - "mcp-server-memory": "dist/index.js" + "shx": "lib/cli.js" }, - "devDependencies": { - "@types/node": "^22", - "@vitest/coverage-v8": "^4.1.8", - "shx": "^0.4.0", - "typescript": "^5.6.2", - "vitest": "^4.1.8" + "engines": { + "node": ">=6" } }, "src/sequentialthinking": { diff --git a/package.json b/package.json index ab0b8c8800..daa00b0c02 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,15 @@ "@modelcontextprotocol/server-everything": "*", "@modelcontextprotocol/server-memory": "*", "@modelcontextprotocol/server-filesystem": "*", - "@modelcontextprotocol/server-sequential-thinking": "*" + "@modelcontextprotocol/server-sequential-thinking": "*", + "@marcelo-ochoa/server-qnap": "*", + "@marcelo-ochoa/server-oracle": "*", + "@marcelo-ochoa/server-postgres": "*", + "@marcelo-ochoa/server-mysql": "*", + "@marcelo-ochoa/server-mikrotik": "*" }, "overrides": { "qs": ">=6.15.2", "hono": ">=4.12.21" } -} +} \ No newline at end of file diff --git a/src/mikrotik/.dockerignore b/src/mikrotik/.dockerignore new file mode 100644 index 0000000000..8d2924846b --- /dev/null +++ b/src/mikrotik/.dockerignore @@ -0,0 +1,3 @@ +dist +node_modules +*.js.map diff --git a/src/mikrotik/AWR_example.md b/src/mikrotik/AWR_example.md new file mode 100644 index 0000000000..3bd2c879e6 --- /dev/null +++ b/src/mikrotik/AWR_example.md @@ -0,0 +1,75 @@ +## MikroTik AWR in action + +**Sample prompts**: +- Generate a MikroTik AWR report +- Audit my router's security and performance using mk-awr +- Model Claude Sonnet 3.5 + +Here is an example of an Automatic Workload Repository (AWR) style report generated by the `mk-awr` tool for a MikroTik router, highlighting performance metrics and security risks: + +### **Device Information** +- **Identity**: Core_Router_Edge +- **Model**: CCR2004-16G-2S+ +- **RouterOS Version**: 7.12.1 +- **Uptime**: 45w2d14:32:10 + +### **Performance Metrics** +- **CPU Load**: 12% ✅ +- **Free Memory**: 3.2 GB / 4.0 GB ✅ +- **Disk Usage**: 128.5 MB / 512.0 MB ✅ + +### **Security Audit** + +#### **Insecure Services** 🔴 **Critical Issue** +- **Insecure Services Enabled**: telnet, ftp, www +- **Issue**: These services transmit data (including credentials) in plain text. +- **Recommendation**: Disable these services and use `ssh` and `www-ssl` instead. + +#### **DNS Open Resolver** ⚠️ **Warning** +- **Allow Remote Requests**: true +- **Issue**: If not properly firewalled, your router can be used in DNS amplification attacks. +- **Recommendation**: Ensure firewall rules drop incoming DNS requests (UDP/53) from the WAN interface. + +#### **Neighbor Discovery** ⚠️ **Warning** +- **Neighbors Found**: 3 devices detected via CDP/LLDP/MNDP. +- **Issue**: Discovery protocols can reveal topology information to potential attackers if active on WAN interfaces. +- **Recommendation**: Restrict neighbor discovery to internal (trusted) interfaces only. + +### **Network Summary** +- **Active Interfaces**: 5 +- **Top Interfaces**: + - `sfp-sfpplus1` (Gateway): Running, 10G + - `ether1` (Local-Bridge): Running, 1G +- **IP Config**: 2 public IPs, 4 internal subnets. + +### **Recommendations** + +#### 🔴 **Critical Priority** + +1. **Secure Access Methods** + - **Action**: Disable insecure management services. + ```routeros + /ip/service disable telnet,ftp,www + ``` + +2. **Firewall DNS Access** + - **Action**: Add a rule to drop DNS requests from WAN. + ```routeros + /ip/firewall/filter add action=drop chain=input dst-port=53 in-interface=ether1 protocol=udp + ``` + +#### 🟡 **Medium Priority** + +3. **Neighbor Discovery Management** + - **Action**: Set discovery interface list to 'none' or 'internal'. + ```routeros + /ip/neighbor/discovery-settings set discover-interface-list=internal + ``` + +4. **Firmware Update** + - **Note**: Version 7.12.1 is stable, but check for latest bugfix releases. + +**Estimated Impact**: +- Significantly reduced attack surface for credential sniffing. +- Prevention of participation in DDoS (DNS amplification) attacks. +- Improved network obscurity by disabling discovery on public ports. diff --git a/src/mikrotik/CHANGELOG.md b/src/mikrotik/CHANGELOG.md new file mode 100644 index 0000000000..a429b2931b --- /dev/null +++ b/src/mikrotik/CHANGELOG.md @@ -0,0 +1,41 @@ +## Change Log + +### 2026-03-11 +- **chore**: Bump server version to 1.0.6 + - Updated version to 1.0.6 across package.json, server.json, and server.ts + - Migrated to `McpServer` API from deprecated `Server` class + - Implemented `ResourceTemplate` for dynamic resource discovery + + +### 2026-03-07 +- **chore**: Bump server version to 1.0.5 + - Updated version to 1.0.5 across package.json, server.json, and server.ts + - Refactored error handling in `resources.ts` to throw `McpError` when connection is not established. + + +### 2026-01-22 +- **chore**: Bump server version to 1.0.4 + - Updated version to 1.0.4 across package.json, server.json, and server.ts + - Refactored prompt names to be more descriptive for better CLI visibility +- **feat**: Implemented `listResourcesHandler` and `readResourceHandler` for MikroTik interfaces as `mikrotik://interface/{name}`. +- **feat**: Added bridge and bridge port resources as `mikrotik://bridge/{name}` and `mikrotik://bridge/{name}/{port}`. +- **feat**: Added IP route resources as `mikrotik://route/{id}`. +- **refactor**: Structured tool handlers and resources into separate modules for better readability and maintenance. + +### 2026-01-16 +- **feat**: Added `mk-awr` tool for automated performance and security reports including log auditing. +- **refactor**: Renamed `mk-print` tool to `mk-get` for better alignment with other MCP segments. +- **docs**: Added `Demos.md` with usage examples for Claude Desktop, Gemini CLI, and Antigravity. +- **chore**: Updated version to 1.0.2 and synchronized `server.json` and `package.json`. + +### 2026-01-08 +- **feat**: Improved login handling to correctly detect `!trap` responses +- **docs**: Updated documentation with Docker examples and License information + +### 2026-01-07 +- **feat**: Initial MikroTik MCP server implementation + - Added support for connecting to MikroTik routers via RouterOS API + - Implemented `mk-connect`, `mk-report`, and `mk-print` tools + - Added support for optional host/secure startup arguments + - Integrated with `MK_USER` and `MK_PASSWORD` environment variables + - Implemented basic and secure (TLS) connection modes \ No newline at end of file diff --git a/src/mikrotik/Demos.md b/src/mikrotik/Demos.md new file mode 100644 index 0000000000..f2365ce51d --- /dev/null +++ b/src/mikrotik/Demos.md @@ -0,0 +1,256 @@ +# Demos + +Some sample usage scenarios are shown below: + +## This Demo is using two Mikrotik routers + +The system report for the MikroTik router at 172.29.160.1 (connected via tunnel) has been generated. This device is a hAP ac². + +### 🚀 System Resources +- Model: hAP ac² (RBD52G-5HacD2HnD) +- Uptime: 1 week, 6 days, 23 hours. +RouterOS Version: 6.49.19 (stable). +- CPU: ARMv7 (4 cores) @ 716MHz. +- CPU Load: 0% (Idle/Minimum load). +- Memory: 74.2 MB free out of 128 MB total. +- Firmware: Current is 6.49.7, but there is an upgrade available to 6.49.19. + +### 🌡️ Health & Storage +- Bad Blocks: 0. +- HDD Space: 1.8 MB free out of 16 MB. + +### 🌐 Network Traffic Snapshot +- Interface RX Rate TX Rate Status +- ether1 (WAN) 23.7 kbps 159.4 kbps Running +- bridge 50.8 kbps 164.3 kbps Running +- wlan4 0 bps 3.9 kbps Running +- pptp-exactas 0 bps 0 bps Connected/Idle + +### 🔍 Observation +The router is operating very comfortably with 0% CPU load. +Firmware Update: You are running an older firmware (6.49.7) than the installed RouterOS version (6.49.19). You might want to consider running /system routerboard upgrade to synchronize them. +Storage: The 16MB flash is quite tight (1.8MB free). Avoid saving large log files or files on this device. + +The system report for the MikroTik router 10.1.1.1 (CCR1009-7G-1C-1S+) has been generated. Here are the key performance and health metrics: + +### 🚀 System Resources +- Uptime: 20 hours, 36 minutes. +- RouterOS Version: 6.49.19 (long-term). +- CPU: TileGx (9 cores) @ 1200MHz. +- CPU Load: 1% (Idle/Low load). +- Memory: 1.8 GB free out of 2 GB total. +- HDD Space: 81.4 MB free out of 134.2 MB. + +### 🌡️ Health & Power +- Temperatures: Chassis 31°C / CPU 55°C. +- Power Redundancy: Both PSU1 and PSU2 are in OK state. +- Voltage: 24V. +- Power Consumption: 24.2W. +- Fan Speed: 5405 RPM (Active). + +### 🌐 Network Traffic (Current Snapshot) +- Interface RX Rate TX Rate Status +- sfpplus1 160.9 Mbps 5.8 Mbps Running +- bridge2 7.6 Mbps 160.7 Mbps Running +- bridge1 4.3 Mbps 6.9 Mbps Running +- ether1-ether7 0 bps 0 bps Not Running + +### 🔍 Observation +The router is in excellent health with very low CPU utilization despite handling approximately 160 Mbps of traffic on sfpplus1 and bridge2. +Power redundancy is confirmed as both PSUs are operational. +The system is running a stable (long-term) version of RouterOS. + +## 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) +* Credentials are passed via environment variables `MK_USER` and `MK_PASSWORD` + +```json +{ + "mcpServers": { + "mikrotik": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "MK_USER=ai_agent", + "-e", + "MK_PASSWORD=ai_password", + "mochoa/mcp-mikrotik", + "host.docker.internal"] + } + } +} +``` + +Note: Due Docker Desktop can not route to Mikrotik router 172.29.160.1, we use `host.docker.internal` to connect to the router using socat forwarding. For example: + +```sh +socat TCP-LISTEN:8721,fork TCP:172.29.160.1:8721 +``` + +### NPX + +```json +{ + "mcpServers": { + "mikrotik": { + "command": "npx", + "args": [ + "-y", + "@marcelo-ochoa/server-mikrotik", + "172.29.160.1" + ], + "env": { + "MK_USER": "ai_agent", + "MK_PASSWORD": "ai_password" + } + } + } +} +``` + +### Demo Prompts + +Sample prompts using above routers. + +- mk-connect 172.29.160.1 ai_agent ai_password false +- mk-get system/resource +- mk-awr +- using information extracted from mikrotik routers 10.1.1.1 and 172.29.160.1 make a plan to migrate site-site actual VPN connection using pptp to a more secure implementation L2TP/IPsec, do an step by step implementation for the server running at 10.1.1.1 and a client in 172.29.160.1 + +Based on the AWR reports and log data extracted from both routers (CCR1009 and hAP ac²), I have designed a migration plan to replace the insecure PPTP tunnel with a hardware-accelerated L2TP/IPsec site-to-site VPN. + +#### 📋 Migration Strategy +- Infrastructure: Both routers support hardware encryption (TileGx on CCR and IPQ4000 on hAP ac²), so IPsec will run with minimal CPU impact. +- Security: We will use AES-256-CBC with SHA256 for the IPsec phase 2 proposal. +- Method: High-availability "Dual Stack" migration — we will keep PPTP running on a different port/id while we test L2TP, then disable PPTP once confirmed. + +#### 🛠️ Step 1: Server Configuration (Router 10.1.1.1 - mk-gw2) +Run these commands on the CCR1009: + +```bash +# 1. Create a specific Pool for the VPN clients +/ip pool add name=vpn-pool-l2tp ranges=10.1.252.10-10.1.252.20 +# 2. Create the PPP Profile with strong encryption +/ppp profile add name=profile-l2tp-secure local-address=10.1.252.1 \ + remote-address=vpn-pool-l2tp use-encryption=yes dns-server=10.1.1.1 +# 3. Create the user secret (migrating user 'mochoa') +/ppp secret add name=mochoa password=STonG_PaSSwORD service=l2tp \ + profile=profile-l2tp-secure +# 4. Enable L2TP Server with IPsec +# Note: 'use-ipsec=yes' automatically creates the Phase 1/2 IPsec identities +/interface l2tp-server server set enabled=yes use-ipsec=yes \ + ipsec-secret=MyVerySecureSharedSecret default-profile=profile-l2tp-secure +# 5. Open Firewall for IPsec (UDP 500, 4500 and IP Protocol 50/ESP) +/ip firewall filter add action=accept chain=input protocol=udp dst-port=500,4500 \ + comment="VPN: IPsec IKE/NAT-T" place-before=0 +/ip firewall filter add action=accept chain=input protocol=ipsec-esp \ + comment="VPN: IPsec ESP" place-before=0 +/ip firewall filter add action=accept chain=input protocol=udp dst-port=1701 \ + comment="VPN: L2TP" place-before=0 +``` + +#### 🛠️ Step 2: Client Configuration (Router 172.29.160.1 - hAP ac²) +Run these commands on the hAP ac²: + +```bash +# 1. Create the L2TP Client +# Replace [PUBLIC_IP_SERVER] with the WAN IP of mk-gw2 +/interface l2tp-client add name=vpn-to-hq connect-to=[PUBLIC_IP_SERVER] \ + user=mochoa password=STonG_PaSSwORD use-ipsec=yes \ + ipsec-secret=MyVerySecureSharedSecret disabled=no profile=default-encryption +# 2. Configure Site-to-Site Routing +# This allows 172.29.160.x to reach the 10.1.1.x network +/ip route add dst-address=10.1.0.0/16 gateway=vpn-to-hq +``` + +#### 🛠️ Step 3: Verify the Connection +Run these commands on both routers to verify the connection: + +```bash +# On mk-gw2 +/interface l2tp-server server print +/interface l2tp-server session print +/interface l2tp-server statistics print + +# On hAP ac² +/interface l2tp-client print +/interface l2tp-client session print +/interface l2tp-client statistics print +``` + +## Using Gemini CLI + +[Gemini CLI](https://github.com/google-gemini/gemini-cli/) +is an open-source AI agent that brings the power of Gemini directly +into your terminal. It provides lightweight access to Gemini, giving you the +most direct path from your prompt to our model. + +Using this sample settings.json file at ~/.gemini/ directory: + +```json +{ + "mcpServers": { + "mikrotik": { + "command": "npx", + "args": [ + "-y", + "@marcelo-ochoa/server-mikrotik", + "172.29.160.1" + ], + "env": { + "MK_USER": "ai_agent", + "MK_PASSWORD": "ai_password" + } + } + }, + "security": { + "auth": { + "selectedType": "gemini-api-key" + } + }, + "ui": { + "theme": "ANSI" + }, + "selectedAuthType": "gemini-api-key", + "theme": "Dracula" +} +``` + +### Sample prompts with Gemini CLI + +- mk-connect to 172.29.160.1 using ai_agent as user and ai_password as password using mikrotik mcp server +- mk-get system/resource +- mk-awr +- using information extracted from mikrotik routers 10.1.1.1 and 172.29.160.1 make a plan to migrate site-site actual VPN connection using pptp to a more secure implementation L2TP/IPsec, do an step by step implementation for the server running at 10.1.1.1 and a client in 172.29.160.1 + +## Using Antigravity Code Editor + +Put this in `~/.gemini/antigravity/mcp_config.json` + +```json +{ + "mcpServers": { + "mikrotik": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "MK_USER=ai_agent", + "-e", + "MK_PASSWORD=ai_password", + "mochoa/mcp-mikrotik", + "host.docker.internal" + ] + } + } +``` diff --git a/src/mikrotik/Dockerfile b/src/mikrotik/Dockerfile new file mode 100644 index 0000000000..1c0983e7a9 --- /dev/null +++ b/src/mikrotik/Dockerfile @@ -0,0 +1,29 @@ +FROM node:slim AS builder + +COPY src/mikrotik /app +COPY tsconfig.json /tsconfig.json + +WORKDIR /app + +RUN --mount=type=cache,target=/root/.npm npm install + +RUN npm run build + +RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev + +FROM node:slim AS release + +# Update and upgrade to fix OS-level vulnerabilities +RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/* + +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"] diff --git a/src/mikrotik/LICENSE b/src/mikrotik/LICENSE new file mode 100644 index 0000000000..4a93985763 --- /dev/null +++ b/src/mikrotik/LICENSE @@ -0,0 +1,216 @@ +The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0. + +Contributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License. + +No rights beyond those granted by the applicable original license are conveyed for such contributions. + +--- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright + owner or by an individual or Legal Entity authorized to submit on behalf + of the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +--- + +MIT License + +Copyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +Creative Commons Attribution 4.0 International (CC-BY-4.0) + +Documentation in this project (excluding specifications) is licensed under +CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for +the full license text. diff --git a/src/mikrotik/README.md b/src/mikrotik/README.md new file mode 100644 index 0000000000..b9c7e56881 --- /dev/null +++ b/src/mikrotik/README.md @@ -0,0 +1,169 @@ +# MikroTik RouterOS API MCP Server + +A Read-Only MCP server implementation for interacting with MikroTik routers using the RouterOS API. + +## Features + +- **Connection Management**: Connect and authenticate with MikroTik routers. +- **Resource Monitoring**: Dynamic access to interfaces, bridges, and routing tables via MCP resources. +- **Protocol Support**: Works with both plain TCP (8728) and secure SSL/TLS (8729). +- **Modern & Legacy Login**: Supports RouterOS versions prior to and after v6.43. + +## Resources + +This server exposes MikroTik entities as MCP resources for direct inspection: + +- **Interfaces**: `mikrotik://interface/{name}` + - Example: `mikrotik://interface/ether1` +- **Bridges**: `mikrotik://bridge/{name}` + - Example: `mikrotik://bridge/bridge1` +- **Bridge Ports**: `mikrotik://bridge/{bridge_name}/{interface_name}` + - Example: `mikrotik://bridge/bridge1/ether2` +- **IP Routes**: `mikrotik://route/{id}` + - Example: `mikrotik://route/400AF317` (Note: internal IDs are used for routing entries) + +## Tools + +1. `mk-connect`: Connects to a router. + - `host`: IP address of the router. + - `user`: Username. + - `password`: Password. + - `secure`: (Optional) Use SSL/TLS. Default is `false`. + +Example: + mk-connect 192.168.88.1 admin mypassword + +2. `mk-report`: Generates a comprehensive system report. + - Aggregates system resources, health, routerboard info, and interface traffic statistics (using `monitor-traffic` once). + +3. `mk-get`: Returns a JSON array with the result of a MikroTik API `/print` command. + - `sentence`: The API path (e.g., `/ip/route`, `/interface`, `/log`). + - The server automatically ensures the path starts with `/` and ends with `/print`. + +4. `mk-awr`: Generates an Automatic Workload Repository (AWR) style report for MikroTik. + - Includes performance metrics, security audit, and recommendations. + - No input required. + +## Prompts + +The server provides several pre-defined prompts for common tasks: + +- **mk-connect: Connect to MikroTik**: Helps you establish a connection to your router. +- **mk-report: System Report**: Requests a full system status and traffic report. +- **mk-get-route: Routing Table**: Specifically asks for the current IP routing table. +- **mk-get-interface: List Interfaces**: Specifically asks for all configured interfaces. +- **mk-get-log: View Logs**: Requests the latest system log entries. +- **mk-awr: Security Audit**: Initiates a full performance and security audit of the router. + +### Configuration + +The MikroTik server can use environment variables or the `mk-connect` tool for secure credential management: + +- **`MK_USER`**: MikroTik username (required if providing host at startup) +- **`MK_PASSWORD`**: MikroTik password (required if providing host at startup) + +### Startup Arguments + +You can optionally provide the host and security setting as command-line arguments: +1. `host`: (Optional) IP address of the router. +2. `secure`: (Optional) Use SSL/TLS. Default is `false`. + +If these are provided, the server will attempt to connect automatically on startup using `MK_USER` and `MK_PASSWORD`. + +### Local Build + +```bash +cd src/mikrotik +npm install +npm run build +``` + +### Usage with Claude Desktop + +Add this to your `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "mikrotik": { + "command": "npx", + "args": [ + "-y", + "@marcelo-ochoa/server-mikrotik", + "192.168.88.1" + ], + "env": { + "MK_USER": "ai_agent", + "MK_PASSWORD": "ai_password" + } + } + } +} +``` + +Sample using Docker: + +```json +{ + "mcpServers": { + "mikrotik": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "MK_USER=admin", + "-e", + "MK_PASSWORD=password", + "mochoa/mcp-mikrotik", + "192.168.88.1", + "false" + ] + } +} +``` + +Note: Arguments and environment variables are optional. You can connect later using `mk-connect`. + +## Demos + +Using the information of `mk-get`: + - interface + - ip/address + - ip/route + - ip/route/rules + - ip/firewall[address-list,filter,nat,mangle] +find potential security risk and no-used entities + +See [Demos](https://github.com/marcelo-ochoa/servers/blob/main/src/mikrotik/Demos.md) for usage examples with Claude Desktop, Gemini CLI, and Antigravity Code Editor. + +### MikroTik AWR in action + +See [MikroTik AWR in action](https://github.com/marcelo-ochoa/servers/blob/main/src/mikrotik/AWR_example.md) for an example of an Automatic Workload Repository (AWR) style report generated by the `mk-awr` tool for a MikroTik router, highlighting performance metrics and security risks. + +## Docker + +Building the container: + +```bash +docker build -t mochoa/mcp-mikrotik -f src/mikrotik/Dockerfile . +``` + +Running the container: + +```bash +docker run -i --rm -e MK_USER=admin -e MK_PASSWORD=mypassword mochoa/mcp-mikrotik 192.168.88.1 +``` + +## Change Log + +See [Change Log](https://github.com/marcelo-ochoa/servers/blob/main/src/mikrotik/CHANGELOG.md) for the history of changes. + +## Sources + +As usual, the code of this extension is at [GitHub](https://github.com/marcelo-ochoa/servers), feel free to suggest changes and make contributions. + +## 📜 License + +This project is licensed under the Apache License, Version 2.0 for new contributions, with existing code under MIT - see the [LICENSE](https://github.com/marcelo-ochoa/servers/blob/main/src/mikrotik/LICENSE) file for details. diff --git a/src/mikrotik/db.ts b/src/mikrotik/db.ts new file mode 100644 index 0000000000..f34d73e72d --- /dev/null +++ b/src/mikrotik/db.ts @@ -0,0 +1,39 @@ +import { MikroTikApi } from "./tools/api.js"; + +let api: MikroTikApi | null = null; +let connectionConfig: any = null; + +export function setApi(newApi: MikroTikApi | null, config: any) { + if (api) { + api.close(); + } + api = newApi; + connectionConfig = config; +} + +export function getApi(): MikroTikApi | null { + return api; +} + +export async function initializeApi(host: string, user?: string, password?: string, secure: boolean = false) { + const dbUser = user || process.env.MK_USER; + const dbPassword = password || process.env.MK_PASSWORD; + + if (!dbUser || !dbPassword) { + throw new Error("Environment variables MK_USER and MK_PASSWORD must be set."); + } + + const newApi = new MikroTikApi({ debug: false }); + try { + await newApi.connect(host, undefined, secure); + const loggedIn = await newApi.login(dbUser, dbPassword); + if (!loggedIn) { + newApi.close(); + throw new Error("Login failed: invalid username or password"); + } + setApi(newApi, { host, user: dbUser, secure }); + } catch (error: any) { + newApi.close(); + throw error; + } +} diff --git a/src/mikrotik/handlers.ts b/src/mikrotik/handlers.ts new file mode 100644 index 0000000000..b8639977b3 --- /dev/null +++ b/src/mikrotik/handlers.ts @@ -0,0 +1,23 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { connectHandler } from "./tools/connect.js"; +import { getHandler } from "./tools/get.js"; +import { reportHandler } from "./tools/report.js"; +import { awrHandler } from "./tools/awr.js"; + +export { initializeApi } from "./db.js"; +export { listResourcesHandler, readResourceHandler } from "./resources.js"; + +const toolHandlers: Record Promise> = { + "mk-connect": connectHandler, + "mk-get": getHandler, + "mk-report": reportHandler, + "mk-awr": awrHandler, +}; + +export const callToolHandler = async (request: CallToolRequest) => { + const handler = toolHandlers[request.params.name]; + if (handler) { + return handler(request); + } + throw new Error(`Unknown tool: ${request.params.name}`); +}; diff --git a/src/mikrotik/index.ts b/src/mikrotik/index.ts new file mode 100644 index 0000000000..696f71517e --- /dev/null +++ b/src/mikrotik/index.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node +import { runServer } from "./server.js"; + +runServer().catch((error) => { + console.error("Fatal error starting Mikrotik MCP server:", error); + process.exit(1); +}); diff --git a/src/mikrotik/package.json b/src/mikrotik/package.json new file mode 100644 index 0000000000..e8ab3065ea --- /dev/null +++ b/src/mikrotik/package.json @@ -0,0 +1,41 @@ +{ + "name": "@marcelo-ochoa/server-mikrotik", + "mcpName": "io.github.marcelo-ochoa/mikrotik", + "version": "1.0.6", + "repository": { + "type": "git", + "url": "https://github.com/marcelo-ochoa/servers.git", + "subfolder": "src/mikrotik" + }, + "description": "An MCP server for MikroTik RouterOS API.", + "keywords": [ + "read-only-mcp", + "mikrotik", + "ai-agent", + "llm-tool", + "rag" + ], + "license": "MIT", + "author": "Marcelo Fabian Ochoa", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/marcelo-ochoa/servers/issues", + "type": "module", + "bin": { + "mcp-server-mikrotik": "dist/index.js" + }, + "main": "dist/api.js", + "types": "dist/api.d.ts", + "scripts": { + "build": "tsc && shx chmod +x dist/*.js", + "prepare": "npm run build", + "watch": "tsc --watch" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.24.2" + }, + "devDependencies": { + "@types/node": "^22", + "shx": "^0.3.4", + "typescript": "^5.6.2" + } +} \ No newline at end of file diff --git a/src/mikrotik/resources.ts b/src/mikrotik/resources.ts new file mode 100644 index 0000000000..24545764fa --- /dev/null +++ b/src/mikrotik/resources.ts @@ -0,0 +1,132 @@ +import { ListResourcesRequest, ReadResourceRequest, McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; +import { getApi } from "./db.js"; + +export const listResourcesHandler = async (request: ListResourcesRequest) => { + const currentApi = getApi(); + if (!currentApi) { + return { resources: [] }; + } + + try { + const intReplies = await currentApi.talk(["/interface/print"]); + const bridgeReplies = await currentApi.talk(["/interface/bridge/print"]); + const portReplies = await currentApi.talk(["/interface/bridge/port/print"]); + const routeReplies = await currentApi.talk(["/ip/route/print"]); + + const interfaces = intReplies.filter((r) => r.command === "!re").map((r) => r.attributes); + const bridges = bridgeReplies.filter((r) => r.command === "!re").map((r) => r.attributes); + const bridgePorts = portReplies.filter((r) => r.command === "!re").map((r) => r.attributes); + const routes = routeReplies.filter((r) => r.command === "!re").map((r) => r.attributes); + + const resources: any[] = []; + + // Regular interfaces + interfaces.forEach((i) => { + resources.push({ + uri: `mikrotik://interface/${i.name}`, + mimeType: "application/json", + name: `Interface ${i.name}`, + description: `MikroTik interface ${i.name} (${i.type})`, + }); + }); + + // Bridges + bridges.forEach((b) => { + resources.push({ + uri: `mikrotik://bridge/${b.name}`, + mimeType: "application/json", + name: `Bridge ${b.name}`, + description: `MikroTik bridge ${b.name}`, + }); + }); + + // Bridge ports + bridgePorts.forEach((p) => { + resources.push({ + uri: `mikrotik://bridge/${p.bridge}/${p.interface}`, + mimeType: "application/json", + name: `Port ${p.interface} on ${p.bridge}`, + description: `MikroTik bridge port ${p.interface} assigned to bridge ${p.bridge}`, + }); + }); + + // IP Routes + routes.forEach((r) => { + const id = r[".id"].startsWith('*') ? r[".id"].slice(1) : r[".id"]; + resources.push({ + uri: `mikrotik://route/${id}`, + mimeType: "application/json", + name: `Route to ${r["dst-address"]}`, + description: `MikroTik IP route to ${r["dst-address"]} via ${r["gateway"] || "unknown"}`, + }); + }); + + return { resources }; + } catch (error: any) { + throw new Error(`Error listing resources: ${error.message}`); + } +}; + +export const readResourceHandler = async (request: ReadResourceRequest) => { + const { uri } = request.params; + const currentApi = getApi(); + if (!currentApi) { + throw new McpError(ErrorCode.InvalidRequest, "Not connected to MikroTik. Use mk-connect first."); + } + + try { + // Handle interface resource + const ifaceMatch = uri.match(/^mikrotik:\/\/interface\/(.+)$/); + if (ifaceMatch) { + const name = decodeURIComponent(ifaceMatch[1]); + const replies = await currentApi.talk(["/interface/print"]); + const item = replies.filter((r) => r.command === "!re").map((r) => r.attributes).find(i => i.name === name); + if (!item) throw new Error(`Interface not found: ${name}`); + return { + contents: [{ uri, mimeType: "application/json", text: JSON.stringify(item, null, 2) }], + }; + } + + // Handle bridge port resource (check this first because it's more specific than bridge) + const portMatch = uri.match(/^mikrotik:\/\/bridge\/([^\/]+)\/(.+)$/); + if (portMatch) { + const bridgeName = decodeURIComponent(portMatch[1]); + const portName = decodeURIComponent(portMatch[2]); + const replies = await currentApi.talk(["/interface/bridge/port/print"]); + const item = replies.filter((r) => r.command === "!re").map((r) => r.attributes).find(i => i.bridge === bridgeName && i.interface === portName); + if (!item) throw new Error(`Bridge port not found: ${portName} on ${bridgeName}`); + return { + contents: [{ uri, mimeType: "application/json", text: JSON.stringify(item, null, 2) }], + }; + } + + // Handle bridge resource + const bridgeMatch = uri.match(/^mikrotik:\/\/bridge\/(.+)$/); + if (bridgeMatch) { + const name = decodeURIComponent(bridgeMatch[1]); + const replies = await currentApi.talk(["/interface/bridge/print"]); + const item = replies.filter((r) => r.command === "!re").map((r) => r.attributes).find(i => i.name === name); + if (!item) throw new Error(`Bridge not found: ${name}`); + return { + contents: [{ uri, mimeType: "application/json", text: JSON.stringify(item, null, 2) }], + }; + } + + // Handle route resource + const routeMatch = uri.match(/^mikrotik:\/\/route\/(.+)$/); + if (routeMatch) { + const rawId = decodeURIComponent(routeMatch[1]); + const id = rawId.startsWith('*') ? rawId : '*' + rawId; + const replies = await currentApi.talk(["/ip/route/print"]); + const item = replies.filter((r) => r.command === "!re").map((r) => r.attributes).find(r => r[".id"] === id); + if (!item) throw new Error(`Route not found: ${id}`); + return { + contents: [{ uri, mimeType: "application/json", text: JSON.stringify(item, null, 2) }], + }; + } + + throw new Error(`Invalid resource URI: ${uri}`); + } catch (error: any) { + throw new Error(`Error reading resource: ${error.message}`); + } +}; diff --git a/src/mikrotik/server.json b/src/mikrotik/server.json new file mode 100644 index 0000000000..292759713c --- /dev/null +++ b/src/mikrotik/server.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.marcelo-ochoa/mikrotik", + "description": "MCP server for MikroTik RouterOS API", + "repository": { + "url": "https://github.com/marcelo-ochoa/servers", + "source": "github", + "subfolder": "src/mikrotik" + }, + "version": "1.0.6", + "packages": [ + { + "registryType": "npm", + "identifier": "@marcelo-ochoa/server-mikrotik", + "version": "1.0.6", + "transport": { + "type": "stdio" + }, + "packageArguments": [ + { + "type": "positional", + "valueHint": "host", + "description": "MikroTik host", + "isRequired": false + } + ], + "environmentVariables": [ + { + "description": "MikroTik username", + "isRequired": false, + "format": "string", + "isSecret": false, + "name": "MK_USER" + }, + { + "description": "MikroTik password", + "isRequired": false, + "format": "string", + "isSecret": true, + "name": "MK_PASSWORD" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/mikrotik/server.ts b/src/mikrotik/server.ts new file mode 100644 index 0000000000..e37808c86e --- /dev/null +++ b/src/mikrotik/server.ts @@ -0,0 +1,142 @@ +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import { callToolHandler, listResourcesHandler, readResourceHandler, initializeApi } from "./handlers.js"; +import { tools } from "./tools.js"; + +// Create server instance +const server = new McpServer({ + name: "mikrotik-api", + version: "1.0.6", +}); + +const promptsData = [ + { name: "mk-connect: Connect to MikroTik", description: "connect to MikroTik using host, user and password" }, + { name: "mk-report: System Report", description: "show a comprehensive system report" }, + { name: "mk-get-route: Routing Table", description: "print ip/route to show the routing table" }, + { name: "mk-get-interface: List Interfaces", description: "print interface to list all interfaces" }, + { name: "mk-get-log: View Logs", description: "print log to view system logs" }, + { name: "mk-awr: Security Audit", description: "audit router's security and performance" } +]; + +// Register Prompts +server.registerPrompt("mikrotik-prompts", { + description: "List available MikroTik prompts" +}, async () => ({ + messages: [ + { + role: "assistant", + content: { + type: "text", + text: "Available MikroTik prompts:\n" + promptsData.map(p => `- ${p.name}: ${p.description}`).join("\n") + } + } + ] +})); + +let lock: Promise = Promise.resolve(); + +async function executeSequential(fn: () => Promise): Promise { + const result = (async () => { + try { + await lock; + } catch (e) { + // Ignore errors from previous commands to let the next one run + } + return fn(); + })(); + lock = result.catch(() => { }); + return result; +} + +// Register Resource Templates +const ifaceTemplate = new ResourceTemplate("mikrotik://interface/{name}", { + list: async () => executeSequential(() => listResourcesHandler({} as any)) +}); +const bridgeTemplate = new ResourceTemplate("mikrotik://bridge/{name}", { + list: async () => executeSequential(() => listResourcesHandler({} as any)) +}); +const bridgePortTemplate = new ResourceTemplate("mikrotik://bridge/{bridge}/{port}", { + list: async () => executeSequential(() => listResourcesHandler({} as any)) +}); +const routeTemplate = new ResourceTemplate("mikrotik://route/{id}", { + list: async () => executeSequential(() => listResourcesHandler({} as any)) +}); + +server.registerResource("Interface", ifaceTemplate, { description: "MikroTik interface information" }, async (uri: URL) => { + return executeSequential(() => readResourceHandler({ params: { uri: uri.toString() } } as any)); +}); +server.registerResource("Bridge", bridgeTemplate, { description: "MikroTik bridge information" }, async (uri: URL) => { + return executeSequential(() => readResourceHandler({ params: { uri: uri.toString() } } as any)); +}); +server.registerResource("Bridge Port", bridgePortTemplate, { description: "MikroTik bridge port information" }, async (uri: URL) => { + return executeSequential(() => readResourceHandler({ params: { uri: uri.toString() } } as any)); +}); +server.registerResource("Route", routeTemplate, { description: "MikroTik routing information" }, async (uri: URL) => { + return executeSequential(() => readResourceHandler({ params: { uri: uri.toString() } } as any)); +}); + +// Register Tools +tools.forEach((tool: any) => { + // Basic mapping of JSON schema to Zod for simple cases + let inputSchema: any = z.object({}); + if (tool.inputSchema && tool.inputSchema.properties) { + const shape: Record = {}; + for (const [key, prop] of Object.entries(tool.inputSchema.properties)) { + let field: any = z.any(); + if ((prop as any).type === "string") { + field = z.string(); + } else if ((prop as any).type === "boolean") { + field = z.boolean(); + } + + if ((prop as any).description) { + field = field.describe((prop as any).description); + } + + if (tool.inputSchema.required && !(tool.inputSchema.required as string[]).includes(key)) { + field = field.optional(); + } else if (!tool.inputSchema.required) { + field = field.optional(); + } + shape[key] = field; + } + inputSchema = z.object(shape); + } + + server.registerTool( + tool.name, + { + description: tool.description, + inputSchema: inputSchema + }, + async (args: any) => { + return executeSequential(() => callToolHandler({ params: { name: tool.name, arguments: args } } as any)); + } + ); +}); + +export async function runServer() { + const args = process.argv.slice(2); + const host = args[0]; + const secure = args[1] === "true"; + + if (host) { + try { + await initializeApi(host, undefined, undefined, secure); + } catch (error) { + console.error("Failed to connect to MikroTik:", error); + } + } else { + console.error("Warning: No MikroTik host provided. Use mk-connect tool before using other functionality."); + } + + const transport = new StdioServerTransport(); + await server.connect(transport); + + process.stdin.on("close", () => { + console.error("Mikrotik MCP Server closed"); + server.close(); + process.exit(0); + }); +} diff --git a/src/mikrotik/tools.ts b/src/mikrotik/tools.ts new file mode 100644 index 0000000000..4b93b6b45c --- /dev/null +++ b/src/mikrotik/tools.ts @@ -0,0 +1,47 @@ +export const tools = [ + { + name: "mk-connect", + description: "Connect to the MikroTik router", + inputSchema: { + type: "object", + properties: { + host: { type: "string", description: "Router IP address" }, + user: { type: "string", description: "Username" }, + password: { type: "string", description: "Password" }, + secure: { type: "boolean", description: "Use secure connection (TLS/SSL)", default: false }, + }, + required: ["host", "user", "password"], + }, + }, + { + name: "mk-report", + description: "Generates a comprehensive system report including resource usage, health, and interface traffic statistics.", + inputSchema: { + type: "object", + properties: {}, + }, + }, + { + name: "mk-get", + description: "Returns a JSON array with the result of a MikroTik API /print command. For example /ip/route stand for /ip/route/print", + inputSchema: { + type: "object", + properties: { + sentence: { + type: "string", + description: "The API path (e.g., /ip/route, /interface). '/print' will be automatically appended." + }, + }, + required: ["sentence"], + }, + }, + { + name: "mk-awr", + description: "Generates an Automatic Workload Repository (AWR) style report for MikroTik, including performance metrics, security audit, and recommendations.", + inputSchema: { + type: "object", + properties: {}, + }, + }, +]; + diff --git a/src/mikrotik/tools/api.ts b/src/mikrotik/tools/api.ts new file mode 100644 index 0000000000..8143f0fea6 --- /dev/null +++ b/src/mikrotik/tools/api.ts @@ -0,0 +1,234 @@ +import net from 'net'; +import tls from 'tls'; +import crypto from 'crypto'; + +export type Word = string; +export type Sentence = Word[]; +export type ReplyAttributes = { [key: string]: string }; +export type Reply = { + command: string; + attributes: ReplyAttributes; +}; + +export class MikroTikApi { + private socket: net.Socket | null = null; + private buffer: Buffer = Buffer.alloc(0); + private resolveRead: ((sentence: Sentence) => void) | null = null; + private rejectRead: ((err: Error) => void) | null = null; + private debug: boolean = false; + + constructor(options: { debug?: boolean } = {}) { + this.debug = !!options.debug; + } + + async connect(host: string, port?: number, secure: boolean = false): Promise { + const targetPort = port || (secure ? 8729 : 8728); + return new Promise((resolve, reject) => { + const onConnect = () => { + if (this.debug) console.log(`Connected to ${host}:${targetPort}`); + resolve(); + }; + + const onError = (err: Error) => { + if (this.debug) console.error('Socket error:', err); + if (this.rejectRead) { + this.rejectRead(err); + this.rejectRead = null; + } + reject(err); + }; + + if (secure) { + this.socket = tls.connect(targetPort, host, { + rejectUnauthorized: false, + // Specific ciphers if needed, like in python example: ciphers="ECDHE-RSA-AES256-GCM-SHA384" + }, onConnect); + } else { + this.socket = net.connect(targetPort, host, onConnect); + } + + this.socket.on('data', (data) => { + this.buffer = Buffer.concat([this.buffer, data]); + this.processBuffer(); + }); + + this.socket.on('error', onError); + + this.socket.on('end', () => { + if (this.debug) console.log('Connection ended'); + if (this.rejectRead) { + this.rejectRead(new Error('Connection closed')); + this.rejectRead = null; + } + }); + }); + } + + private processBuffer() { + while (this.resolveRead) { + const sentence = this.readSentenceFromBuffer(); + if (sentence) { + const resolve = this.resolveRead; + this.resolveRead = null; + this.rejectRead = null; + resolve(sentence); + } else { + break; + } + } + } + + private readSentenceFromBuffer(): Sentence | null { + let offset = 0; + const words: Sentence = []; + + while (true) { + const { length, bytesRead } = this.decodeLength(this.buffer.slice(offset)); + if (length === null) return null; // Need more data for length + + offset += bytesRead; + if (length === 0) { + this.buffer = this.buffer.slice(offset); + return words; + } + + if (this.buffer.length < offset + length) return null; // Need more data for word + + const word = this.buffer.slice(offset, offset + length).toString('utf8'); + words.push(word); + offset += length; + } + } + + private decodeLength(buf: Buffer): { length: number | null; bytesRead: number } { + if (buf.length === 0) return { length: null, bytesRead: 0 }; + const c = buf[0]; + if ((c & 0x80) === 0x00) { + return { length: c, bytesRead: 1 }; + } else if ((c & 0xC0) === 0x80) { + if (buf.length < 2) return { length: null, bytesRead: 0 }; + return { length: ((c & 0x3F) << 8) + buf[1], bytesRead: 2 }; + } else if ((c & 0xE0) === 0xC0) { + if (buf.length < 3) return { length: null, bytesRead: 0 }; + return { length: ((c & 0x1F) << 16) + (buf[1] << 8) + buf[2], bytesRead: 3 }; + } else if ((c & 0xF0) === 0xE0) { + if (buf.length < 4) return { length: null, bytesRead: 0 }; + return { length: ((c & 0x0F) << 24) + (buf[1] << 16) + (buf[2] << 8) + buf[3], bytesRead: 4 }; + } else if ((c & 0xF8) === 0xF0) { + if (buf.length < 5) return { length: null, bytesRead: 0 }; + return { length: (buf[1] << 24) + (buf[2] << 16) + (buf[3] << 8) + buf[4], bytesRead: 5 }; + } + return { length: null, bytesRead: 0 }; + } + + private encodeLength(len: number): Buffer { + if (len < 0x80) { + return Buffer.from([len]); + } else if (len < 0x4000) { + return Buffer.from([(len >> 8) | 0x80, len & 0xFF]); + } else if (len < 0x200000) { + return Buffer.from([(len >> 16) | 0xC0, (len >> 8) & 0xFF, len & 0xFF]); + } else if (len < 0x10000000) { + return Buffer.from([(len >> 24) | 0xE0, (len >> 16) & 0xFF, (len >> 8) & 0xFF, len & 0xFF]); + } else { + return Buffer.from([0xF0, (len >> 24) & 0xFF, (len >> 16) & 0xFF, (len >> 8) & 0xFF, len & 0xFF]); + } + } + + async writeSentence(words: Sentence): Promise { + if (!this.socket) throw new Error('Not connected'); + for (const word of words) { + if (this.debug) console.log(`<<< ${word}`); + const buf = Buffer.from(word, 'utf8'); + this.socket.write(this.encodeLength(buf.length)); + this.socket.write(buf); + } + this.socket.write(Buffer.from([0])); // End of sentence + } + + async readSentence(): Promise { + if (this.resolveRead) throw new Error('Already waiting for a sentence'); + return new Promise((resolve, reject) => { + this.resolveRead = (sentence) => { + if (this.debug) sentence.forEach(w => console.log(`>>> ${w}`)); + resolve(sentence); + }; + this.rejectRead = reject; + this.processBuffer(); + }); + } + + async talk(words: Sentence): Promise { + await this.writeSentence(words); + const replies: Reply[] = []; + while (true) { + const sentence = await this.readSentence(); + if (sentence.length === 0) continue; + const reply: Reply = { + command: sentence[0], + attributes: {} + }; + for (let i = 1; i < sentence.length; i++) { + const word = sentence[i]; + const eqIdx = word.indexOf('=', 1); + let key: string; + let value: string; + + if (eqIdx === -1) { + key = word; + value = ''; + } else { + key = word.slice(0, eqIdx); + value = word.slice(eqIdx + 1); + } + + // Strip leading '=' or '.' from key + if (key.startsWith('=') || key.startsWith('.')) { + key = key.slice(1); + } + + reply.attributes[key] = value; + } + replies.push(reply); + if (reply.command === '!done') return replies; + if (reply.command === '!trap') { + // Usually followed by !done, but we should keep reading just in case + continue; + } + if (reply.command === '!fatal') { + const message = reply.attributes['message'] || 'Fatal error'; + throw new Error(message); + } + } + } + + async login(username: string, password: string): Promise { + const replies = await this.talk(['/login', `=name=${username}`, `=password=${password}`]); + + if (replies.some(r => r.command === '!trap')) return false; + + const lastReply = replies[replies.length - 1]; + + if (lastReply.attributes['ret']) { + // Legacy MD5 login + const challenge = Buffer.from(lastReply.attributes['ret'], 'hex'); + const md = crypto.createHash('md5'); + md.update(Buffer.from([0])); + md.update(password); + md.update(challenge); + const response = md.digest('hex'); + + const replies2 = await this.talk(['/login', `=name=${username}`, `=response=00${response}`]); + return !replies2.some(r => r.command === '!trap'); + } + + return lastReply.command === '!done'; + } + + close() { + if (this.socket) { + this.socket.destroy(); + this.socket = null; + } + } +} diff --git a/src/mikrotik/tools/awr.ts b/src/mikrotik/tools/awr.ts new file mode 100644 index 0000000000..064909f18f --- /dev/null +++ b/src/mikrotik/tools/awr.ts @@ -0,0 +1,144 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { getApi } from "../db.js"; + +export const awrHandler = async (request: CallToolRequest) => { + const currentApi = getApi(); + if (!currentApi) { + return { + content: [{ type: "text", text: "Not connected. Call mk-connect tool first." }], + isError: true, + }; + } + + try { + const report: any = { + timestamp: new Date().toISOString(), + system: {}, + resources: {}, + network: {}, + security: {}, + recommendations: [], + }; + + // 1. Identity & Resources + const idReplies = await currentApi.talk(["/system/identity/print"]); + report.system.identity = idReplies.filter(r => r.command === "!re").map(r => r.attributes)[0] || {}; + + const resReplies = await currentApi.talk(["/system/resource/print"]); + report.resources = resReplies.filter(r => r.command === "!re").map(r => r.attributes)[0] || {}; + + const rbReplies = await currentApi.talk(["/system/routerboard/print"]); + report.system.routerboard = rbReplies.filter(r => r.command === "!re").map(r => r.attributes)[0] || {}; + + // 2. Services Audit + const svcReplies = await currentApi.talk(["/ip/service/print"]); + const services = svcReplies.filter(r => r.command === "!re").map(r => r.attributes); + report.security.services = services; + + // 3. DNS Audit + const dnsReplies = await currentApi.talk(["/ip/dns/print"]); + const dns = dnsReplies.filter(r => r.command === "!re").map(r => r.attributes)[0] || {}; + report.security.dns = dns; + + // 4. Neighbors (Potential Leak) + const nbReplies = await currentApi.talk(["/ip/neighbor/print"]); + report.security.neighbors = nbReplies.filter(r => r.command === "!re").map(r => r.attributes); + + // 5. Interface Summary + const intReplies = await currentApi.talk(["/interface/print"]); + const interfaces = intReplies.filter(r => r.command === "!re").map(r => r.attributes); + report.network.interfaces = interfaces.map(i => ({ + name: i.name, + type: i.type, + actual_mtu: i["actual-mtu"], + running: i.running, + disabled: i.disabled + })); + + // 6. Log Audit (Looking for scans/suspicious activity) + const logReplies = await currentApi.talk(["/log/print"]); + const logs = logReplies.filter(r => r.command === "!re").map(r => r.attributes); + const suspiciousPatterns = [ + { key: 'failedLogins', pattern: 'login failed' }, + { key: 'insecureLogins', pattern: 'via telnet' }, + { key: 'ftpLogins', pattern: 'via ftp' }, + { key: 'connectionProbes', pattern: 'tcp connection established from' } + ]; + + const suspiciousSummary: any = { + failedLogins: { count: 0, lastSeen: null, examples: [] }, + insecureLogins: { count: 0, lastSeen: null, examples: [] }, + ftpLogins: { count: 0, lastSeen: null, examples: [] }, + connectionProbes: { count: 0, lastSeen: null, examples: [] } + }; + + suspiciousPatterns.forEach(p => { + const matches = logs.filter(l => l.message.toLowerCase().includes(p.pattern)); + if (matches.length > 0) { + suspiciousSummary[p.key] = { + count: matches.length, + lastSeen: matches[matches.length - 1].time, + examples: matches.slice(-3).map(m => m.message) + }; + } + }); + report.security.suspiciousActivity = suspiciousSummary; + + // 7. Generate Recommendations + const recommendations: string[] = []; + + // CPU & Memory + if (parseInt(report.resources.cpu_load) > 80) { + recommendations.push("High CPU load detected (>80%). Check processes and firewall rules."); + } + const freeMem = parseInt(report.resources["free-memory"]) || 0; + const totalMem = parseInt(report.resources["total-memory"]) || 1; + if (freeMem / totalMem < 0.1) { + recommendations.push("Low free memory available (<10%). Possible leak or over-subscription."); + } + + // Security - Insecure Services + const insecureServices = services.filter(s => + s.disabled === "false" && ["telnet", "ftp", "www"].includes(s.name) + ); + if (insecureServices.length > 0) { + recommendations.push(`Insecure services enabled: ${insecureServices.map(s => s.name).join(", ")}. Consider disabling them or switching to encrypted alternatives (SSH, SSL).`); + } + + // Security - DNS Open Resolver + if (dns["allow-remote-requests"] === "true") { + recommendations.push("DNS 'allow-remote-requests' is enabled. Ensure you have firewall rules to prevent being used as an open resolver from WAN."); + } + + // Security - Neighbors + if (report.security.neighbors.length > 0) { + recommendations.push("Neighbors discovery is active and found devices. Ensure neighbor discovery is disabled on public-facing (WAN) interfaces."); + } + + // Security - Log Analysis + if (suspiciousSummary.failedLogins.count > 0) { + recommendations.push(`Detected ${suspiciousSummary.failedLogins.count} failed login attempts. Check for brute-force attacks and implement 'fail2ban' style firewall rules.`); + } + if (suspiciousSummary.insecureLogins.count > 0 || suspiciousSummary.ftpLogins.count > 0) { + recommendations.push("Logins via insecure protocols (Telnet/FTP) detected in logs. Disable these services immediately to prevent credential sniffing."); + } + if (suspiciousSummary.connectionProbes.count > 0) { + recommendations.push(`Connection probes detected (${suspiciousSummary.connectionProbes.count}). Review firewall input chain and consider blacklisting scan IPs.`); + } + + report.recommendations = recommendations; + + return { + content: [{ + type: "text", + text: JSON.stringify(report, null, 2), + }], + }; + + } catch (error: any) { + return { + content: [{ type: "text", text: `Error generating AWR report: ${error.message}` }], + isError: true, + }; + } +}; diff --git a/src/mikrotik/tools/cli.ts b/src/mikrotik/tools/cli.ts new file mode 100644 index 0000000000..0e1151beed --- /dev/null +++ b/src/mikrotik/tools/cli.ts @@ -0,0 +1,77 @@ +import { MikroTikApi } from './api.js'; +import * as readline from 'readline/promises'; +import { stdin as input, stdout as output } from 'process'; + +async function main() { + const args = process.argv.slice(2); + if (args.length < 1) { + console.log('Usage: node cli.js [user] [pass] [secure]'); + console.log('Example: node cli.js 192.168.88.1 admin password false'); + return; + } + + const host = args[0]; + const user = args[1] || 'admin'; + const pass = args[2] || ''; + const secure = args[3] === 'true'; + + const api = new MikroTikApi({ debug: false }); + try { + console.log(`Connecting to ${host}...`); + await api.connect(host, undefined, secure); + + console.log(`Logging in as ${user}...`); + if (!await api.login(user, pass)) { + console.error('Login failed'); + api.close(); + return; + } + console.log('Login successful'); + + const rl = readline.createInterface({ input, output }); + let sentence: string[] = []; + + console.log('Enter command lines (e.g. /system/identity/print), then an empty line to send.'); + while (true) { + const line = await rl.question('> '); + if (line === '') { + if (sentence.length > 0) { + try { + const replies = await api.talk(sentence); + const results = replies + .filter(r => r.command === '!re') + .map(r => r.attributes); + + if (results.length > 0) { + console.log(JSON.stringify(results, null, 2)); + } + + const trap = replies.find(r => r.command === '!trap'); + if (trap) { + console.error('Error (!trap):', JSON.stringify(trap.attributes, null, 2)); + } + + const done = replies.find(r => r.command === '!done'); + if (done && Object.keys(done.attributes).length > 0) { + console.log('Info (!done):', JSON.stringify(done.attributes, null, 2)); + } + } catch (err) { + const message = (err as Error).message; + console.error('Command failed:', message); + if (message.includes('connection closed') || message.includes('Connection closed') || message.includes('ended')) { + api.close(); + process.exit(0); + } + } + sentence = []; + } + } else { + sentence.push(line); + } + } + } catch (err) { + console.error('Connection error:', (err as Error).message); + } +} + +main(); diff --git a/src/mikrotik/tools/connect.ts b/src/mikrotik/tools/connect.ts new file mode 100644 index 0000000000..3ef49121da --- /dev/null +++ b/src/mikrotik/tools/connect.ts @@ -0,0 +1,29 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { MikroTikApi } from "./api.js"; +import { setApi } from "../db.js"; + +export const connectHandler = async (request: CallToolRequest) => { + const { host, user, password, secure = false } = request.params.arguments as any; + const newApi = new MikroTikApi({ debug: false }); + try { + await newApi.connect(host, undefined, secure); + const loggedIn = await newApi.login(user, password); + if (!loggedIn) { + newApi.close(); + return { + content: [{ type: "text", text: "Login failed: invalid username or password" }], + isError: true, + }; + } + setApi(newApi, { host, user, secure }); + return { + content: [{ type: "text", text: `Connected successfully to ${host}` }], + }; + } catch (error: any) { + newApi.close(); + return { + content: [{ type: "text", text: `Connection error: ${error.message}` }], + isError: true, + }; + } +}; diff --git a/src/mikrotik/tools/get.ts b/src/mikrotik/tools/get.ts new file mode 100644 index 0000000000..2617ca435f --- /dev/null +++ b/src/mikrotik/tools/get.ts @@ -0,0 +1,35 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { getApi } from "../db.js"; + +export const getHandler = async (request: CallToolRequest) => { + let { sentence } = request.params.arguments as any; + if (!sentence.startsWith('/')) { + sentence = '/' + sentence; + } + if (sentence.endsWith('/print')) { + sentence = sentence.slice(0, -6); + } + const command = sentence + '/print'; + + const currentApi = getApi(); + if (!currentApi) { + return { + content: [{ type: "text", text: "Not connected to MikroTik. Use mk-connect first." }], + isError: true, + }; + } + try { + const replies = await currentApi.talk([command]); + const results = replies + .filter((r) => r.command === "!re") + .map((r) => r.attributes); + return { + content: [{ type: "text", text: JSON.stringify(results, null, 2) }], + }; + } catch (error: any) { + return { + content: [{ type: "text", text: `Error executing command: ${error.message}` }], + isError: true, + }; + } +}; diff --git a/src/mikrotik/tools/report.ts b/src/mikrotik/tools/report.ts new file mode 100644 index 0000000000..8a822b0d7b --- /dev/null +++ b/src/mikrotik/tools/report.ts @@ -0,0 +1,68 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { getApi } from "../db.js"; + +export const reportHandler = async (request: CallToolRequest) => { + const currentApi = getApi(); + if (!currentApi) { + return { + content: [{ type: "text", text: "Not connected to MikroTik. Use mk-connect first." }], + isError: true, + }; + } + + try { + const report: any = {}; + + // 1. System Resources + const resReplies = await currentApi.talk(["/system/resource/print"]); + report.resources = resReplies.filter(r => r.command === "!re").map(r => r.attributes)[0] || {}; + + // 2. Routerboard Info + const rbReplies = await currentApi.talk(["/system/routerboard/print"]); + report.routerboard = rbReplies.filter(r => r.command === "!re").map(r => r.attributes)[0] || {}; + + // 3. System Health (might fail on some boards) + try { + const healthReplies = await currentApi.talk(["/system/health/print"]); + report.health = healthReplies.filter(r => r.command === "!re").map(r => r.attributes); + } catch (e) { + report.health = "Not available or error: " + (e as Error).message; + } + + // 4. Interfaces and Traffic + const intReplies = await currentApi.talk(["/interface/print"]); + const interfaces = intReplies.filter(r => r.command === "!re").map(r => r.attributes); + report.interfaces = interfaces; + + const runningInterfaces = interfaces.filter(i => i.running === "true" && i.disabled === "false").map(i => i.name); + + if (runningInterfaces.length > 0) { + report.traffic = {}; + for (const name of runningInterfaces) { + try { + const trafficReplies = await currentApi.talk(["/interface/monitor-traffic", `=interface=${name}`, "=once="]); + report.traffic[name] = trafficReplies.filter(r => r.command === "!re").map(r => r.attributes)[0] || {}; + } catch (e) { + report.traffic[name] = "Error: " + (e as Error).message; + } + } + } + + // 5. CPU Profile (brief snapshot if possible) + try { + const profileReplies = await currentApi.talk(["/tool/profile", "=once="]); + report.cpuProfile = profileReplies.filter(r => r.command === "!re").map(r => r.attributes); + } catch (e) { + report.cpuProfile = "Not available or error: " + (e as Error).message; + } + + return { + content: [{ type: "text", text: JSON.stringify(report, null, 2) }], + }; + } catch (error: any) { + return { + content: [{ type: "text", text: `Error generating report: ${error.message}` }], + isError: true, + }; + } +}; diff --git a/src/mikrotik/tsconfig.json b/src/mikrotik/tsconfig.json new file mode 100644 index 0000000000..23344452cf --- /dev/null +++ b/src/mikrotik/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": ".", + "moduleResolution": "NodeNext", + "module": "NodeNext" + }, + "include": [ + "./**/*.ts" + ] +} \ No newline at end of file diff --git a/src/mysql/.dockerignore b/src/mysql/.dockerignore new file mode 100644 index 0000000000..8d2924846b --- /dev/null +++ b/src/mysql/.dockerignore @@ -0,0 +1,3 @@ +dist +node_modules +*.js.map diff --git a/src/mysql/AWR_example.md b/src/mysql/AWR_example.md new file mode 100644 index 0000000000..6ec523873e --- /dev/null +++ b/src/mysql/AWR_example.md @@ -0,0 +1,72 @@ +## MySQL AWR in action + +**Sample prompts**: +- Generate a performance report using mysql-awr +- Model Claude Sonnet 4.5 (Thinking) + +Here is an example of a performance report generated by the `mysql-awr` tool, highlighting specific optimization opportunities: + +### **Database Configuration** +- **Version**: MariaDB 10.5.2 +- **Buffer Pool Size**: 256 MB +- **Max Connections**: 100 +- **Table Open Cache**: 400 +- **Query Cache**: Enabled (64 MB) + +### **Performance Metrics** + +#### **InnoDB Buffer Pool** +- **Hit Ratio**: 99.94% ✅ (Excellent - most reads from memory) +- **Read Requests**: 11.3M +- **Physical Reads**: 6,278 + +#### **Table Cache** +- **Hit Ratio**: 68.02% ⚠️ (Low - tables are being opened frequently) +- **Cache Hits**: 8,135 +- **Cache Misses**: 3,824 + +#### **Temporary Tables** +- **Total Created**: 1,119 +- **Created on Disk**: 459 (41.02%) ⚠️ (High - indicates memory pressure) + +### **Top Queries by Execution Time** + +#### **1. Ticket Statistics by Time Period** (0.76s) +```sql +SELECT FROM_UNIXTIME(UNIX_TIMESTAMP(glpi_tickets_distinct.date), ?) AS PERIOD, + SUM(IF(glpi_tickets_distinct.status = ?, ...)) ... +FROM (SELECT DISTINCTROW glpi_tickets.* FROM glpi_tickets + WHERE glpi_tickets.is_deleted = ?) AS glpi_tickets_distinct +GROUP BY period ORDER BY period ASC +``` +- **Rows Examined**: 33,004 +- **Rows Returned**: 156 +- **Issues**: Full table scan, disk temp table, no index used +- **Optimization**: Add index on `(is_deleted, date)` + +#### **2. Ticket Categorization Report** (0.47s) +```sql +SELECT glpi_itilcategories.completename, COUNT(DISTINCTROW(glpi_tickets.id)) +FROM glpi_tickets +LEFT JOIN glpi_itilcategories ON glpi_itilcategories.id = glpi_tickets.itilcategories_id +WHERE glpi_tickets.is_deleted = ? +GROUP BY glpi_itilcategories.completename +``` +- **Rows Examined**: 16,502 +- **Optimization**: Add composite index on `(is_deleted, itilcategories_id)` + +### **Recommendations** + +1. **Increase Temporary Table Memory** 🔴 **High Priority** + - Current: 41% of temp tables spill to disk + - Action: Increase `tmp_table_size` and `max_heap_table_size` to 64-128MB + +2. **Expand Table Open Cache** 🟡 **Medium Priority** + - Current hit ratio: 68% + - Action: Increase `table_open_cache` from 400 to at least 1000 + +3. **Add Missing Indexes** 🔴 **High Priority** + ```sql + ALTER TABLE glpi_tickets ADD INDEX idx_deleted_date (is_deleted, date); + ALTER TABLE glpi_tickets ADD INDEX idx_deleted_category (is_deleted, itilcategories_id); + ``` diff --git a/src/mysql/CHANGELOG.md b/src/mysql/CHANGELOG.md new file mode 100644 index 0000000000..90e2383624 --- /dev/null +++ b/src/mysql/CHANGELOG.md @@ -0,0 +1,133 @@ +## Change Log + +### 2026-03-11 +- **chore**: Bump server version to 1.0.7 + - Updated version to 1.0.7 across package.json, server.json, and server.ts + - Migrated to `McpServer` API from deprecated `Server` class + - Refactored resources into separate `resources.ts` for better modularity + +### 2026-03-07 +- **chore**: Bump server version to 1.0.6 + - Updated version to 1.0.6 across package.json, server.json, and server.ts + - Refactored resources into separate `resources.ts` for better modularity. + + +### 2026-01-22 +- **chore**: Bump server version to 1.0.5 + - Updated version to 1.0.5 across package.json, server.json, and server.ts + - Refactored prompt names to be more descriptive for better CLI visibility + +- **chore**: Bump server version to 1.0.4 + - Updated version to 1.0.4 across package.json, server.json, and server.ts + +### 2026-01-07 +- **feat**: Make initial connection string optional at startup + - Modified `runServer` to allow server startup without a database URL + - Added warning message when starting without a connection string + - Updated error messages to guide users to use the `mysql-connect` tool + - Updated README with documentation for optional connection string and `mysql-connect` tool usage + +### 2025-12-12 +- **chore**: Bump server version to 1.0.3 + - Updated version to 1.0.3 across package.json, server.json, and server.ts + - Added link to Demos.md in README for comprehensive usage examples + - Published package @marcelo-ochoa/server-mysql@1.0.3 to npm registry + - Rebuilt Docker image mochoa/mcp-mysql with updated functionality + +- **chore**: Bump server version to 1.0.2 + - Updated version to 1.0.2 across package.json, server.json, and server.ts + - Updated LICENSE link in README to point to GitHub repository + - Published package @marcelo-ochoa/server-mysql@1.0.2 to npm registry + +- **docs**: Add comprehensive demo documentation and HR sample schema + - Added Demos.md with usage examples for: + - Claude Desktop (Docker and NPX configurations) + - Docker AI integration + - Gemini CLI usage + - Antigravity Code Editor setup + - Added HR schema and data SQL scripts for MySQL/MariaDB: + - `hr_schema_mysql.sql` - Complete HR schema with tables (regions, countries, locations, departments, jobs, employees, job_history) + - `hr_data_mysql.sql` - Sample data for HR schema + - Comprehensive table documentation with comments + - Foreign key constraints and indexes for performance + - Enhanced README with better documentation organization + +### 2025-12-03 +- **feat**: Add prompts capability and list handler to MySQL server + - Updated version to 1.0.1 + - Added `prompts: {}` capability to server configuration + - Implemented `PromptsListRequestSchema` using zod for request validation + - Added prompts array with 5 MySQL-specific prompt templates: + - `mysql-query` - Example query execution + - `mysql-explain` - Query execution plan analysis + - `mysql-stats` - Table statistics retrieval + - `mysql-connect` - Database connection instructions + - `mysql-awr` - Performance report generation + - Added request handler for `prompts/list` endpoint + - Published package @marcelo-ochoa/server-mysql@1.0.1 to npm registry + +### 2025-12-01 +- **feat**: Bump server version to 1.0.0 + - Updated version to 1.0.0 across package.json, server.json, and server.ts + - Added AWR_example.md with performance report examples for GLPI database + - Added CHANGELOG.md for better change tracking + - Enhanced mysql-awr tool with improved reporting capabilities + - Updated README with MySQL AWR in action section + +### 2025-11-27 +- **chore**: Bump patch version in server.json + - Minor version update for server configuration + +- **feat**: Add `ListResourceTemplates` handler to MySQL server and enhance connection string parsing + - Enhanced server capabilities with resource template listing + - Improved connection string parsing in db.ts to support connection options + - Updated package dependencies + +- **docs**: Update README to document recent server.json definitions, version bumps, URL simplification, and graceful shutdown + - Comprehensive documentation updates + +- **feat**: Add server.json definitions and update versions for MySQL server + - Added server.json with MCP server metadata and schema + - Updated version to 0.1.2 + - Added mcpName field to package.json + - Configured environment variables (MYSQL_USER, MYSQL_PASSWORD) in server definition + +### 2025-11-26 +- **docs**: Update README to include recent server version bumps, URL simplification, and graceful shutdown + - Updated documentation with latest changes and improvements + +- **chore**: Bump MySQL server version to 0.1.1 + - Updated package versions to reflect recent improvements + - Synchronized package-lock.json with new versions + +- **feat**: Simplify database resource URLs and add graceful server shutdown on stdin close + - Simplified resource URL format from `mysql://database/table/schema` to cleaner format + - Added graceful shutdown handling for improved stability + - Enhanced db.ts with better connection management + +### Recent Updates + +- **2026-01-07** (762080d) + - Impl optional argument on run and environtment variable setup for initial connect + +- **2025-12-14** (d740363) + - update CHANGELOG.md files + +- **2025-12-12** (ba86d83) + - unifi release version and link to demos on MySQL/PostgreSQL + - Added server.json with MCP server metadata and schema + - Updated version to 0.1.2 + - Added mcpName field to package.json + - Configured environment variables (MYSQL_USER, MYSQL_PASSWORD) in server definition + +- **2025-11-26** (6dade3b) + - docs: Update READMEs to include recent server version bumps, URL simplification, and graceful shutdown + +- **2025-11-26** (ca32105) + - chore: Bump MySQL and PostgreSQL server versions to 0.1.1 and 0.6.4 + +- **2025-11-26** (d065d11) + - feat: Simplify database resource URLs and add graceful server shutdown on stdin close + +- **2025-11-25** (ca2c3fb) + - feat: Add new MySQL service with AWR, query, explain, and stats tools, along with updated CI/CD workflow and dependencies diff --git a/src/mysql/Demos.md b/src/mysql/Demos.md new file mode 100644 index 0000000000..f9764a46ab --- /dev/null +++ b/src/mysql/Demos.md @@ -0,0 +1,250 @@ +# Demos + +Some sample usage scenarios are shown below: + +## This Demo is using the HR Schema + +The HR schema is a sample schema that is not included in the MySQL distribution. It is a simple schema that contains a few tables and some sample data. + +To start a sample docker mysql container, run the following command: + +```sh +% docker run -d --name some-mysql -e MYSQL_ROOT_PASSWORD=my_2025 -p 3306:3306 mariadb:10 +``` + +To load the HR schema, run the following command: + +```sh +% cat hr_schema_mysql.sql|docker exec -i some-mysql mysql -u root -pmy_2025 +``` + +To load the HR data, run the following command: + +```sh +% cat hr_data_mysql.sql|docker exec -i some-mysql mysql -u root -pmy_2025 hr +``` + +## 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) +* Credentials are passed via environment variables `MYSQL_USER` and `MYSQL_PASSWORD` + +```json +{ + "mcpServers": { + "mysql": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "MYSQL_USER=root", + "-e", + "MYSQL_PASSWORD=my_2025", + "mochoa/mcp-mysql", + "host.docker.internal:3306/hr"] + } + } +} +``` + +### NPX + +```json +{ + "mcpServers": { + "mysql": { + "command": "npx", + "args": [ + "-y", + "@marcelo-ochoa/server-mysql", + "localhost:3306/hr" + ], + "env": { + "MYSQL_USER": "root", + "MYSQL_PASSWORD": "my_2025" + } + } + } +} +``` + +Replace `/hr` with your database name. + +### Demo Prompts + +Sample prompts using the converted HR schema for MySQL. + +- mysql-connect to host.docker.internal:3306/hr using root as user and my_2025 as password using mysql mcp server +- mysql-query SELECT c.country_name, l.city, COUNT(d.department_id) +FROM countries c +JOIN locations l ON c.country_id = l.country_id +JOIN departments d ON l.location_id = d.location_id +WHERE d.department_id IN + (SELECT e.department_id FROM employees e + GROUP BY e.department_id + HAVING COUNT(e.department_id) > 5) +GROUP BY c.country_name, l.city +- mysql-explain the execution plan +- visualize above execution plan in text mode +- mysql-stats of countries, locations and departments +- based on above table and index stats rewrite above query with a better execution plan +- visualize original and rewritten execution plan +- load resource mysql://hr/countries/schema +- mysql-awr + +## Using Docker AI + +[Ask Gordon](https://docs.docker.com/desktop/features/gordon/) is an AI assistant designed to streamline your Docker workflow by providing contextual assistance tailored to your local environment. Currently in Beta and available in Docker Desktop version 4.38.0 or later, Ask Gordon offers intelligent support for various Docker-related tasks. + +```sh +% cd src/mysql +% docker ai 'mysql-stats for table countries' + + • Calling stats ✔️ + + Here are the statistics for the COUNTRIES table: + + ### Table Statistics: + + • Schema: hr + • Table Name: countries + • Number of Rows: 25 + • Average Row Length: 655 bytes + • Last Analyzed: 2025-12-11 22:00:38 + + ### Index Statistics: + + • Index Name: PRIMARY + • Non Unique: 0 + • Cardinality: 25 + • Index Type: BTREE + + ### Column Statistics: + + 1. country_id: + + • Type: char(2) + • Nullable: NO + • Key: PRI + + 2. country_name: + + • Type: varchar(60) + • Nullable: YES + + 3. region_id: + + • Type: int + • Nullable: YES + • Key: MUL +``` + +Using this sample gordon-mcp.yml file in a current directory: + +```yml +services: + time: + image: mcp/time + mysql: + image: mochoa/mcp-mysql + command: ["host.docker.internal:3306/hr"] + environment: + - MYSQL_USER=root + - MYSQL_PASSWORD=my_2025 +``` + +## Using Gemini CLI + +[Gemini CLI](https://github.com/google-gemini/gemini-cli/) +is an open-source AI agent that brings the power of Gemini directly +into your terminal. It provides lightweight access to Gemini, giving you the +most direct path from your prompt to our model. + +Using this sample settings.json file at ~/.gemini/ directory: + +```json +{ + "mcpServers": { + "mysql": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "MYSQL_USER=root", + "-e", + "MYSQL_PASSWORD=my_2025", + "mochoa/mcp-mysql", + "host.docker.internal:3306/hr" + ] + } + }, + "security": { + "auth": { + "selectedType": "gemini-api-key" + } + }, + "ui": { + "theme": "ANSI" + }, + "selectedAuthType": "gemini-api-key", + "theme": "Dracula" +} +``` + +### Sample prompts with Gemini CLI + +- connect to host.docker.internal:3306/hr using root as user and my_2025 as password using mysql mcp server + +- mysql-query SELECT c.country_name, l.city, COUNT(d.department_id) + FROM countries c + JOIN locations l ON c.country_id = l.country_id + JOIN departments d ON l.location_id = d.location_id + WHERE d.department_id IN +   (SELECT e.department_id FROM employees e +    GROUP BY e.department_id +    HAVING COUNT(e.department_id) > 5) + GROUP BY c.country_name, l.city + +- mysql-explain the execution plan + +- visualize above execution plan in text mode + +- mysql-stats of countries, locations and departments + +- based on above table and index stats rewrite above query with a better execution plan + +- visualize original and rewritten execution plan + +## Using Antigravity Code Editor + +Put this in `~/.gemini/antigravity/mcp_config.json` + +```json +{ + "mcpServers": { + "mysql": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "MYSQL_USER=root", + "-e", + "MYSQL_PASSWORD=my_2025", + "mochoa/mcp-mysql", + "host.docker.internal:3306/hr" + ] + } + }, + "inputs": [] +} +``` diff --git a/src/mysql/Dockerfile b/src/mysql/Dockerfile new file mode 100644 index 0000000000..b4cb8ec174 --- /dev/null +++ b/src/mysql/Dockerfile @@ -0,0 +1,30 @@ +FROM node:slim AS builder + +COPY src/mysql /app +COPY tsconfig.json /tsconfig.json + +WORKDIR /app + +RUN --mount=type=cache,target=/root/.npm npm install + +RUN npm run build + +RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev + +FROM dhi.io/node:26-alpine-sfw-ent-dev AS release + +# Update and upgrade to fix OS-level vulnerabilities +RUN apk update && apk upgrade --no-cache + +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 /usr/bin/npm ci --ignore-scripts --omit-dev + +ENTRYPOINT ["node", "dist/index.js"] + diff --git a/src/mysql/LICENSE b/src/mysql/LICENSE new file mode 100644 index 0000000000..4a93985763 --- /dev/null +++ b/src/mysql/LICENSE @@ -0,0 +1,216 @@ +The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0. + +Contributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License. + +No rights beyond those granted by the applicable original license are conveyed for such contributions. + +--- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright + owner or by an individual or Legal Entity authorized to submit on behalf + of the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +--- + +MIT License + +Copyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +Creative Commons Attribution 4.0 International (CC-BY-4.0) + +Documentation in this project (excluding specifications) is licensed under +CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for +the full license text. diff --git a/src/mysql/README.md b/src/mysql/README.md new file mode 100644 index 0000000000..66989021e4 --- /dev/null +++ b/src/mysql/README.md @@ -0,0 +1,282 @@ +# MySQL Database + +A Model Context Protocol server that provides read-only access to MySQL databases. This server enables LLMs to inspect database schemas, execute and explain read-only queries, and analyze performance metrics. + +## Components + +### Tools + +- **mysql-query** + - Execute read-only SQL queries against the connected MySQL database + - Input: `sql` (string): The SQL query to execute + - All queries are executed within a READ ONLY transaction + +- **mysql-explain** + - Explain plan SQL queries against the connected MySQL database + - Input: `sql` (string): The SQL query to explain + - Returns execution plan in JSON format + +- **mysql-stats** + - Get statistics for a given table in the current connected database + - Input: `name` (string): The table name + - Returns comprehensive table, index, and column statistics + +- **mysql-connect** + - Reconnect using new credentials + - Input: `connectionString` (string): MySQL connect string (e.g., host.docker.internal:3306/mydb) + - Input: `user` (string): Username (e.g., root) + - Input: `password` (string): Password + +Example: + mysql-connect host.docker.internal:3306/hr root my_2025 + +- **mysql-awr** + - Generate a MySQL performance report similar to Oracle AWR + - Includes database statistics, InnoDB metrics, top queries (requires performance_schema), table/index statistics, connection info, and optimization recommendations + - No input required + +### Resources + +The server provides schema information for each table in the MySQL database: + +- **Table Schemas** (`mysql:////schema`) + - JSON schema information for each table + - Includes column names and data types + - Automatically discovered from MySQL database metadata + +## Configuration + +The MySQL server uses environment variables or the `mysql-connect` tool for secure credential management: + +- **`MYSQL_USER`**: MySQL username (optional if using `mysql-connect`) +- **`MYSQL_PASSWORD`**: MySQL password (optional if using `mysql-connect`) + +### Connection String + +The connection string should contain only the host, port, and database information (without embedded credentials). Providing it as a command-line argument is **optional**. If omitted at startup, you must use the `mysql-connect` tool to establish a connection before using other functionality. + +**Supported connection string formats:** +- `mysql://host:port/dbname` +- `host:port/dbname` + +## 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 MySQL server is running on the host network (e.g., localhost) +* Credentials are passed via environment variables `MYSQL_USER` and `MYSQL_PASSWORD` + +```json +{ + "mcpServers": { + "mysql": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", "MYSQL_USER=myuser", + "-e", "MYSQL_PASSWORD=mypassword", + "mochoa/mcp-mysql" + ] + } + } +} +``` + +Note: You can still provide the connection string as a final argument if you want to connect automatically on startup: `"args": [..., "mochoa/mcp-mysql", "host.docker.internal:3306/mydb"]`. + +### NPX + +```json +{ + "mcpServers": { + "mysql": { + "command": "npx", + "args": [ + "-y", + "@marcelo-ochoa/server-mysql" + ], + "env": { + "MYSQL_USER": "myuser", + "MYSQL_PASSWORD": "mypassword" + } + } + } +} +``` + +Replace `/mydb` with your database name. + +**Note**: Replace the following placeholders with your actual values: +- `myuser` and `mypassword` with your MySQL credentials +- `localhost:3306` with your MySQL server host and port +- `mydb` with your database name + +## Usage with VS Code + +For manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`. + +Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others. + +> Note that the `mcp` key is not needed in the `.vscode/mcp.json` file. + +### Docker + +**Note**: When using Docker and connecting to a MySQL server on your host machine, use `host.docker.internal` instead of `localhost` in the connection URL. + +```json +{ + "mcp": { + "inputs": [ + { + "type": "promptString", + "id": "mysql_user", + "description": "MySQL username" + }, + { + "type": "promptString", + "id": "mysql_password", + "description": "MySQL password", + "password": true + } + ], + "servers": { + "mysql": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", "MYSQL_USER=${input:mysql_user}", + "-e", "MYSQL_PASSWORD=${input:mysql_password}", + "mochoa/mcp-mysql" + ] + } + } + } +} +``` + +Note: You can add an input for `mysql_url` and append it to `args` if you want to connect on startup. + +### NPX + +```json +{ + "mcp": { + "inputs": [ + { + "type": "promptString", + "id": "mysql_user", + "description": "MySQL username" + }, + { + "type": "promptString", + "id": "mysql_password", + "description": "MySQL password", + "password": true + } + ], + "servers": { + "mysql": { + "command": "npx", + "args": [ + "-y", + "@marcelo-ochoa/server-mysql" + ], + "env": { + "MYSQL_USER": "${input:mysql_user}", + "MYSQL_PASSWORD": "${input:mysql_password}" + } + } + } + } +} +``` + +## Usage with Antigravity Code Editor + +Put this in `~/.gemini/antigravity/mcp_config.json` + +```json +{ + "mcpServers": { + "mysql": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "MYSQL_USER=myuser", + "-e", + "MYSQL_PASSWORD=mypassword", + "mochoa/mcp-mysql", + "host.docker.internal:3306/mydb" + ] + } + }, + "inputs": [] +} +``` + +## Performance Schema + +For optimal performance monitoring with the `mysql-awr` tool, ensure that the Performance Schema is enabled in your MySQL configuration: + +```ini +[mysqld] +performance_schema = ON +``` + +The Performance Schema provides detailed query statistics and performance metrics. If it's not enabled, the AWR report will still generate but with limited query-level statistics. + +## Building + +Docker: + +```sh +docker build -t mochoa/mcp-mysql -f src/mysql/Dockerfile . +``` + +NPM: + +```sh +cd src/mysql +npm install +npm run build +``` + +## Demo Prompts + +Sample prompts to try with the MySQL MCP server: + +- Connect to host.docker.internal:3306/mydb using root as user and password123 as password using mysql mcp server +- Query all tables in the current database +- Get stats for the `users` table +- Explain the execution plan for: SELECT * FROM users WHERE email = 'test@example.com' +- Generate a performance report using mysql-awr +- Based on the AWR report, what optimizations would you recommend? + +## Demos + +See [Demos](https://github.com/marcelo-ochoa/servers/blob/main/src/mysql/Demos.md) for usage examples with Claude Desktop, Docker AI, Gemini CLI, and Antigravity Code Editor. + +## MySQL AWR in action + +See [MySQL AWR in action](https://github.com/marcelo-ochoa/servers/blob/main/src/mysql/AWR_example.md) for an example of a performance report generated by the `mysql-awr` tool, highlighting specific optimization opportunities. + +## Change Log + +See [Change Log](https://github.com/marcelo-ochoa/servers/blob/main/src/mysql/CHANGELOG.md) for the history of changes. + +## Sources + +As usual, the code of this extension is at [GitHub](https://github.com/marcelo-ochoa/servers), feel free to suggest changes and make contributions. + +## 📜 License + +This project is licensed under the Apache License, Version 2.0 for new contributions, with existing code under MIT - see the [LICENSE](https://github.com/marcelo-ochoa/servers/blob/main/src/mysql/LICENSE) file for details. diff --git a/src/mysql/db.ts b/src/mysql/db.ts new file mode 100644 index 0000000000..9576df5f7f --- /dev/null +++ b/src/mysql/db.ts @@ -0,0 +1,144 @@ +import mysql from "mysql2/promise"; + +let pool: mysql.Pool | undefined = undefined; + +let resourceBaseUrl: URL | undefined = undefined; + +export async function initializePool(connectionString: string) { + const dbUser = process.env.MYSQL_USER; + const dbPassword = process.env.MYSQL_PASSWORD; + + if (!dbUser || !dbPassword) { + console.error( + "Error: Environment variables MYSQL_USER and MYSQL_PASSWORD must be set.", + ); + process.exit(1); + } + + // Parse the connection string to extract host, port, database, and options + // Expected format: mysql://host:port/dbname?options or host:port/dbname?options + let host: string; + let port: number; + let database: string; + let connectionOptions: Record = {}; + + try { + // Try parsing as URL first + if (connectionString.startsWith('mysql://')) { + const url = new URL(connectionString); + host = url.hostname; + port = url.port ? parseInt(url.port) : 3306; + database = url.pathname.slice(1); // Remove leading '/' + + // Parse query parameters for connection options + url.searchParams.forEach((value, key) => { + // Convert string values to appropriate types + if (value === 'true' || value === 'false') { + connectionOptions[key] = value === 'true'; + } else if (!isNaN(Number(value))) { + connectionOptions[key] = Number(value); + } else { + connectionOptions[key] = value; + } + }); + } else { + // Parse format: host:port/dbname or host:port/dbname?options + const [baseConnection, queryString] = connectionString.split('?'); + const match = baseConnection.match(/^([^:]+):(\d+)\/(.+)$/); + if (!match) { + throw new Error("Invalid connection string format. Expected: host:port/dbname?options or mysql://host:port/dbname?options"); + } + host = match[1]; + port = parseInt(match[2]); + database = match[3]; + + // Parse query string if present + if (queryString) { + const params = new URLSearchParams(queryString); + params.forEach((value, key) => { + // Convert string values to appropriate types + if (value === 'true' || value === 'false') { + connectionOptions[key] = value === 'true'; + } else if (!isNaN(Number(value))) { + connectionOptions[key] = Number(value); + } else { + connectionOptions[key] = value; + } + }); + } + } + } catch (err) { + console.error("Error parsing connection string:", err); + process.exit(1); + } + + // Handle special SSL option conversion + // ssl=0 or ssl=false means disable SSL + // ssl=1 or ssl=true means enable SSL with default settings (accepting self-signed certs) + if ('ssl' in connectionOptions) { + if (connectionOptions.ssl === 0 || connectionOptions.ssl === false) { + connectionOptions.ssl = false; + } else if (connectionOptions.ssl === 1 || connectionOptions.ssl === true) { + // MySQL2 requires ssl to be an object, not a boolean + connectionOptions.ssl = { + rejectUnauthorized: false // Accept self-signed certificates + }; + } + // If ssl is already an object or string (like a path), keep it as is + } + + pool = mysql.createPool({ + user: dbUser, + password: dbPassword, + host, + port, + database, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0, + ...connectionOptions, // Spread connection options from URL + }); + + // Test connection + const connection = await pool.getConnection(); + connection.release(); + + // Build resource base URL without credentials + const url = new URL(`mysql://${database}`); + resourceBaseUrl = url; +} + +export function isPoolInitialized(): boolean { + return pool !== undefined; +} + +export function getPool(): mysql.Pool { + if (!pool) { + throw new Error("MySQL connection pool not initialized. Use mysql-connect tool first."); + } + return pool; +} + +export function getResourceBaseUrl(): URL { + if (!resourceBaseUrl) { + throw new Error("Resource Base URL not initialized. Use mysql-connect tool first."); + } + return resourceBaseUrl; +} + +export async function withConnection(callback: (connection: mysql.PoolConnection) => Promise): Promise { + const pool = getPool(); + const connection = await pool.getConnection(); + try { + return await callback(connection); + } finally { + connection.release(); + } +} + +export async function closePool() { + if (pool) { + await pool.end(); + pool = undefined; + } +} diff --git a/src/mysql/handlers.ts b/src/mysql/handlers.ts new file mode 100644 index 0000000000..61f27af4bd --- /dev/null +++ b/src/mysql/handlers.ts @@ -0,0 +1,25 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { queryHandler } from "./tools/query.js"; +import { statsHandler } from "./tools/stats.js"; +import { connectHandler } from "./tools/connect.js"; +import { explainHandler } from "./tools/explain.js"; +import { awrHandler } from "./tools/awr.js"; + +export { listResourcesHandler, readResourceHandler } from "./resources.js"; + +const toolHandlers: Record Promise> = { + "mysql-query": queryHandler, + "mysql-stats": statsHandler, + "mysql-explain": explainHandler, + "mysql-connect": connectHandler, + "mysql-awr": awrHandler, +}; + +export const callToolHandler = async (request: CallToolRequest) => { + const handler = toolHandlers[request.params.name]; + if (handler) { + return handler(request); + } + throw new Error(`Unknown tool: ${request.params.name}`); +}; + diff --git a/src/mysql/hr_data_mysql.sql b/src/mysql/hr_data_mysql.sql new file mode 100644 index 0000000000..860ab193b4 --- /dev/null +++ b/src/mysql/hr_data_mysql.sql @@ -0,0 +1,305 @@ +-- ============================================================================ +-- MySQL Data Insert Script for HR Schema +-- Generated from Oracle/PostgreSQL HR Schema Data +-- Date: 2025-12-12 +-- Total Records: 218 rows across 7 tables +-- ============================================================================ + +-- Use HR database +USE hr; + +-- Disable foreign key checks temporarily for faster inserts and circular deps +SET FOREIGN_KEY_CHECKS = 0; + +-- ============================================================================ +-- Table: REGIONS (5 rows) +-- ============================================================================ +INSERT INTO regions (region_id, region_name) VALUES +(10, 'Europe'), +(20, 'Americas'), +(30, 'Asia'), +(40, 'Oceania'), +(50, 'Africa'); + +-- ============================================================================ +-- Table: COUNTRIES (25 rows) +-- ============================================================================ +INSERT INTO countries (country_id, country_name, region_id) VALUES +('AR', 'Argentina', 20), +('AU', 'Australia', 40), +('BE', 'Belgium', 10), +('BR', 'Brazil', 20), +('CA', 'Canada', 20), +('CH', 'Switzerland', 10), +('CN', 'China', 30), +('DE', 'Germany', 10), +('DK', 'Denmark', 10), +('EG', 'Egypt', 50), +('FR', 'France', 10), +('GB', 'United Kingdom of Great Britain and Northern Ireland', 10), +('IL', 'Israel', 30), +('IN', 'India', 30), +('IT', 'Italy', 10), +('JP', 'Japan', 30), +('KW', 'Kuwait', 30), +('ML', 'Malaysia', 30), +('MX', 'Mexico', 20), +('NG', 'Nigeria', 50), +('NL', 'Netherlands', 10), +('SG', 'Singapore', 30), +('US', 'United States of America', 20), +('ZM', 'Zambia', 50), +('ZW', 'Zimbabwe', 50); + +-- ============================================================================ +-- Table: LOCATIONS (23 rows) +-- ============================================================================ +INSERT INTO locations (location_id, street_address, postal_code, city, state_province, country_id) VALUES +(1000, '1297 Via Cola di Rie', '00989', 'Roma', NULL, 'IT'), +(1100, '93091 Calle della Testa', '10934', 'Venice', NULL, 'IT'), +(1200, '2017 Shinjuku-ku', '1689', 'Tokyo', 'Tokyo Prefecture', 'JP'), +(1300, '9450 Kamiya-cho', '6823', 'Hiroshima', NULL, 'JP'), +(1400, '2014 Jabberwocky Rd', '26192', 'Southlake', 'Texas', 'US'), +(1500, '2011 Interiors Blvd', '99236', 'South San Francisco', 'California', 'US'), +(1600, '2007 Zagora St', '50090', 'South Brunswick', 'New Jersey', 'US'), +(1700, '2004 Charade Rd', '98199', 'Seattle', 'Washington', 'US'), +(1800, '147 Spadina Ave', 'M5V 2L7', 'Toronto', 'Ontario', 'CA'), +(1900, '6092 Boxwood St', 'YSW 9T2', 'Whitehorse', 'Yukon', 'CA'), +(2000, '40-5-12 Laogianggen', '190518', 'Beijing', NULL, 'CN'), +(2100, '1298 Vileparle (E)', '490231', 'Bombay', 'Maharashtra', 'IN'), +(2200, '12-98 Victoria Street', '2901', 'Sydney', 'New South Wales', 'AU'), +(2300, '198 Clementi North', '540198', 'Singapore', NULL, 'SG'), +(2400, '8204 Arthur St', NULL, 'London', NULL, 'GB'), +(2500, 'Magdalen Centre, The Oxford Science Park', 'OX9 9ZB', 'Oxford', 'Oxford', 'GB'), +(2600, '9702 Chester Road', '09629850293', 'Stretford', 'Manchester', 'GB'), +(2700, 'Schwanthalerstr. 7031', '80925', 'Munich', 'Bavaria', 'DE'), +(2800, 'Rua Frei Caneca 1360 ', '01307-002', 'Sao Paulo', 'Sao Paulo', 'BR'), +(2900, '20 Rue des Corps-Saints', '1730', 'Geneva', 'Geneve', 'CH'), +(3000, 'Murtenstrasse 921', '3095', 'Bern', 'BE', 'CH'), +(3100, 'Pieter Breughelstraat 837', '3029SK', 'Utrecht', 'Utrecht', 'NL'), +(3200, 'Mariano Escobedo 9991', '11932', 'Mexico City', 'Distrito Federal', 'MX'); + +-- ============================================================================ +-- Table: JOBS (19 rows) +-- ============================================================================ +INSERT INTO jobs (job_id, job_title, min_salary, max_salary) VALUES +('AC_ACCOUNT', 'Public Accountant', 4200, 9000), +('AC_MGR', 'Accounting Manager', 8200, 16000), +('AD_ASST', 'Administration Assistant', 3000, 6000), +('AD_PRES', 'President', 20080, 40000), +('AD_VP', 'Administration Vice President', 15000, 30000), +('FI_ACCOUNT', 'Accountant', 4200, 9000), +('FI_MGR', 'Finance Manager', 8200, 16000), +('HR_REP', 'Human Resources Representative', 4000, 9000), +('IT_PROG', 'Programmer', 4000, 10000), +('MK_MAN', 'Marketing Manager', 9000, 15000), +('MK_REP', 'Marketing Representative', 4000, 9000), +('PR_REP', 'Public Relations Representative', 4500, 10500), +('PU_CLERK', 'Purchasing Clerk', 2500, 5500), +('PU_MAN', 'Purchasing Manager', 8000, 15000), +('SA_MAN', 'Sales Manager', 10000, 20080), +('SA_REP', 'Sales Representative', 6000, 12008), +('SH_CLERK', 'Shipping Clerk', 2500, 5500), +('ST_CLERK', 'Stock Clerk', 2008, 5000), +('ST_MAN', 'Stock Manager', 5500, 8500); + +-- ============================================================================ +-- Table: DEPARTMENTS (27 rows) +-- Note: manager_id will be updated after employees are inserted +-- ============================================================================ +INSERT INTO departments (department_id, department_name, manager_id, location_id) VALUES +(10, 'Administration', NULL, 1700), +(20, 'Marketing', NULL, 1800), +(30, 'Purchasing', NULL, 1700), +(40, 'Human Resources', NULL, 2400), +(50, 'Shipping', NULL, 1500), +(60, 'IT', NULL, 1400), +(70, 'Public Relations', NULL, 2700), +(80, 'Sales', NULL, 2500), +(90, 'Executive', NULL, 1700), +(100, 'Finance', NULL, 1700), +(110, 'Accounting', NULL, 1700), +(120, 'Treasury', NULL, 1700), +(130, 'Corporate Tax', NULL, 1700), +(140, 'Control And Credit', NULL, 1700), +(150, 'Shareholder Services', NULL, 1700), +(160, 'Benefits', NULL, 1700), +(170, 'Manufacturing', NULL, 1700), +(180, 'Construction', NULL, 1700), +(190, 'Contracting', NULL, 1700), +(200, 'Operations', NULL, 1700), +(210, 'IT Support', NULL, 1700), +(220, 'NOC', NULL, 1700), +(230, 'IT Helpdesk', NULL, 1700), +(240, 'Government Sales', NULL, 1700), +(250, 'Retail Sales', NULL, 1700), +(260, 'Recruiting', NULL, 1700), +(270, 'Payroll', NULL, 1700); + +-- ============================================================================ +-- Table: EMPLOYEES (107 rows) +-- ============================================================================ +INSERT INTO employees (employee_id, first_name, last_name, email, phone_number, hire_date, job_id, salary, commission_pct, manager_id, department_id) VALUES +(100, 'Steven', 'King', 'SKING', '1.515.555.0100', '2013-06-17', 'AD_PRES', 24000.00, NULL, NULL, 90), +(101, 'Neena', 'Yang', 'NYANG', '1.515.555.0101', '2015-09-21', 'AD_VP', 17000.00, NULL, 100, 90), +(102, 'Lex', 'Garcia', 'LGARCIA', '1.515.555.0102', '2011-01-13', 'AD_VP', 17000.00, NULL, 100, 90), +(103, 'Alexander', 'James', 'AJAMES', '1.590.555.0103', '2016-01-03', 'IT_PROG', 9000.00, NULL, 102, 60), +(104, 'Bruce', 'Miller', 'BMILLER', '1.590.555.0104', '2017-05-21', 'IT_PROG', 6000.00, NULL, 103, 60), +(105, 'David', 'Williams', 'DWILLIAMS', '1.590.555.0105', '2015-06-25', 'IT_PROG', 4800.00, NULL, 103, 60), +(106, 'Valli', 'Jackson', 'VJACKSON', '1.590.555.0106', '2016-02-05', 'IT_PROG', 4800.00, NULL, 103, 60), +(107, 'Diana', 'Nguyen', 'DNGUYEN', '1.590.555.0107', '2017-02-07', 'IT_PROG', 4200.00, NULL, 103, 60), +(108, 'Nancy', 'Gruenberg', 'NGRUENBE', '1.515.555.0108', '2012-08-17', 'FI_MGR', 12008.00, NULL, 101, 100), +(109, 'Daniel', 'Faviet', 'DFAVIET', '1.515.555.0109', '2012-08-16', 'FI_ACCOUNT', 9000.00, NULL, 108, 100), +(110, 'John', 'Chen', 'JCHEN', '1.515.555.0110', '2015-09-28', 'FI_ACCOUNT', 8200.00, NULL, 108, 100), +(111, 'Ismael', 'Sciarra', 'ISCIARRA', '1.515.555.0111', '2015-09-30', 'FI_ACCOUNT', 7700.00, NULL, 108, 100), +(112, 'Jose Manuel', 'Urman', 'JMURMAN', '1.515.555.0112', '2016-03-07', 'FI_ACCOUNT', 7800.00, NULL, 108, 100), +(113, 'Luis', 'Popp', 'LPOPP', '1.515.555.0113', '2017-12-07', 'FI_ACCOUNT', 6900.00, NULL, 108, 100), +(114, 'Den', 'Li', 'DLI', '1.515.555.0114', '2012-12-07', 'PU_MAN', 11000.00, NULL, 100, 30), +(115, 'Alexander', 'Khoo', 'AKHOO', '1.515.555.0115', '2013-05-18', 'PU_CLERK', 3100.00, NULL, 114, 30), +(116, 'Shelli', 'Baida', 'SBAIDA', '1.515.555.0116', '2015-12-24', 'PU_CLERK', 2900.00, NULL, 114, 30), +(117, 'Sigal', 'Tobias', 'STOBIAS', '1.515.555.0117', '2015-07-24', 'PU_CLERK', 2800.00, NULL, 114, 30), +(118, 'Guy', 'Himuro', 'GHIMURO', '1.515.555.0118', '2016-11-15', 'PU_CLERK', 2600.00, NULL, 114, 30), +(119, 'Karen', 'Colmenares', 'KCOLMENA', '1.515.555.0119', '2017-08-10', 'PU_CLERK', 2500.00, NULL, 114, 30), +(120, 'Matthew', 'Weiss', 'MWEISS', '1.650.555.0120', '2014-07-18', 'ST_MAN', 8000.00, NULL, 100, 50), +(121, 'Adam', 'Fripp', 'AFRIPP', '1.650.555.0121', '2015-04-10', 'ST_MAN', 8200.00, NULL, 100, 50), +(122, 'Payam', 'Kaufling', 'PKAUFLIN', '1.650.555.0122', '2013-05-01', 'ST_MAN', 7900.00, NULL, 100, 50), +(123, 'Shanta', 'Vollman', 'SVOLLMAN', '1.650.555.0123', '2015-10-10', 'ST_MAN', 6500.00, NULL, 100, 50), +(124, 'Kevin', 'Mourgos', 'KMOURGOS', '1.650.555.0124', '2017-11-16', 'ST_MAN', 5800.00, NULL, 100, 50), +(125, 'Julia', 'Nayer', 'JNAYER', '1.650.555.0125', '2015-03-16', 'ST_CLERK', 3200.00, NULL, 120, 50), +(126, 'Irene', 'Mikkilineni', 'IMIKKILI', '1.650.555.0126', '2016-09-28', 'ST_CLERK', 2700.00, NULL, 120, 50), +(127, 'James', 'Landry', 'JLANDRY', '1.650.555.0127', '2017-01-14', 'ST_CLERK', 2400.00, NULL, 120, 50), +(128, 'Steven', 'Markle', 'SMARKLE', '1.650.555.0128', '2018-03-08', 'ST_CLERK', 2200.00, NULL, 120, 50), +(129, 'Laura', 'Bissot', 'LBISSOT', '1.650.555.0129', '2015-08-20', 'ST_CLERK', 3300.00, NULL, 121, 50), +(130, 'Mozhe', 'Atkinson', 'MATKINSO', '1.650.555.0130', '2015-10-30', 'ST_CLERK', 2800.00, NULL, 121, 50), +(131, 'James', 'Marlow', 'JAMRLOW', '1.650.555.0131', '2015-02-16', 'ST_CLERK', 2500.00, NULL, 121, 50), +(132, 'TJ', 'Olson', 'TJOLSON', '1.650.555.0132', '2017-04-10', 'ST_CLERK', 2100.00, NULL, 121, 50), +(133, 'Jason', 'Mallin', 'JMALLIN', '1.650.555.0133', '2014-06-14', 'ST_CLERK', 3300.00, NULL, 122, 50), +(134, 'Michael', 'Rogers', 'MROGERS', '1.650.555.0134', '2016-08-26', 'ST_CLERK', 2900.00, NULL, 122, 50), +(135, 'Ki', 'Gee', 'KGEE', '1.650.555.0135', '2017-12-12', 'ST_CLERK', 2400.00, NULL, 122, 50), +(136, 'Hazel', 'Philtanker', 'HPHILTAN', '1.650.555.0136', '2018-02-06', 'ST_CLERK', 2200.00, NULL, 122, 50), +(137, 'Renske', 'Ladwig', 'RLADWIG', '1.650.555.0137', '2013-07-14', 'ST_CLERK', 3600.00, NULL, 123, 50), +(138, 'Stephen', 'Stiles', 'SSTILES', '1.650.555.0138', '2015-10-26', 'ST_CLERK', 3200.00, NULL, 123, 50), +(139, 'John', 'Seo', 'JSEO', '1.650.555.0139', '2016-02-12', 'ST_CLERK', 2700.00, NULL, 123, 50), +(140, 'Joshua', 'Patel', 'JPATEL', '1.650.555.0140', '2016-04-06', 'ST_CLERK', 2500.00, NULL, 123, 50), +(141, 'Trenna', 'Rajs', 'TRAJS', '1.650.555.0141', '2013-10-17', 'ST_CLERK', 3500.00, NULL, 124, 50), +(142, 'Curtis', 'Davies', 'CDAVIES', '1.650.555.0142', '2015-01-29', 'ST_CLERK', 3100.00, NULL, 124, 50), +(143, 'Randall', 'Matos', 'RMATOS', '1.650.555.0143', '2016-03-15', 'ST_CLERK', 2600.00, NULL, 124, 50), +(144, 'Peter', 'Vargas', 'PVARGAS', '1.650.555.0144', '2016-07-09', 'ST_CLERK', 2500.00, NULL, 124, 50), +(145, 'John', 'Russell', 'JRUSSEL', '1.011.555.0145', '2014-10-01', 'SA_MAN', 14000.00, 0.40, 100, 80), +(146, 'Karen', 'Partners', 'KPARTNER', '1.011.555.0146', '2015-01-05', 'SA_MAN', 13500.00, 0.30, 100, 80), +(147, 'Alberto', 'Errazuriz', 'AERRAZUR', '1.011.555.0147', '2015-03-10', 'SA_MAN', 12000.00, 0.30, 100, 80), +(148, 'Gerald', 'Cambrault', 'GCAMBRAU', '1.011.555.0148', '2017-10-15', 'SA_MAN', 11000.00, 0.30, 100, 80), +(149, 'Eleni', 'Zlotkey', 'EZLOTKEY', '1.011.555.0149', '2018-01-29', 'SA_MAN', 10500.00, 0.20, 100, 80), +(150, 'Peter', 'Tucker', 'PTUCKER', '1.011.555.0145', '2015-01-30', 'SA_REP', 10000.00, 0.30, 145, 80), +(151, 'David', 'Bernstein', 'DBERNSTE', '1.011.555.0146', '2015-03-24', 'SA_REP', 9500.00, 0.25, 145, 80), +(152, 'Peter', 'Hall', 'PHALL', '1.011.555.0147', '2015-08-20', 'SA_REP', 9000.00, 0.25, 145, 80), +(153, 'Christopher', 'Olsen', 'COLSEN', '1.011.555.0148', '2016-03-30', 'SA_REP', 8000.00, 0.20, 145, 80), +(154, 'Nanette', 'Cambrault', 'NCAMBRAU', '1.011.555.0149', '2016-12-09', 'SA_REP', 7500.00, 0.20, 145, 80), +(155, 'Oliver', 'Tuvault', 'OTUVAULT', '1.011.555.0150', '2017-11-23', 'SA_REP', 7000.00, 0.15, 145, 80), +(156, 'Janette', 'Smith', 'JSMITH', '1.011.555.0146', '2016-02-10', 'SA_REP', 10000.00, 0.35, 146, 80), +(157, 'Patrick', 'Sully', 'PSULLY', '1.011.555.0146', '2016-03-04', 'SA_REP', 9500.00, 0.35, 146, 80), +(158, 'Allan', 'McEwen', 'AMCEWEN', '1.011.555.0147', '2016-08-01', 'SA_REP', 9000.00, 0.35, 146, 80), +(159, 'Lindsey', 'Johnson', 'LJOHNSON', '1.011.555.0148', '2017-03-10', 'SA_REP', 8000.00, 0.30, 146, 80), +(160, 'Louise', 'Doran', 'LDORAN', '1.011.555.0149', '2017-12-15', 'SA_REP', 7500.00, 0.30, 146, 80), +(161, 'Sarath', 'Sewall', 'SSEWALL', '1.011.555.0150', '2016-11-03', 'SA_REP', 7000.00, 0.25, 146, 80), +(162, 'Clara', 'Vishney', 'CVISHNEY', '1.011.555.0147', '2015-11-11', 'SA_REP', 10500.00, 0.25, 147, 80), +(163, 'Danielle', 'Greene', 'DGREENE', '1.011.555.0148', '2017-03-19', 'SA_REP', 9500.00, 0.15, 147, 80), +(164, 'Mattea', 'Marvins', 'MMARVINS', '1.011.555.0149', '2018-01-24', 'SA_REP', 7200.00, 0.10, 147, 80), +(165, 'David', 'Lee', 'DLEE', '1.011.555.0150', '2018-02-23', 'SA_REP', 6800.00, 0.10, 147, 80), +(166, 'Sundar', 'Ande', 'SANDE', '1.011.555.0151', '2018-03-24', 'SA_REP', 6400.00, 0.10, 147, 80), +(167, 'Amit', 'Banda', 'ABANDA', '1.011.555.0152', '2018-04-21', 'SA_REP', 6200.00, 0.10, 147, 80), +(168, 'Lisa', 'Ozer', 'LOZER', '1.011.555.0148', '2015-03-11', 'SA_REP', 11500.00, 0.25, 148, 80), +(169, 'Harrison', 'Bloom', 'HBLOOM', '1.011.555.0149', '2016-03-23', 'SA_REP', 10000.00, 0.20, 148, 80), +(170, 'Tayler', 'Fox', 'TFOX', '1.011.555.0150', '2016-01-24', 'SA_REP', 9600.00, 0.20, 148, 80), +(171, 'William', 'Smith', 'WSMITH', '1.011.555.0151', '2017-02-23', 'SA_REP', 7400.00, 0.15, 148, 80), +(172, 'Elizabeth', 'Bates', 'EBATES', '1.011.555.0152', '2017-03-24', 'SA_REP', 7300.00, 0.15, 148, 80), +(173, 'Sundita', 'Kumar', 'SKUMAR', '1.011.555.0153', '2018-04-21', 'SA_REP', 6100.00, 0.10, 148, 80), +(174, 'Ellen', 'Abel', 'EABEL', '1.011.555.0149', '2014-05-11', 'SA_REP', 11000.00, 0.30, 149, 80), +(175, 'Alyssa', 'Hutton', 'AHUTTON', '1.011.555.0150', '2015-03-19', 'SA_REP', 8800.00, 0.25, 149, 80), +(176, 'Jonathon', 'Taylor', 'JTAYLOR', '1.011.555.0151', '2016-03-24', 'SA_REP', 8600.00, 0.20, 149, 80), +(177, 'Jack', 'Livingston', 'JLIVINGS', '1.011.555.0152', '2016-04-23', 'SA_REP', 8400.00, 0.20, 149, 80), +(178, 'Kimberely', 'Grant', 'KGRANT', '1.011.555.0153', '2017-05-24', 'SA_REP', 7000.00, 0.15, 149, NULL), +(179, 'Charles', 'Johnson', 'CJOHNSON', '1.011.555.0154', '2018-01-04', 'SA_REP', 6200.00, 0.10, 149, 80), +(180, 'Winston', 'Taylor', 'WTAYLOR', '1.650.555.0145', '2016-01-24', 'SH_CLERK', 3200.00, NULL, 120, 50), +(181, 'Jean', 'Fleaur', 'JFLEAUR', '1.650.555.0146', '2016-02-23', 'SH_CLERK', 3100.00, NULL, 120, 50), +(182, 'Martha', 'Sullivan', 'MSULLIVA', '1.650.555.0147', '2017-06-21', 'SH_CLERK', 2500.00, NULL, 120, 50), +(183, 'Girard', 'Geoni', 'GGEONI', '1.650.555.0148', '2018-02-03', 'SH_CLERK', 2800.00, NULL, 120, 50), +(184, 'Nandita', 'Sarchand', 'NSARCHAN', '1.650.555.0149', '2014-01-27', 'SH_CLERK', 4200.00, NULL, 121, 50), +(185, 'Alexis', 'Bull', 'ABULL', '1.650.555.0150', '2015-02-20', 'SH_CLERK', 4100.00, NULL, 121, 50), +(186, 'Julia', 'Dellinger', 'JDELLING', '1.650.555.0151', '2016-06-24', 'SH_CLERK', 3400.00, NULL, 121, 50), +(187, 'Anthony', 'Cabrio', 'ACABRIO', '1.650.555.0152', '2017-02-07', 'SH_CLERK', 3000.00, NULL, 121, 50), +(188, 'Kelly', 'Chung', 'KCHUNG', '1.650.555.0153', '2015-06-14', 'SH_CLERK', 3800.00, NULL, 122, 50), +(189, 'Jennifer', 'Dilly', 'JDILLY', '1.650.555.0154', '2015-08-13', 'SH_CLERK', 3600.00, NULL, 122, 50), +(190, 'Timothy', 'Venzl', 'TVENZL', '1.650.555.0155', '2016-07-11', 'SH_CLERK', 2900.00, NULL, 122, 50), +(191, 'Randall', 'Perkins', 'RPERKINS', '1.650.555.0156', '2017-12-19', 'SH_CLERK', 2500.00, NULL, 122, 50), +(192, 'Sarah', 'Bell', 'SBELL', '1.650.555.0157', '2014-02-04', 'SH_CLERK', 4000.00, NULL, 123, 50), +(193, 'Britney', 'Everett', 'BEVERETT', '1.650.555.0158', '2015-03-03', 'SH_CLERK', 3900.00, NULL, 123, 50), +(194, 'Samuel', 'McLeod', 'SMCLEOD', '1.650.555.0159', '2016-07-01', 'SH_CLERK', 3200.00, NULL, 123, 50), +(195, 'Vance', 'Jones', 'VJONES', '1.650.555.0160', '2017-03-17', 'SH_CLERK', 2800.00, NULL, 123, 50), +(196, 'Alana', 'Walsh', 'AWALSH', '1.650.555.0161', '2016-04-24', 'SH_CLERK', 3100.00, NULL, 124, 50), +(197, 'Kevin', 'Feeney', 'KFEENEY', '1.650.555.0162', '2016-05-23', 'SH_CLERK', 3000.00, NULL, 124, 50), +(198, 'Donald', 'OConnell', 'DOCONNEL', '1.650.555.0163', '2017-06-21', 'SH_CLERK', 2600.00, NULL, 124, 50), +(199, 'Douglas', 'Grant', 'DGRANT', '1.650.555.0164', '2018-01-13', 'SH_CLERK', 2600.00, NULL, 124, 50), +(200, 'Jennifer', 'Whalen', 'JWHALEN', '1.515.555.0165', '2013-09-17', 'AD_ASST', 4400.00, NULL, 101, 10), +(201, 'Michael', 'Martinez', 'MMARTINE', '1.515.555.0166', '2014-02-17', 'MK_MAN', 13000.00, NULL, 100, 20), +(202, 'Pat', 'Davis', 'PDAVIS', '1.603.555.0167', '2015-08-17', 'MK_REP', 6000.00, NULL, 201, 20), +(203, 'Susan', 'Jacobs', 'SJACOBS', '1.515.555.0168', '2012-06-07', 'HR_REP', 6500.00, NULL, 101, 40), +(204, 'Hermann', 'Brown', 'HBROWN', '1.515.555.0169', '2012-06-07', 'PR_REP', 10000.00, NULL, 101, 70), +(205, 'Shelley', 'Higgins', 'SHIGGINS', '1.515.555.0170', '2012-06-07', 'AC_MGR', 12008.00, NULL, 101, 110), +(206, 'William', 'Gietz', 'WGIETZ', '1.515.555.0171', '2012-06-07', 'AC_ACCOUNT', 8300.00, NULL, 205, 110); + +-- ============================================================================ +-- Update DEPARTMENTS with manager_id values +-- ============================================================================ +UPDATE departments SET manager_id = 200 WHERE department_id = 10; +UPDATE departments SET manager_id = 201 WHERE department_id = 20; +UPDATE departments SET manager_id = 114 WHERE department_id = 30; +UPDATE departments SET manager_id = 203 WHERE department_id = 40; +UPDATE departments SET manager_id = 121 WHERE department_id = 50; +UPDATE departments SET manager_id = 103 WHERE department_id = 60; +UPDATE departments SET manager_id = 204 WHERE department_id = 70; +UPDATE departments SET manager_id = 145 WHERE department_id = 80; +UPDATE departments SET manager_id = 100 WHERE department_id = 90; +UPDATE departments SET manager_id = 108 WHERE department_id = 100; +UPDATE departments SET manager_id = 205 WHERE department_id = 110; + +-- ============================================================================ +-- Table: JOB_HISTORY (10 rows) +-- ============================================================================ +INSERT INTO job_history (employee_id, start_date, end_date, job_id, department_id) VALUES +(101, '2007-09-21', '2011-10-27', 'AC_ACCOUNT', 110), +(101, '2011-10-28', '2015-03-15', 'AC_MGR', 110), +(102, '2011-01-13', '2016-07-24', 'IT_PROG', 60), +(114, '2016-03-24', '2017-12-31', 'ST_CLERK', 50), +(122, '2017-01-01', '2017-12-31', 'ST_CLERK', 50), +(176, '2016-03-24', '2016-12-31', 'SA_REP', 80), +(176, '2017-01-01', '2017-12-31', 'SA_MAN', 80), +(200, '2005-09-17', '2011-06-17', 'AD_ASST', 90), +(200, '2012-07-01', '2016-12-31', 'AC_ACCOUNT', 90), +(201, '2014-02-17', '2017-12-19', 'MK_REP', 20); + +-- Re-enable triggers +SET FOREIGN_KEY_CHECKS = 1; + +-- ============================================================================ +-- Verify row counts +-- ============================================================================ +SELECT 'regions' as table_name, COUNT(*) as row_count FROM regions +UNION ALL +SELECT 'countries', COUNT(*) FROM countries +UNION ALL +SELECT 'locations', COUNT(*) FROM locations +UNION ALL +SELECT 'jobs', COUNT(*) FROM jobs +UNION ALL +SELECT 'departments', COUNT(*) FROM departments +UNION ALL +SELECT 'employees', COUNT(*) FROM employees +UNION ALL +SELECT 'job_history', COUNT(*) FROM job_history +ORDER BY table_name; + +-- ============================================================================ +-- End of data insert script +-- Total rows inserted: 218 +-- ============================================================================ diff --git a/src/mysql/hr_schema_mysql.sql b/src/mysql/hr_schema_mysql.sql new file mode 100644 index 0000000000..b004ad3e98 --- /dev/null +++ b/src/mysql/hr_schema_mysql.sql @@ -0,0 +1,164 @@ +-- ============================================================================ +-- MySQL DDL Script for HR Schema +-- Generated from partial PostgreSQL/Oracle HR Schema +-- Date: 2025-12-12 +-- ============================================================================ + +-- Create database if it doesn't exist and use it +CREATE DATABASE IF NOT EXISTS hr; +USE hr; + +-- Disable foreign key checks for dropping tables +SET FOREIGN_KEY_CHECKS = 0; + +-- Drop tables if they exist +DROP TABLE IF EXISTS job_history; +DROP TABLE IF EXISTS employees; +DROP TABLE IF EXISTS departments; +DROP TABLE IF EXISTS jobs; +DROP TABLE IF EXISTS locations; +DROP TABLE IF EXISTS countries; +DROP TABLE IF EXISTS regions; + +-- Re-enable foreign key checks +SET FOREIGN_KEY_CHECKS = 1; + +-- ============================================================================ +-- Table: REGIONS +-- Description: Stores region information (e.g., Americas, Europe, Asia) +-- ============================================================================ +CREATE TABLE regions ( + region_id INT NOT NULL, + region_name VARCHAR(25), + CONSTRAINT reg_id_pk PRIMARY KEY (region_id) +) COMMENT='Regions table that contains region numbers and names'; + +-- ============================================================================ +-- Table: COUNTRIES +-- Description: Stores country information with region association +-- ============================================================================ +CREATE TABLE countries ( + country_id CHAR(2) NOT NULL, + country_name VARCHAR(60), + region_id INT, + CONSTRAINT country_c_id_pk PRIMARY KEY (country_id), + CONSTRAINT countr_reg_fk FOREIGN KEY (region_id) + REFERENCES regions(region_id) +) COMMENT='Country table with country ID and associated region ID'; + +-- ============================================================================ +-- Table: LOCATIONS +-- Description: Stores physical location information for offices +-- ============================================================================ +CREATE TABLE locations ( + location_id INT NOT NULL, + street_address VARCHAR(40), + postal_code VARCHAR(12), + city VARCHAR(30) NOT NULL, + state_province VARCHAR(25), + country_id CHAR(2), + CONSTRAINT loc_id_pk PRIMARY KEY (location_id), + CONSTRAINT loc_c_id_fk FOREIGN KEY (country_id) + REFERENCES countries(country_id) +) COMMENT='Locations table with addresses of company offices'; + +-- ============================================================================ +-- Table: JOBS +-- Description: Stores job titles and salary ranges +-- ============================================================================ +CREATE TABLE jobs ( + job_id VARCHAR(10) NOT NULL, + job_title VARCHAR(35) NOT NULL, + min_salary INT, + max_salary INT, + CONSTRAINT job_id_pk PRIMARY KEY (job_id) +) COMMENT='Jobs table with job titles and salary ranges'; + +-- ============================================================================ +-- Table: DEPARTMENTS +-- Description: Stores department information +-- Note: MANAGER_ID FK is added after EMPLOYEES table is created +-- ============================================================================ +CREATE TABLE departments ( + department_id INT NOT NULL COMMENT 'Primary key of departments table', + department_name VARCHAR(30) NOT NULL, + manager_id INT COMMENT 'Manager ID of a department. Foreign key to employee_id', + location_id INT, + CONSTRAINT dept_id_pk PRIMARY KEY (department_id), + CONSTRAINT dept_loc_fk FOREIGN KEY (location_id) + REFERENCES locations(location_id) +) COMMENT='Departments table showing department details'; + +-- ============================================================================ +-- Table: EMPLOYEES +-- Description: Stores employee information +-- ============================================================================ +CREATE TABLE employees ( + employee_id INT NOT NULL COMMENT 'Primary key of employees table', + first_name VARCHAR(20), + last_name VARCHAR(25) NOT NULL, + email VARCHAR(25) NOT NULL COMMENT 'Email address - must be unique', + phone_number VARCHAR(20), + hire_date DATE NOT NULL, + job_id VARCHAR(10) NOT NULL, + salary DECIMAL(8,2) COMMENT 'Monthly salary - must be greater than zero', + commission_pct DECIMAL(2,2) COMMENT 'Commission percentage (0.00 to 0.99)', + manager_id INT, + department_id INT, + CONSTRAINT emp_emp_id_pk PRIMARY KEY (employee_id), + CONSTRAINT emp_email_uk UNIQUE (email), + CONSTRAINT emp_salary_min CHECK (salary > 0), + CONSTRAINT emp_dept_fk FOREIGN KEY (department_id) + REFERENCES departments(department_id), + CONSTRAINT emp_job_fk FOREIGN KEY (job_id) + REFERENCES jobs(job_id), + CONSTRAINT emp_manager_fk FOREIGN KEY (manager_id) + REFERENCES employees(employee_id) +) COMMENT='Employees table containing employee details'; + +-- ============================================================================ +-- Add MANAGER_ID foreign key to DEPARTMENTS table +-- (Circular reference with EMPLOYEES table) +-- ============================================================================ +ALTER TABLE departments + ADD CONSTRAINT dept_mgr_fk FOREIGN KEY (manager_id) + REFERENCES employees(employee_id); + +-- ============================================================================ +-- Table: JOB_HISTORY +-- Description: Stores employee job history +-- ============================================================================ +CREATE TABLE job_history ( + employee_id INT NOT NULL COMMENT 'Foreign key to employee_id in employees table', + start_date DATE NOT NULL COMMENT 'Start date of the job - part of composite primary key', + end_date DATE NOT NULL COMMENT 'End date of the job - must be greater than start_date', + job_id VARCHAR(10) NOT NULL, + department_id INT, + CONSTRAINT jhist_emp_id_st_date_pk PRIMARY KEY (employee_id, start_date), + CONSTRAINT jhist_date_interval CHECK (end_date > start_date), + CONSTRAINT jhist_emp_fk FOREIGN KEY (employee_id) + REFERENCES employees(employee_id), + CONSTRAINT jhist_job_fk FOREIGN KEY (job_id) + REFERENCES jobs(job_id), + CONSTRAINT jhist_dept_fk FOREIGN KEY (department_id) + REFERENCES departments(department_id) +) COMMENT='Job history table tracking employee job changes'; + +-- ============================================================================ +-- Create indexes for better query performance +-- ============================================================================ +CREATE INDEX emp_department_ix ON employees(department_id); +CREATE INDEX emp_job_ix ON employees(job_id); +CREATE INDEX emp_manager_ix ON employees(manager_id); +CREATE INDEX emp_name_ix ON employees(last_name, first_name); +CREATE INDEX dept_location_ix ON departments(location_id); +CREATE INDEX jhist_job_ix ON job_history(job_id); +CREATE INDEX jhist_employee_ix ON job_history(employee_id); +CREATE INDEX jhist_department_ix ON job_history(department_id); +CREATE INDEX loc_city_ix ON locations(city); +CREATE INDEX loc_state_province_ix ON locations(state_province); +CREATE INDEX loc_country_ix ON locations(country_id); + +-- ============================================================================ +-- End of script +-- ============================================================================ diff --git a/src/mysql/index.ts b/src/mysql/index.ts new file mode 100644 index 0000000000..d54ba436e1 --- /dev/null +++ b/src/mysql/index.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { runServer } from "./server.js"; + +runServer().catch(console.error); diff --git a/src/mysql/package.json b/src/mysql/package.json new file mode 100644 index 0000000000..6fd80c6995 --- /dev/null +++ b/src/mysql/package.json @@ -0,0 +1,44 @@ +{ + "name": "@marcelo-ochoa/server-mysql", + "mcpName": "io.github.marcelo-ochoa/mysql", + "version": "1.0.7", + "repository": { + "type": "git", + "url": "https://github.com/marcelo-ochoa/servers.git", + "subfolder": "src/mysql" + }, + "description": "An MCP server for MySQL databases.", + "keywords": [ + "read-only-mcp", + "mysql-database", + "ai-agent", + "llm-tool", + "rag" + ], + "license": "MIT", + "author": "Marcelo Fabian Ochoa", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/marcelo-ochoa/servers/issues", + "type": "module", + "bin": { + "mcp-server-mysql": "dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc && shx chmod +x dist/*.js", + "prepare": "npm run build", + "watch": "tsc --watch" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.24.2", + "@toon-format/toon": "^1.0.0", + "mysql2": "^3.11.5" + }, + "devDependencies": { + "@types/node": "^22.10.1", + "shx": "^0.3.4", + "typescript": "^5.6.2" + } +} \ No newline at end of file diff --git a/src/mysql/resources.ts b/src/mysql/resources.ts new file mode 100644 index 0000000000..8ab68a66bc --- /dev/null +++ b/src/mysql/resources.ts @@ -0,0 +1,61 @@ +import { ListResourcesRequest, ReadResourceRequest } from "@modelcontextprotocol/sdk/types.js"; +import { withConnection, getResourceBaseUrl, isPoolInitialized } from "./db.js"; + +const SCHEMA_PATH = "schema"; + +export const listResourcesHandler = async (request: ListResourcesRequest) => { + if (!isPoolInitialized()) { + return { resources: [] }; + } + const resourceBaseUrl = getResourceBaseUrl(); + return await withConnection(async (connection) => { + const [result] = await connection.query( + "SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE()", + ); + return { + resources: (result as any[]).map((row: any) => ({ + uri: new URL(`${row.table_name}/${SCHEMA_PATH}`, resourceBaseUrl).href, + mimeType: "application/json", + name: `"${row.table_name}" database schema`, + })), + }; + }); +}; + +export const readResourceHandler = async (request: ReadResourceRequest) => { + 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"); + } + + return await withConnection(async (connection) => { + const [columns] = await connection.query( + `SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_TYPE, EXTRA, COLUMN_COMMENT + FROM information_schema.COLUMNS + WHERE TABLE_NAME = ? AND TABLE_SCHEMA = DATABASE()`, + [tableName] + ); + + const [indexes] = await connection.query( + `SELECT INDEX_NAME, NON_UNIQUE, SEQ_IN_INDEX, COLUMN_NAME, COLLATION, CARDINALITY, INDEX_TYPE, COMMENT + FROM information_schema.STATISTICS + WHERE TABLE_NAME = ? AND TABLE_SCHEMA = DATABASE()`, + [tableName] + ); + + return { + contents: [ + { + uri: request.params.uri, + mimeType: "application/json", + text: JSON.stringify({ columns, indexes }, null, 2), + }, + ], + }; + }); +}; diff --git a/src/mysql/server.json b/src/mysql/server.json new file mode 100644 index 0000000000..fcc86f4654 --- /dev/null +++ b/src/mysql/server.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.marcelo-ochoa/mysql", + "description": "An MCP server for MySQL databases.", + "repository": { + "url": "https://github.com/marcelo-ochoa/servers", + "source": "github", + "subfolder": "src/mysql" + }, + "version": "1.0.7", + "packages": [ + { + "registryType": "npm", + "identifier": "@marcelo-ochoa/server-mysql", + "version": "1.0.7", + "transport": { + "type": "stdio" + }, + "packageArguments": [ + { + "type": "positional", + "valueHint": "connectionString", + "description": "MySQL connection string", + "isRequired": false + } + ], + "environmentVariables": [ + { + "description": "MySQL username", + "isRequired": false, + "format": "string", + "isSecret": false, + "name": "MYSQL_USER" + }, + { + "description": "MySQL password", + "isRequired": false, + "format": "string", + "isSecret": true, + "name": "MYSQL_PASSWORD" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/mysql/server.ts b/src/mysql/server.ts new file mode 100644 index 0000000000..94b17551cc --- /dev/null +++ b/src/mysql/server.ts @@ -0,0 +1,109 @@ +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import { initializePool } from "./db.js"; +import { listResourcesHandler, readResourceHandler, callToolHandler } from "./handlers.js"; +import { tools } from "./tools.js"; + +// Create server instance +const server = new McpServer({ + name: "mysql-server", + version: "1.0.7", +}); + +const prompts = [ + { name: "mysql-query: Execute Query", description: "mysql-query select * from test_users" }, + { name: "mysql-explain: Explain Query", description: "mysql-explain select * from test_users" }, + { name: "mysql-stats: Table Statistics", description: "mysql-stats test_users" }, + { name: "mysql-connect: Database Connection", description: "mysql-connect to MySQL using a string like host.docker.internal:3306/dbname user name and password" }, + { name: "mysql-awr: Performance Report", description: "mysql-awr for MySQL performance report similar to Oracle AWR" } +]; + +// Register Prompts +server.registerPrompt("mysql-prompts", { + description: "List available MySQL prompts" +}, async () => ({ + messages: [ + { + role: "assistant", + content: { + type: "text", + text: "Available MySQL prompts:\n" + prompts.map(p => `- ${p.name}: ${p.description}`).join("\n") + } + } + ] +})); + +// Register Resource Templates +const resourceTemplate = new ResourceTemplate("mysql://{database}/{table_name}/schema", { + list: async () => listResourcesHandler({} as any) +}); +server.registerResource( + "Table Schema", + resourceTemplate, + { description: "Schema information for a MySQL database table including column names and data types" }, + async (uri) => { + return readResourceHandler({ params: { uri: uri.href } } as any); + } +); + +// Register Tools +tools.forEach(tool => { + // Basic mapping of JSON schema to Zod for simple cases + let inputSchema: any = z.object({}); + if (tool.inputSchema && tool.inputSchema.properties) { + const shape: Record = {}; + for (const [key, prop] of Object.entries(tool.inputSchema.properties)) { + let field: any = z.any(); + if ((prop as any).type === "string") { + field = z.string(); + } + if ((prop as any).description) { + field = field.describe((prop as any).description); + } + if (tool.inputSchema.required && !tool.inputSchema.required.includes(key)) { + field = field.optional(); + } else if (!tool.inputSchema.required) { + field = field.optional(); + } + shape[key] = field; + } + inputSchema = z.object(shape); + } + + server.registerTool( + tool.name, + { + description: tool.description, + inputSchema: inputSchema + }, + async (args: any) => { + return callToolHandler({ params: { name: tool.name, arguments: args } } as any); + } + ); +}); + +export async function runServer() { + const args = process.argv.slice(2); + const databaseUrl = args[0]; + + if (databaseUrl) { + try { + await initializePool(databaseUrl); + } catch (error) { + console.error("Failed to initialize database pool:", error); + process.exit(1); + } + } else { + console.error("Warning: No database URL provided. Use mysql-connect tool before using other functionality."); + } + + const transport = new StdioServerTransport(); + await server.connect(transport); + + process.stdin.on("close", () => { + console.error("MySQL MCP Server closed"); + server.close(); + process.exit(0); + }); +} diff --git a/src/mysql/tools.ts b/src/mysql/tools.ts new file mode 100644 index 0000000000..5eae58df36 --- /dev/null +++ b/src/mysql/tools.ts @@ -0,0 +1,125 @@ +export const tools = [ + { + name: "mysql-query", + description: "This tool executes SQL queries in a READ ONLY session connected to a MySQL database. If no active connection exists, it uses MCP server registration argument and environment variables MYSQL_USER and MYSQL_PASSWORD.\n\nYou should:\n\n\tExecute the provided SQL query.\n\n\tReturn the results in Toon format.\n\nArgs:\n\n\tsql: The SQL query to execute.\n\n\nThe `model` argument should specify only the name and version of the LLM (Large Language Model) you are using, with no additional information.\nThe `mcp_client` argument should specify only the name of the MCP (Model Context Protocol) client you are using, with no additional information.\n\nReturns:\n\n\tJson-formatted query results.\nFor every SQL query you generate, please include a comment at the beginning of the SELECT statement (or other main SQL command) that identifies the LLM model name and version you are using. Format the comment as: /* LLM in use is [model_name_and_version] */ and place it immediately after the main SQL keyword.\nFor example:\n\nSELECT /* LLM in use is claude-sonnet-4 */ column1, column2 FROM table_name;\n\nPlease apply this format consistently to all SQL queries you generate, using your actual model name and version in the comment\n", + inputSchema: { + type: "object", + properties: { + sql: { + type: "string", + description: "The SQL query to execute" + }, + mcp_client: { + "type": "string", + "description": "Specify the name and version of the MCP client implementation being used (e.g. Copilot, Claude, Cline...)", + "default": "UNKNOWN-MCP-CLIENT" + }, + model: { + "type": "string", + "description": "The name (and version) of the language model being used by the MCP client to process requests (e.g. gpt-4.1, claude-sonnet-4, llama4...)", + "default": "UNKNOWN-LLM" + } + }, + required: ["sql", "mcp_client", "model"] + }, + }, + { + name: "mysql-stats", + description: "Get comprehensive statistics for a specific MySQL table. This tool retrieves detailed information including row counts, table size, index information, column statistics, and other metadata that can help optimize queries and understand data distribution.\n\nArgs:\n\n\tname: The name of the table to get statistics for.\n\n\nThe `model` argument should specify only the name and version of the LLM (Large Language Model) you are using, with no additional information.\nThe `mcp_client` argument should specify only the name of the MCP (Model Context Protocol) client you are using, with no additional information.\n\nReturns:\n\n\tJson-formatted table statistics including row counts, size information, indexes, and column details.\n", + inputSchema: { + type: "object", + properties: { + name: { + type: "string", + description: "The name of the table to get statistics for" + }, + mcp_client: { + "type": "string", + "description": "Specify the name and version of the MCP client implementation being used (e.g. Copilot, Claude, Cline...)", + "default": "UNKNOWN-MCP-CLIENT" + }, + model: { + "type": "string", + "description": "The name (and version) of the language model being used by the MCP client to process requests (e.g. gpt-4.1, claude-sonnet-4, llama4...)", + "default": "UNKNOWN-LLM" + } + }, + required: ["name"] + }, + }, + { + name: "mysql-explain", + description: "Generate and display the execution plan for a given SQL query using MySQL's EXPLAIN command. This tool helps you understand how MySQL will execute your query, including information about table scans, joins, indexes used, and estimated costs.\n\nArgs:\n\n\tsql: The SQL query to explain.\n\n\nThe `model` argument should specify only the name and version of the LLM (Large Language Model) you are using, with no additional information.\nThe `mcp_client` argument should specify only the name of the MCP (Model Context Protocol) client you are using, with no additional information.\n\nReturns:\n\n\tDetailed execution plan showing how MySQL will process the query, including costs, row estimates, and access methods.\n", + inputSchema: { + type: "object", + properties: { + sql: { + type: "string", + description: "The SQL query to explain" + }, + mcp_client: { + "type": "string", + "description": "Specify the name and version of the MCP client implementation being used (e.g. Copilot, Claude, Cline...)", + "default": "UNKNOWN-MCP-CLIENT" + }, + model: { + "type": "string", + "description": "The name (and version) of the language model being used by the MCP client to process requests (e.g. gpt-4.1, claude-sonnet-4, llama4...)", + "default": "UNKNOWN-LLM" + } + }, + required: ["sql", "mcp_client", "model"] + }, + }, + { + name: "mysql-connect", + description: "Provides an interface to connect to a specified MySQL database. If a database connection is already active, the tool will close the existing connection before establishing a new one.\n\nThis tool accepts three required parameters:\n\n\tconnectionString: The MySQL connection string without embedded credentials (e.g., mysql://host:port/dbname or host:port/dbname)\n\tuser: The MySQL username\n\tpassword: The MySQL password\n\nThe credentials are stored in environment variables MYSQL_USER and MYSQL_PASSWORD for the session.\n\nThe `model` argument should only be used to specify the name and version of the LLM (Large Language Model) you are using, with no additional information.\nThe `mcp_client` argument should specify only the name of the MCP (Model Context Protocol) client you are using, with no additional information.\n", + inputSchema: { + type: "object", + properties: { + connectionString: { + type: "string", + description: "The MySQL connection string (e.g. mysql://host:port/dbname or host:port/dbname)" + }, + user: { + type: "string", + description: "The MySQL user (e.g. root)" + }, + password: { + type: "string", + description: "The MySQL password" + }, + mcp_client: { + "type": "string", + "description": "Specify the name and version of the MCP client implementation being used (e.g. Copilot, Claude, Cline...)", + "default": "UNKNOWN-MCP-CLIENT" + }, + model: { + "type": "string", + "description": "The name (and version) of the language model being used by the MCP client to process requests (e.g. gpt-4.1, claude-sonnet-4, llama4...)", + "default": "UNKNOWN-LLM" + } + }, + required: ["connectionString", "user", "password"] + }, + }, + { + name: "mysql-awr", + description: "Generate a MySQL performance report similar to Oracle AWR. Includes database statistics, InnoDB metrics, top queries (requires performance_schema), table/index statistics, connection info, and optimization recommendations.", + inputSchema: { + type: "object", + properties: { + mcp_client: { + "type": "string", + "description": "Specify the name and version of the MCP client implementation being used (e.g. Copilot, Claude, Cline...)", + "default": "UNKNOWN-MCP-CLIENT" + }, + model: { + "type": "string", + "description": "The name (and version) of the language model being used by the MCP client to process requests (e.g. gpt-4.1, claude-sonnet-4, llama4...)", + "default": "UNKNOWN-LLM" + } + }, + }, + }, +]; diff --git a/src/mysql/tools/awr.ts b/src/mysql/tools/awr.ts new file mode 100644 index 0000000000..7966a2d60e --- /dev/null +++ b/src/mysql/tools/awr.ts @@ -0,0 +1,255 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { withConnection } from "../db.js"; + +export const awrHandler = async (request: CallToolRequest) => { + try { + return await withConnection(async (connection) => { + const report: any = { + timestamp: new Date().toISOString(), + database_statistics: {}, + top_queries: [], + top_queries_by_cpu: [], + top_queries_by_io: [], + table_statistics: [], + index_statistics: [], + connection_info: {}, + }; + + // Check if performance_schema is enabled + const [perfSchemaCheck] = await connection.query(` + SELECT @@performance_schema as enabled + `); + const hasPerfSchema = (perfSchemaCheck as any)[0].enabled === 1; + + // 1. Database-wide statistics + const [dbStats] = await connection.query(` + SELECT + DATABASE() as database_name, + @@version as mysql_version, + @@version_comment as version_comment, + @@innodb_buffer_pool_size as buffer_pool_size, + @@max_connections as max_connections, + @@table_open_cache as table_open_cache, + @@query_cache_type as query_cache_type, + @@query_cache_size as query_cache_size + `); + report.database_statistics = (dbStats as any)[0]; + + // 2. InnoDB statistics + const [innodbStats] = await connection.query(` + SHOW GLOBAL STATUS WHERE + Variable_name LIKE 'Innodb_buffer_pool%' OR + Variable_name LIKE 'Innodb_rows%' OR + Variable_name LIKE 'Innodb_data%' OR + Variable_name = 'Innodb_page_size' + `); + + const innodbStatsObj: any = {}; + (innodbStats as any[]).forEach((row: any) => { + innodbStatsObj[row.Variable_name] = row.Value; + }); + report.innodb_statistics = innodbStatsObj; + + // Calculate buffer pool hit ratio + const poolReads = parseInt(innodbStatsObj.Innodb_buffer_pool_reads || '0'); + const poolReadRequests = parseInt(innodbStatsObj.Innodb_buffer_pool_read_requests || '0'); + if (poolReadRequests > 0) { + report.innodb_statistics.buffer_pool_hit_ratio = + ((poolReadRequests - poolReads) / poolReadRequests * 100).toFixed(2) + '%'; + } + + // 3. Top queries from performance_schema (if available) + if (hasPerfSchema) { + try { + const baseQuery = ` + SELECT + DIGEST_TEXT as query_text, + COUNT_STAR as exec_count, + ROUND(SUM_TIMER_WAIT / 1000000000000, 2) as total_time_sec, + ROUND(AVG_TIMER_WAIT / 1000000000000, 2) as avg_time_sec, + ROUND(MIN_TIMER_WAIT / 1000000000000, 2) as min_time_sec, + ROUND(MAX_TIMER_WAIT / 1000000000000, 2) as max_time_sec, + SUM_ROWS_EXAMINED as rows_examined, + SUM_ROWS_SENT as rows_sent, + SUM_ROWS_AFFECTED as rows_affected, + SUM_CREATED_TMP_TABLES as tmp_tables, + SUM_CREATED_TMP_DISK_TABLES as tmp_disk_tables, + SUM_SELECT_FULL_JOIN as full_joins, + SUM_SELECT_SCAN as full_scans, + SUM_SORT_MERGE_PASSES as sort_merge_passes, + SUM_NO_INDEX_USED as no_index_used, + SUM_NO_GOOD_INDEX_USED as no_good_index_used + FROM performance_schema.events_statements_summary_by_digest + WHERE SCHEMA_NAME = DATABASE() + AND DIGEST_TEXT NOT LIKE '%performance_schema%' + AND DIGEST_TEXT NOT LIKE '%information_schema%' + `; + + // Top by Total Time + const [topQueries] = await connection.query(` + ${baseQuery} + ORDER BY SUM_TIMER_WAIT DESC + LIMIT 20 + `); + report.top_queries = topQueries; + + // Top by CPU (Rows Examined) + const [topCpuQueries] = await connection.query(` + ${baseQuery} + ORDER BY SUM_ROWS_EXAMINED DESC + LIMIT 5 + `); + report.top_queries_by_cpu = topCpuQueries; + + // Top by IO (Disk Temp Tables) + const [topIoQueries] = await connection.query(` + ${baseQuery} + ORDER BY SUM_CREATED_TMP_DISK_TABLES DESC + LIMIT 5 + `); + report.top_queries_by_io = topIoQueries; + + } catch (error: any) { + report.top_queries_note = `Performance schema is enabled but query stats unavailable: ${error.message}`; + } + } else { + const note = "Performance schema is not enabled. Set performance_schema=ON in my.cnf and restart MySQL."; + report.top_queries_note = note; + report.top_queries_by_cpu_note = note; + report.top_queries_by_io_note = note; + } + + // 4. Table statistics + const [tableStats] = await connection.query(` + SELECT + TABLE_NAME as table_name, + ENGINE as engine, + TABLE_ROWS as estimated_rows, + AVG_ROW_LENGTH as avg_row_length, + ROUND(DATA_LENGTH / 1024 / 1024, 2) as data_size_mb, + ROUND(INDEX_LENGTH / 1024 / 1024, 2) as index_size_mb, + ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) as total_size_mb, + ROUND(DATA_FREE / 1024 / 1024, 2) as data_free_mb, + AUTO_INCREMENT as auto_increment, + CREATE_TIME as created, + UPDATE_TIME as last_updated, + CHECK_TIME as last_checked, + TABLE_COLLATION as collation + FROM information_schema.TABLES + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_TYPE = 'BASE TABLE' + ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC + LIMIT 20 + `); + report.table_statistics = tableStats; + + // 5. Index statistics + const [indexStats] = await connection.query(` + SELECT + TABLE_NAME as table_name, + INDEX_NAME as index_name, + NON_UNIQUE as non_unique, + COUNT(*) as column_count, + GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) as columns, + INDEX_TYPE as index_type, + MAX(CARDINALITY) as cardinality + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + GROUP BY TABLE_NAME, INDEX_NAME, NON_UNIQUE, INDEX_TYPE + ORDER BY TABLE_NAME, INDEX_NAME + LIMIT 50 + `); + report.index_statistics = indexStats; + + // 6. Connection and thread info + const [connInfo] = await connection.query(` + SELECT + COUNT(*) as total_connections, + SUM(CASE WHEN COMMAND != 'Sleep' THEN 1 ELSE 0 END) as active_connections, + SUM(CASE WHEN COMMAND = 'Sleep' THEN 1 ELSE 0 END) as sleeping_connections, + MAX(TIME) as longest_query_time_sec + FROM information_schema.PROCESSLIST + `); + report.connection_info = (connInfo as any)[0]; + + // 7. Global status variables + const [globalStatus] = await connection.query(` + SHOW GLOBAL STATUS WHERE + Variable_name IN ( + 'Threads_connected', 'Threads_running', 'Threads_created', + 'Connections', 'Aborted_connects', 'Aborted_clients', + 'Queries', 'Questions', 'Slow_queries', + 'Com_select', 'Com_insert', 'Com_update', 'Com_delete', + 'Table_locks_immediate', 'Table_locks_waited', + 'Created_tmp_tables', 'Created_tmp_disk_tables', + 'Sort_merge_passes', 'Sort_scan', 'Sort_range', + 'Opened_tables', 'Open_tables', 'Table_open_cache_hits', 'Table_open_cache_misses', + 'Uptime', 'Uptime_since_flush_status' + ) + `); + + const globalStatusObj: any = {}; + (globalStatus as any[]).forEach((row: any) => { + globalStatusObj[row.Variable_name] = row.Value; + }); + report.global_status = globalStatusObj; + + // 8. Table cache hit ratio + const cacheHits = parseInt(globalStatusObj.Table_open_cache_hits || '0'); + const cacheMisses = parseInt(globalStatusObj.Table_open_cache_misses || '0'); + if (cacheHits + cacheMisses > 0) { + report.global_status.table_cache_hit_ratio = + (cacheHits / (cacheHits + cacheMisses) * 100).toFixed(2) + '%'; + } + + // 9. Recommendations + const recommendations: string[] = []; + + // Check buffer pool hit ratio + if (poolReadRequests > 0) { + const hitRatio = (poolReadRequests - poolReads) / poolReadRequests * 100; + if (hitRatio < 95) { + recommendations.push(`InnoDB buffer pool hit ratio is ${hitRatio.toFixed(2)}%. Consider increasing innodb_buffer_pool_size (currently ${innodbStatsObj.Innodb_buffer_pool_size} bytes).`); + } + } + + // Check tmp tables on disk + const tmpTables = parseInt(globalStatusObj.Created_tmp_tables || '0'); + const tmpDiskTables = parseInt(globalStatusObj.Created_tmp_disk_tables || '0'); + if (tmpTables > 0 && tmpDiskTables / tmpTables > 0.25) { + recommendations.push(`${(tmpDiskTables / tmpTables * 100).toFixed(2)}% of temporary tables are created on disk. Consider increasing tmp_table_size and max_heap_table_size.`); + } + + // Check table cache + if (cacheMisses > 0 && cacheHits / (cacheHits + cacheMisses) < 0.85) { + recommendations.push(`Table cache hit ratio is low. Consider increasing table_open_cache (currently ${report.database_statistics.table_open_cache}).`); + } + + // Check for slow queries + const slowQueries = parseInt(globalStatusObj.Slow_queries || '0'); + const totalQueries = parseInt(globalStatusObj.Questions || '0'); + if (totalQueries > 0 && slowQueries / totalQueries > 0.05) { + recommendations.push(`${(slowQueries / totalQueries * 100).toFixed(2)}% of queries are slow. Review slow query log and optimize queries.`); + } + + report.recommendations = recommendations; + + return { + content: [{ + type: "text", + text: JSON.stringify(report, null, 2), + mimeType: "application/json" + }], + isError: false, + }; + }); + } catch (error: any) { + return { + content: [{ + type: "text", + text: `Error generating MySQL performance report: ${error?.message ?? error}` + }], + isError: true, + }; + } +}; diff --git a/src/mysql/tools/connect.ts b/src/mysql/tools/connect.ts new file mode 100644 index 0000000000..b408754227 --- /dev/null +++ b/src/mysql/tools/connect.ts @@ -0,0 +1,37 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { initializePool, closePool } from "../db.js"; + +export const connectHandler = async (request: CallToolRequest) => { + const newConnectionString = request.params.arguments?.connectionString; + const newUser = request.params.arguments?.user; + const newPassword = request.params.arguments?.password; + + if ( + typeof newConnectionString !== "string" || !newConnectionString || + typeof newUser !== "string" || !newUser || + typeof newPassword !== "string" || !newPassword + ) { + return { + content: [{ type: "text", text: "Missing or invalid connectionString, user, or password argument." }], + isError: true, + }; + } + + try { + await closePool(); + // Override env vars for this session + process.env.MYSQL_USER = newUser; + process.env.MYSQL_PASSWORD = newPassword; + await initializePool(newConnectionString); + return { + content: [{ type: "text", text: `Successfully connected to MySQL DB: ${newConnectionString} as user ${newUser}` }], + isError: false, + }; + } catch (err) { + return { + content: [{ type: "text", text: `Failed to connect: ${err}` }], + isError: true, + }; + } +}; + diff --git a/src/mysql/tools/explain.ts b/src/mysql/tools/explain.ts new file mode 100644 index 0000000000..36c79a84ad --- /dev/null +++ b/src/mysql/tools/explain.ts @@ -0,0 +1,16 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { withConnection } from "../db.js"; + +export const explainHandler = async (request: CallToolRequest) => { + const sql = typeof request.params.arguments?.sql === "string" + ? request.params.arguments.sql.replace(/;\s*$/, "") + : ""; + + return await withConnection(async (connection) => { + const [rows] = await connection.query(`EXPLAIN FORMAT=JSON ${sql}`); + return { + content: [{ type: "text", text: JSON.stringify(rows, null, 2), mimeType: "application/json" }], + isError: false, + }; + }); +}; diff --git a/src/mysql/tools/query.ts b/src/mysql/tools/query.ts new file mode 100644 index 0000000000..2f2cd75b6f --- /dev/null +++ b/src/mysql/tools/query.ts @@ -0,0 +1,29 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { encode } from "@toon-format/toon"; +import { withConnection } from "../db.js"; + +export const queryHandler = async (request: CallToolRequest) => { + const sql = typeof request.params.arguments?.sql === "string" + ? request.params.arguments.sql.replace(/;\s*$/, "") + : ""; + + return await withConnection(async (connection) => { + try { + await connection.query("SET SESSION TRANSACTION READ ONLY"); + await connection.query("START TRANSACTION READ ONLY"); + const [rows] = await connection.query(sql); + return { + content: [{ type: "text", text: encode(rows) }], + isError: false, + }; + } catch (error) { + throw error; + } finally { + connection + .query("ROLLBACK") + .catch((error: any) => + console.warn("Could not roll back transaction:", error), + ); + } + }); +}; diff --git a/src/mysql/tools/stats.ts b/src/mysql/tools/stats.ts new file mode 100644 index 0000000000..491dd3d522 --- /dev/null +++ b/src/mysql/tools/stats.ts @@ -0,0 +1,74 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { withConnection } from "../db.js"; + +export const statsHandler = async (request: CallToolRequest) => { + const tableName = request.params.arguments?.name as string; + + return await withConnection(async (connection) => { + // Get table statistics + const [tableStats] = await connection.query(` + SELECT + TABLE_SCHEMA as schema_name, + TABLE_NAME as table_name, + TABLE_ROWS as num_rows, + AVG_ROW_LENGTH as avg_row_length, + DATA_LENGTH as data_length, + INDEX_LENGTH as index_length, + DATA_FREE as data_free, + AUTO_INCREMENT as auto_increment, + CREATE_TIME as create_time, + UPDATE_TIME as update_time, + CHECK_TIME as check_time, + TABLE_COLLATION as collation, + TABLE_COMMENT as comment + FROM information_schema.TABLES + WHERE TABLE_NAME = ? AND TABLE_SCHEMA = DATABASE() + `, [tableName]); + + // Get index statistics + const [indexStats] = await connection.query(` + SELECT + INDEX_NAME as index_name, + NON_UNIQUE as non_unique, + SEQ_IN_INDEX as seq_in_index, + COLUMN_NAME as column_name, + COLLATION as collation, + CARDINALITY as cardinality, + INDEX_TYPE as index_type, + COMMENT as comment + FROM information_schema.STATISTICS + WHERE TABLE_NAME = ? AND TABLE_SCHEMA = DATABASE() + ORDER BY INDEX_NAME, SEQ_IN_INDEX + `, [tableName]); + + // Get column statistics + const [columnStats] = await connection.query(` + SELECT + COLUMN_NAME as column_name, + DATA_TYPE as data_type, + IS_NULLABLE as is_nullable, + COLUMN_DEFAULT as column_default, + CHARACTER_MAXIMUM_LENGTH as max_length, + NUMERIC_PRECISION as numeric_precision, + NUMERIC_SCALE as numeric_scale, + COLUMN_TYPE as column_type, + COLUMN_KEY as column_key, + EXTRA as extra, + COLUMN_COMMENT as comment + FROM information_schema.COLUMNS + WHERE TABLE_NAME = ? AND TABLE_SCHEMA = DATABASE() + ORDER BY ORDINAL_POSITION + `, [tableName]); + + const stats = { + table_stats: Array.isArray(tableStats) && tableStats.length > 0 ? tableStats[0] : null, + index_stats: indexStats, + column_stats: columnStats + }; + + return { + content: [{ type: "text", text: JSON.stringify(stats, null, 2), mimeType: "application/json" }], + isError: false, + }; + }); +}; diff --git a/src/mysql/tsconfig.json b/src/mysql/tsconfig.json new file mode 100644 index 0000000000..8a2e884007 --- /dev/null +++ b/src/mysql/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "." + }, + "include": [ + "./**/*.ts" + ] +} \ No newline at end of file diff --git a/src/oracle/.dockerignore b/src/oracle/.dockerignore new file mode 100644 index 0000000000..8d2924846b --- /dev/null +++ b/src/oracle/.dockerignore @@ -0,0 +1,3 @@ +dist +node_modules +*.js.map diff --git a/src/oracle/AWR_example.md b/src/oracle/AWR_example.md new file mode 100644 index 0000000000..6db22e3b7b --- /dev/null +++ b/src/oracle/AWR_example.md @@ -0,0 +1,90 @@ +## Oracle AWR in action + +Here is an example of an AWR report generated using the `orcl-awr` tool, followed by an analysis of the top SQL statements. + +**Sample prompts**: +- Generate a performance report using orcl-awr +- find top 5 sql statement by IO and CPU from above awr report +- Model Gemini 3 Pro (High) + +### AWR Report Summary + +**Snapshot Period:** +- **Begin:** 30-Nov-25 13:58:49 +- **End:** 30-Nov-25 14:58:45 +- **Duration:** ~60 minutes + +**Load Profile:** +- **DB Time:** 78 minutes (higher than elapsed time, indicating concurrency) +- **Logical Reads:** ~133k blocks/sec (High memory activity) +- **Physical Reads:** ~17.6 blocks/sec (Low disk I/O, good caching) + +**🚨 Critical Finding:** +- **Failed Logon Delay** is the dominant wait event across almost all time slots. +- It accounts for a significant portion of database activity (e.g., ~9% of events in multiple 5-minute windows). +- This confirms the earlier manual analysis: there is a persistent issue with failed login attempts (likely a brute force attack or misconfigured application loop). + +**Performance Health:** +- **CPU Usage:** Low (0.1s per second) +- **I/O:** Very low write activity, moderate read requests +- **Memory:** High logical reads indicate active buffer cache usage + +### Top 5 SQL by CPU Usage + +1. **`b6usrg82hwsa3`** - **DBMS_STATS Gathering** + * **CPU:** 1,420.87s + * **Executions:** 7 + * **Activity:** `call dbms_stats.gather_database_stats_job_proc ( )` + * **Insight:** This is the automated background job for gathering optimizer statistics. It is the heaviest consumer of CPU, which is expected during maintenance windows. + +2. **`2a5bjk27jynyk`** - **Optimizer Snapshot Insert** + * **CPU:** 360.43s + * **Executions:** 91 + * **Activity:** `insert /* KSXM:TAKE_SNPSHOT */ into sys.optstat_snapshot$...` + * **Insight:** Related to the stats gathering process, tracking changes in table statistics. + +3. **`b39m8n96gxk7c`** - **Auto Task Execution** + * **CPU:** 64.23s + * **Executions:** 259 + * **Activity:** `call dbms_autotask_prvt.run_autotask ( :0,:1 )` + * **Insight:** The scheduler framework running automated maintenance tasks. + +4. **`ampw9ddqufjd3`** - **Auto Index Capture** + * **CPU:** 62.64s + * **Executions:** 248 + * **Activity:** `begin /*KAPI:capture*/ dbms_auto_index_internal.capture_sts; end;` + * **Insight:** Oracle 23c's Automatic Indexing feature monitoring the workload. + +5. **`avzy19hxu6gg4`** - **SQL Tuning Advisor** + * **CPU:** 52.08s + * **Executions:** 248 + * **Activity:** `SELECT VALUE(P) FROM TABLE(DBMS_SQLTUNE.SELECT_CURSOR_CACHE...` + * **Insight:** Internal job analyzing the cursor cache for tuning opportunities. + +### Top 5 SQL by Physical I/O (Disk Reads) + +1. **`b6usrg82hwsa3`** - **DBMS_STATS Gathering** (Same as #1 CPU) + * **Disk Reads:** 392,534 + * **Insight:** Heaviest I/O consumer as well, scanning tables to compute statistics. + +2. **`cfcbdfqr6r86d`** - **Stats Operation Update** + * **Disk Reads:** 13,132 + * **Activity:** `UPDATE ... SYS.WRI$_OPTSTAT_OPR_TASKS...` + * **Insight:** Updating the status of statistics operations. + +3. **`4k6u2pfqy4vyw`** - **Wait Class Snapshot** + * **Disk Reads:** 9,414 + * **Activity:** `SELECT ... "SNAP_ID","WAIT_CLASS"...` + * **Insight:** Querying wait class history, likely for AWR or performance monitoring views. + +4. **`1fm07xd16u5nr`** - **Stats Task Insert** + * **Disk Reads:** 8,231 + * **Activity:** `insert /* QOSH:REC_STS */ into sys.wri$_optstat_opr_tasks...` + * **Insight:** Logging statistics operations. + +5. **`b39m8n96gxk7c`** - **Auto Task Execution** (Same as #3 CPU) + * **Disk Reads:** 7,627 + * **Insight:** Maintenance tasks performing moderate I/O. + +### Summary +The database workload is currently dominated by **internal maintenance tasks**, specifically **Optimizer Statistics Gathering** (`dbms_stats`) and **Automatic Indexing** monitoring. There is no significant user workload visible in the top 5 consumers, which aligns with the "Failed Logon Delay" wait event seen earlier—external users are failing to connect, so they aren't generating SQL load! diff --git a/src/oracle/CHANGELOG.md b/src/oracle/CHANGELOG.md new file mode 100644 index 0000000000..4b8510c709 --- /dev/null +++ b/src/oracle/CHANGELOG.md @@ -0,0 +1,163 @@ +## Change Log + +### 2026-03-11 +- **chore**: Bump server version to 1.0.7 + - Updated version to 1.0.7 across package.json, server.json, and server.ts + - Migrated to `McpServer` API from deprecated `Server` class + - Refactored resources into separate `resources.ts` for better modularity + +### 2026-03-07 +- **chore**: Bump server version to 1.0.6 + - Updated version to 1.0.6 across package.json, server.json, and server.ts + - Refactored resources into separate `resources.ts` for better modularity. + + +### 2026-01-22 +- **chore**: Bump server version to 1.0.5 + - Updated version to 1.0.5 across package.json, server.json, and server.ts + - Refactored prompt names to be more descriptive for better CLI visibility + +- **chore**: Bump server version to 1.0.4 + - Updated version to 1.0.4 across package.json, server.json, and server.ts + +### 2026-01-07 +- **feat**: Make initial connection string optional at startup + - Modified `runServer` to allow server startup without a connection string + - Added warning message when starting without a connection string + - Updated error messages to guide users to use the `orcl-connect` tool + - Updated README with documentation for optional connection string and `orcl-connect` tool usage + +### 2025-12-12 +- **chore**: Bump server version to 1.0.3 + - Updated version to 1.0.3 across package.json, server.json, and server.ts + - Published package @marcelo-ochoa/server-oracle@1.0.3 to npm registry + - Rebuilt Docker image mochoa/mcp-oracle with updated dependencies + - Updated LICENSE link in README to point to GitHub repository + +- **docs**: Add MIT License file + - Added LICENSE file with MIT License text + - Updated README with proper license link + +### 2025-12-03 +- **feat**: Upgrade MCP SDK and bump server version to 1.0.2 + - Updated version to 1.0.2 across package.json, server.json, and server.ts + - Upgraded @modelcontextprotocol/sdk from ^1.19.1 to ^1.24.2 + - Maintained existing prompts/list functionality with 5 Oracle-specific prompt templates: + - `orcl-query` - Example query execution + - `orcl-explain` - Query execution plan analysis + - `orcl-stats` - Table/object statistics retrieval + - `orcl-connect` - Database connection instructions + - `orcl-awr` - AWR performance report generation + - Published package @marcelo-ochoa/server-oracle@1.0.2 to npm registry + - Rebuilt Docker image mochoa/mcp-oracle with updated dependencies + +### 2025-12-02 +- **feat**: Add multi-architecture Oracle thick mode support and enhance stats tool to handle schema-prefixed table names + - Updated version to 1.0.1 + - Added support for ARM64 and AMD64 architectures in Oracle thick mode + - Enhanced `orcl-stats` tool to accept schema-prefixed table names (e.g., `HR.COUNTRIES`) + - Modified stats handler to dynamically parse schema from table name + - Updated all database views from `dba_*` to `all_*` for broader compatibility + - Fixed LOB handling in resource handler for `dbms_developer.get_metadata` + - Added `json_serialize` to properly convert JSON metadata to text + +- **refactor**: Move Oracle server usage examples from README to Demos.md + - Extracted "Usage with Claude Desktop" section into separate Demos.md file + - Improved documentation organization and modularity + - Updated README with link to Demos.md + +### 2025-12-01 +- **feat**: Bump server version to 1.0.0 + - Updated version to 1.0.0 across package.json, server.json, and server.ts + - Added AWR_example.md with comprehensive AWR report analysis + - Added CHANGELOG.md for better change tracking + - Renamed tools to use `orcl-` prefix for consistency (orcl-query, orcl-explain, orcl-stats, orcl-connect, orcl-awr) + - Updated handlers and server prompts to reflect new tool names + - Enhanced README with Oracle AWR in action section + +### 2025-11-27 +- **chore**: Bump patch version in server.json + - Minor version update for server configuration + +- **feat**: Add `ListResourceTemplates` handler to Oracle server + - Enhanced server capabilities with resource template listing + - Updated package dependencies + +- **feat**: Add `server.json` to define Oracle MCP server, environment variables, and update related configurations + - Added server.json with MCP server metadata and schema + - Updated version to 0.7.5 + - Added mcpName field to package.json + - Configured environment variables (ORACLE_USER, ORACLE_PASSWORD) in server definition + - Updated .gitignore for better file management + +### 2025-11-27 +- **feat**: Add `server.json` to define Oracle MCP server, environment variables, and update related configurations + - Added server.json with MCP server metadata and schema + - Updated version to 0.7.5 + - Added mcpName field to package.json + - Configured environment variables (ORACLE_USER, ORACLE_PASSWORD) in server definition + +### 2025-11-25 +- **docs**: Add change logs to Oracle and Postgres READMEs + - Detailed new features such as secure Postgres authentication + - Documented Toon format encoding integration + - Added Antigravity Code Editor integration instructions + +### 2025-11-20 +- **feat**: Add Docker image build for postgres service and remove oracle test script +- **feat**: Add initial Postgres server implementation, integrate ModelContextProtocol SDK, and update Oracle tools + - Enhanced Oracle tools integration with new MCP SDK features + +### 2025-11-19 +- **feat**: Encode query and explain plan results using Toon format and add MIME type to stats output + - Improved data serialization using `toon-format` library for better JSON handling + - Added MIME type support for stats output +- **docs**: Add instruction for `mcp_config.json` placement in README + - Clarified configuration file location for Antigravity Code Editor +- **feat**: Add Antigravity Code Editor section to README with image and configuration example + - Added comprehensive setup instructions for Antigravity integration + +### 2025-11-05 +- **docs**: Set Oracle MCP server link + +### 2025-11-04 +- **docs**: Added article about AI pair programming + +### 2025-10-30 +- **feat**: Added keywords and Gemini Code Assist setting +- **fix**: Fix dependencies +- **docs**: Update README with Gemini CLI prompts demo +- **release**: New release with better tools information + +### 2025-07-02 +- **chore**: Update version to 0.7.0 and enhance AWR documentation in Oracle server + +### 2025-07-01 +- **chore**: Update version to 0.6.4 and add AWR functionality in Oracle server + - Implemented Automatic Workload Repository (AWR) report generation + +### 2025-06-17 +- **feat**: Update Oracle server package name and instructions for clarity + +### 2025-06-13 +- **fix**: Correct README and Dockerfile for consistency and clarity + +### 2025-06-09 +- **feat**: Update Oracle README and configuration for improved clarity and functionality + +### 2025-03-20 +- **docs**: Correct typo in README and update Docker configuration for local usage + +### 2025-03-19 +- **docs**: Update README to include stats retrieval and improved execution plan visualization +- **docs**: Update README to include stats functionality and sample Docker configuration +- **feat**: Add stats endpoint for SQL object and update README with Docker AI usage + - Implemented comprehensive table statistics retrieval + +### 2025-03-14 +- **docs**: Update README with explain functionality and demo prompts for Oracle MCP server + +### 2025-03-13 +- **refactor**: Remove inactivity timer and enhance server shutdown handling +- **feat**: Add Oracle MCP server with Docker support and configuration + - Initial release of Oracle MCP server diff --git a/src/oracle/Demos.md b/src/oracle/Demos.md new file mode 100644 index 0000000000..683cfa8ee0 --- /dev/null +++ b/src/oracle/Demos.md @@ -0,0 +1,248 @@ +# Demos + +Some sample usage scenarios are shown below: + +## 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) +* Credentials are passed via environment variables `ORACLE_USER` and `ORACLE_PASSWORD` + +```json +{ + "mcpServers": { + "oracle": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "ORACLE_USER=scott", + "-e", + "ORACLE_PASSWORD=tiger", + "mochoa/mcp-oracle", + "host.docker.internal:1521/freepdb1"] + } + } +} +``` + +### NPX + +```json +{ + "mcpServers": { + "oracle": { + "command": "npx", + "args": [ + "-y", + "@marcelo-ochoa/server-oracle", + "localhost:1521/freepdb1" + ], + "env": { + "ORACLE_USER": "scott", + "ORACLE_PASSWORD": "tiger" + } + } + } +} +``` + +Replace `/freepdb1` with your database name. + +### Demo Prompts + +Sample prompts using the Oracle Database sample HR schema and +[Oracle Database 23ai Free embedded database - Faststart - Docker Desktop Extension](https://open.docker.com/extensions/marketplace?extensionId=mochoa/oraclefree-docker-extension) . + +- orcl-connect to host.docker.internal:1521/freepdb1 using hr as user and hr_2025 as password using oracle mcp server +- orcl-query SELECT COUNTRY_NAME, CITY, COUNT(DEPARTMENT_ID) +FROM HR.COUNTRIES JOIN HR.LOCATIONS USING (COUNTRY_ID) JOIN HR.DEPARTMENTS USING (LOCATION_ID) +WHERE DEPARTMENT_ID IN + (SELECT DEPARTMENT_ID FROM HR.EMPLOYEES + GROUP BY DEPARTMENT_ID + HAVING COUNT(DEPARTMENT_ID)>5) +GROUP BY COUNTRY_NAME, CITY +- orcl-explain the execution plan +- visualize above execution plan in text mode +- orcl-stats of HR.COUNTRIES, HR.LOCATIONS and HR.DEPARTMENTS +- based on above table and index stats rewrite above query with a better execution plan +- visualize original and rewritten execution plan +- load resource oracle://HR/COUNTRIES/schema + +See in action using Claude Desktop App + +![Oracle MCP Server demo](https://github.com/marcelo-ochoa/servers/blob/main/src/oracle/images/demo-prompts.gif?raw=true) + +## Using Docker AI + +[Ask Gordon](https://docs.docker.com/desktop/features/gordon/) is an AI assistant designed to streamline your Docker workflow by providing contextual assistance tailored to your local environment. Currently in Beta and available in Docker Desktop version 4.38.0 or later, Ask Gordon offers intelligent support for various Docker-related tasks. + +```sh +% cd src/oracle +% docker ai 'orcl-stats for table countries' + + • Calling stats ✔️ + + Here are the statistics for the COUNTRIES table: + + ### Table Statistics: + + • Owner: HR + • Table Name: COUNTRIES + • Number of Rows: 25 + • Average Row Length: 16 bytes + • Last Analyzed: 2025-03-10 22:00:38 + + ### Index Statistics: + + • Index Name: COUNTRY_C_ID_PK + • B-Level: 0 + • Leaf Blocks: 1 + • Distinct Keys: 25 + • Number of Rows: 25 + • Clustering Factor: 0 + • Last Analyzed: 2025-03-10 22:00:38 + + ### Column Statistics: + + 1. COUNTRY_ID: + + • Number of Distinct Values: 25 + • Density: 0.04 + • Histogram: NONE + • Last Analyzed: 2025-03-10 22:00:38 + + 2. COUNTRY_NAME: + + • Number of Distinct Values: 25 + • Density: 0.04 + • Histogram: NONE + • Last Analyzed: 2025-03-10 22:00:38 + + 3. REGION_ID: + + • Number of Distinct Values: 5 + • Density: 0.02 + • Histogram: FREQUENCY + • Last Analyzed: 2025-03-10 22:00:38 +``` + +Using this sample gordon-mcp.yml file in a current directory: + +```yml +services: + time: + image: mcp/time + oracle: + image: mochoa/mcp-oracle + command: ["host.docker.internal:1521/freepdb1"] + environment: + - ORACLE_USER=scott + - ORACLE_PASSWORD=tiger +``` + +## Using Gemini CLI + +![Gemini CLI Screenshot](https://github.com/google-gemini/gemini-cli/blob/c583b510e09ddf9d58cca5b6132bf19a8f5a8091/docs/assets/gemini-screenshot.png?raw=true) + +[Gemini CLI](https://github.com/google-gemini/gemini-cli/) +is an open-source AI agent that brings the power of Gemini directly +into your terminal. It provides lightweight access to Gemini, giving you the +most direct path from your prompt to our model. + +Using this sample settings.json file at ~/.gemini/ directory: + +```json +{ + "mcpServers": { + "oracle": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "ORACLE_USER=sh", + "-e", + "ORACLE_PASSWORD=sh_2025", + "mochoa/mcp-oracle", + "host.docker.internal:1521/freepdb1" + ] + } + }, + "security": { + "auth": { + "selectedType": "gemini-api-key" + } + }, + "ui": { + "theme": "ANSI" + }, + "selectedAuthType": "gemini-api-key", + "theme": "Dracula" +} +``` + +### Sample prompts with Gemini CLI + +- connect to host.docker.internal:1521/freepdb1 using hr as user and hr_2025 as password using oracle mcp server + ![connect](https://github.com/marcelo-ochoa/servers/blob/main/src/oracle/images/gemini-cli-connect.png?raw=true) + +- orcl-query SELECT COUNTRY_NAME, CITY, COUNT(DEPARTMENT_ID) + FROM COUNTRIES JOIN LOCATIONS USING (COUNTRY_ID) JOIN DEPARTMENTS USING (LOCATION_ID) + WHERE DEPARTMENT_ID IN +   (SELECT DEPARTMENT_ID FROM EMPLOYEES +    GROUP BY DEPARTMENT_ID +    HAVING COUNT(DEPARTMENT_ID)>5) + GROUP BY COUNTRY_NAME, CITY + ![query](https://github.com/marcelo-ochoa/servers/blob/main/src/oracle/images/gemini-cli-query.png?raw=true) + +- orcl-explain the execution plan + ![explain top](https://github.com/marcelo-ochoa/servers/blob/main/src/oracle/images/gemini-cli-explain-1.png?raw=true) + ![explain botton](https://github.com/marcelo-ochoa/servers/blob/main/src/oracle/images/gemini-cli-explain-2.png?raw=true) + +- visualize above execution plan in text mode + ![visualize](https://github.com/marcelo-ochoa/servers/blob/main/src/oracle/images/gemini-cli-visualize.png?raw=true) + +- orcl-stats of COUNTRIES, LOCATIONS and DEPARTMENTS + ![stats top](https://github.com/marcelo-ochoa/servers/blob/main/src/oracle/images/gemini-cli-stats-1.png?raw=true) + ![stats botton](https://github.com/marcelo-ochoa/servers/blob/main/src/oracle/images/gemini-cli-stats-2.png?raw=true) + +- based on above table and index stats rewrite above query with a better execution plan + ![new-query](https://github.com/marcelo-ochoa/servers/blob/main/src/oracle/images/gemini-cli-new-query.png?raw=true) + +- visualize original and rewritten execution plan + ![bot-plans](https://github.com/marcelo-ochoa/servers/blob/main/src/oracle/images/gemini-cli-both-plans.png?raw=true) + +## Using Antigravity Code Editor + +![antigravity](https://github.com/marcelo-ochoa/servers/blob/main/src/oracle/images/antigravity.png?raw=true) + +Put this in `~/.gemini/antigravity/mcp_config.json` + +```json +{ + "mcpServers": { + "oracle": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "ORACLE_USER=hr", + "-e", + "ORACLE_PASSWORD=hr_2025", + "mochoa/mcp-oracle", + "host.docker.internal:1521/freepdb1" + ] + } + }, + "inputs": [] +} +``` diff --git a/src/oracle/Dockerfile b/src/oracle/Dockerfile new file mode 100644 index 0000000000..dd619392a9 --- /dev/null +++ b/src/oracle/Dockerfile @@ -0,0 +1,72 @@ +FROM node:slim AS builder + +COPY src/oracle /app +COPY tsconfig.json /tsconfig.json + +WORKDIR /app + +RUN --mount=type=cache,target=/root/.npm npm install + +RUN npm run build + +RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev + +FROM dhi.io/node:26-alpine-sfw-ent-dev AS shim-builder +RUN apk update && apk add --no-cache gcc musl-dev +WORKDIR /tmp +RUN echo '#define _GNU_SOURCE' > shim.c && \ + echo '#include ' >> shim.c && \ + echo '#include ' >> shim.c && \ + echo '#include ' >> shim.c && \ + echo 'char *canonicalize_file_name(const char *path) { return realpath(path, NULL); }' >> shim.c && \ + echo 'int bindresvport(int sd, struct sockaddr_in *sin) { return 0; }' >> shim.c && \ + echo 'int __dn_expand(const unsigned char *msg, const unsigned char *eomorig, const unsigned char *comp_dn, char *exp_dn, int length) { return 0; }' >> shim.c && \ + echo 'int __dn_skipname(const unsigned char *comp_dn, const unsigned char *eom) { return 0; }' >> shim.c && \ + echo 'int __res_nsearch(void *statp, const char *dname, int class, int type, unsigned char *answer, int anslen) { return -1; }' >> shim.c && \ + gcc -shared -fPIC -o libshim.so shim.c + +FROM dhi.io/node:26-alpine-sfw-ent-dev AS release + +# Use BuildKit's automatic platform detection +ARG TARGETARCH + +# Install Oracle Instant Client for thick mode support +# This is required for databases using Advanced Networking Option (ANO) encryption +# Using version 19.23 - stable and well-tested (version 23.x has issues with ORA-24960) +# Using full 'basic' package instead of 'basiclite' as it includes encryption libraries +# Supports both ARM64 and x86_64 architectures +RUN apk update && \ + apk add --no-cache libaio libnsl curl unzip && \ + cd /tmp && \ + # Detect architecture and set appropriate download URL + if [ "$TARGETARCH" = "arm64" ]; then \ + ARCH_SUFFIX="arm64"; \ + echo "Detected ARM64 architecture"; \ + else \ + ARCH_SUFFIX="x64"; \ + echo "Detected x86_64 architecture"; \ + fi && \ + IC_URL="https://download.oracle.com/otn_software/linux/instantclient/1928000/instantclient-basic-linux.$ARCH_SUFFIX-19.28.0.0.0dbru.zip" && \ + echo "Downloading Oracle Instant Client from: $IC_URL" && \ + curl -s -o instantclient.zip "$IC_URL" && \ + unzip -q instantclient.zip && \ + mv instantclient_19_28 /usr/lib/instantclient && \ + rm -rf instantclient.zip /tmp/* + +COPY --from=shim-builder /tmp/libshim.so /usr/lib/libshim.so + +ENV LD_PRELOAD=/usr/lib/libshim.so:/lib/libgcompat.so.0 +ENV LD_LIBRARY_PATH=/usr/lib/instantclient + +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 /usr/bin/npm ci --ignore-scripts --omit-dev + +ENTRYPOINT ["node", "dist/index.js"] + diff --git a/src/oracle/LICENSE b/src/oracle/LICENSE new file mode 100644 index 0000000000..4a93985763 --- /dev/null +++ b/src/oracle/LICENSE @@ -0,0 +1,216 @@ +The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0. + +Contributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License. + +No rights beyond those granted by the applicable original license are conveyed for such contributions. + +--- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright + owner or by an individual or Legal Entity authorized to submit on behalf + of the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +--- + +MIT License + +Copyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +Creative Commons Attribution 4.0 International (CC-BY-4.0) + +Documentation in this project (excluding specifications) is licensed under +CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for +the full license text. diff --git a/src/oracle/README.md b/src/oracle/README.md new file mode 100644 index 0000000000..6fc346b557 --- /dev/null +++ b/src/oracle/README.md @@ -0,0 +1,149 @@ +# Oracle Database + +A Model Context Protocol server that provides read-only access to Oracle Database. This server enables LLMs to inspect database schemas, execute and explain read-only queries. + +## Components + +### Tools + +- **orcl-query** + - Execute read-only SQL queries against the connected Oracle Database + - Input: `sql` (string): The SQL query to execute + - All queries are executed within a READ ONLY transaction + +- **orcl-explain** + - Explain plan SQL queries against the connected Oracle Database + - Input: `sql` (string): The SQL query to execute + - Requires GRANT SELECT_CATALOG_ROLE TO your_user; + +- **orcl-stats** + - Get statistics for a given table on current connected schema + - Input: `name` (string): The table name + - Table owner is equal to USER SQL function returning value + +- **orcl-connect** + - Reconnect using new credentials + - Input: `connectionString` (string): SQLNet connect string for example host.docker.internal:1521/freepdb1 + - Input: `user` (string): Username for example scott + - Input: `password` (string): Password, for example tiger + +Example: + orcl-connect host.docker.internal:1521/freepdb1 hr hr_2025 + +- **orcl-awr** + - Automatic Workload Repository (AWR) with optional sql_id, requires SELECT_CATALOG_ROLE and grant execute on DBMS_WORKLOAD_REPOSITORY package + - Input: `sql_id` (string): (optional) SQL id to get the AWR report for an specific query, if null full last generated AWR report + +### Resources + +The server provides schema information for each table in the Oracle Database current connected user: + +- **Table Schemas** (`oracle://USER/
/schema`) + - JSON schema information for each table + - Includes column names and data types + - Automatically discovered from Oracle Database metadata + +## Change Log + +See [Change Log](https://github.com/marcelo-ochoa/servers/blob/main/src/oracle/CHANGELOG.md) for the history of changes. + +## Demos + +See [Demos](https://github.com/marcelo-ochoa/servers/blob/main/src/oracle/Demos.md) for usage examples with Claude Desktop, Docker AI, Gemini CLI, and Antigravity Code Editor. + + +## Oracle AWR in action + +See [Oracle AWR in action](https://github.com/marcelo-ochoa/servers/blob/main/src/oracle/AWR_example.md) for an example of an AWR report generated using the `orcl-awr` tool, followed by an analysis of the top SQL statements. + + +The Oracle server uses environment variables or the `orcl-connect` tool for secure credential management: + +- **`ORACLE_USER`**: Oracle username (optional if using `orcl-connect`) +- **`ORACLE_PASSWORD`**: Oracle password (optional if using `orcl-connect`) + +### Connection String + +The connection string should contain only the host, port, and service/SID information (without embedded credentials). Providing it as a command-line argument is **optional**. If omitted at startup, you must use the `orcl-connect` tool to establish a connection before using other functionality. + +**Supported connection string formats:** +- `host:port/service_name` +- `host:port:SID` + +## Building + +Docker: + +```sh +docker build -t mochoa/mcp-oracle -f src/oracle/Dockerfile . +``` + +## 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 Oracle Database is running on the host network (e.g., localhost) +* Credentials are passed via environment variables `ORACLE_USER` and `ORACLE_PASSWORD` + +```json +{ + "mcpServers": { + "oracle": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", "ORACLE_USER=myuser", + "-e", "ORACLE_PASSWORD=mypassword", + "mochoa/mcp-oracle", + "host.docker.internal:1521/freepdb1" + ] + } + } +} +``` + +Note: You can still provide the connection string as a final argument if you want to connect automatically on startup: `"args": [..., "mochoa/mcp-oracle", "host.docker.internal:1521/freepdb1"]`. + +## Sources + +As usual the code of this extension is at [GitHub](https://github.com/marcelo-ochoa/servers), feel free to suggest changes and make contributions, note that I am a beginner developer of React and TypeScript so contributions to make this UI better are welcome. + +## Compared to SQLcl MCP server + +Using SQLcl Docker extension you could register a connection using: + +```sh +docker exec --user sqlcl -ti mochoa_sqlcl-docker-extension-desktop-extension-service /opt/sqlcl/bin/sql -save hr_mcp -savepwd hr/hr_2025@host.docker.internal:1521/freepdb1 +``` + +after that using this registration: + +```yml +{ + "mcpServers": { + "sqlcl-mcp-server": { + "type": "stdio", + "command": "docker", + "args": [ + "exec", + "--user", + "sqlcl", + "-i", + "mochoa_sqlcl-docker-extension-desktop-extension-service", + "/opt/sqlcl/bin/sql", + "-mcp" + ] + } + } +} +``` + +Just replace above Demo prompts instead of "orcl-query" tool use "run-sql". + +## 📜 License + +This project is licensed under the Apache License, Version 2.0 for new contributions, with existing code under MIT - see the [LICENSE](https://github.com/marcelo-ochoa/servers/blob/main/src/oracle/LICENSE) file for details. diff --git a/src/oracle/db.ts b/src/oracle/db.ts new file mode 100644 index 0000000000..8b95339a0b --- /dev/null +++ b/src/oracle/db.ts @@ -0,0 +1,98 @@ +import oracledb from "oracledb"; + +let pool: oracledb.Pool | undefined = undefined; +let thickModeInitialized = false; + +export 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); + } + + // Initialize thick mode if not already done + // This is required for databases that use Advanced Networking Option (ANO) + // encryption and data integrity features + if (!thickModeInitialized) { + try { + // Try to initialize thick mode with explicit library path + // This may fail if Oracle Instant Client is not installed, but will + // fall back to thin mode for databases that don't require encryption + const libDir = process.env.LD_LIBRARY_PATH || '/usr/lib/instantclient'; + oracledb.initOracleClient({ libDir }); + thickModeInitialized = true; + //console.log(`Oracle thick mode initialized successfully with libDir: ${libDir}`); + } catch (err) { + // If thick mode initialization fails, continue with thin mode + // This will work for databases that don't require ANO + console.warn("Could not initialize Oracle thick mode:", err); + console.warn("Continuing in thin mode (may not work with encrypted connections)"); + } + } + + try { + pool = await oracledb.createPool({ + user: dbUser, + password: dbPassword, + connectionString, + poolMin: 4, + poolMax: 10, + poolIncrement: 1, + queueTimeout: 60000, + }); + } catch (err) { + console.error("connectionString:", connectionString); + console.error("Error initializing connection pool:", err); + process.exit(1); + } +} + +export function isPoolInitialized(): boolean { + return pool !== undefined; +} + +export function getPool(): oracledb.Pool { + if (!pool) { + throw new Error("Oracle connection pool not initialized. Use orcl-connect tool first."); + } + return pool; +} + +export async function withConnection(callback: (connection: oracledb.Connection) => Promise): Promise { + const pool = getPool(); + let connection: oracledb.Connection | undefined; + try { + connection = await pool.getConnection(); + return await callback(connection); + } finally { + if (connection) { + try { + await connection.close(); + } catch (err) { + console.error("Error closing Oracle connection:", err); + } + } + } +} + +export function getPoolStatus(): string { + if (!pool) { + return "Pool not initialized"; + } + return `Pool created with ${pool.poolMin} min, ${pool.poolMax} max connections. Connections open: ${pool.connectionsOpen}, in use: ${pool.connectionsInUse}.`; +} + +export async function closePool() { + if (pool) { + try { + await pool.close(0); + pool = undefined; + } catch (err) { + console.error("Error closing pool:", err); + } + } +} diff --git a/src/oracle/gordon-mcp.yml b/src/oracle/gordon-mcp.yml new file mode 100644 index 0000000000..4af76e83b5 --- /dev/null +++ b/src/oracle/gordon-mcp.yml @@ -0,0 +1,9 @@ +services: + time: + image: mcp/time + oracle: + image: mochoa/mcp-oracle + command: ["host.docker.internal:1521/freepdb1"] + environment: + - ORACLE_USER=scott + - ORACLE_PASSWORD=tiger diff --git a/src/oracle/handlers.ts b/src/oracle/handlers.ts new file mode 100644 index 0000000000..44b246b5e3 --- /dev/null +++ b/src/oracle/handlers.ts @@ -0,0 +1,25 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { queryHandler } from "./tools/query.js"; +import { explainHandler } from "./tools/explain.js"; +import { statsHandler } from "./tools/stats.js"; +import { connectHandler } from "./tools/connect.js"; +import { awrHandler } from "./tools/awr.js"; + +export { listResourcesHandler, readResourceHandler } from "./resources.js"; + +const toolHandlers: Record Promise> = { + "orcl-query": queryHandler, + "orcl-explain": explainHandler, + "orcl-stats": statsHandler, + "orcl-connect": connectHandler, + "orcl-awr": awrHandler, +}; + +export const callToolHandler = async (request: CallToolRequest) => { + const handler = toolHandlers[request.params.name]; + if (handler) { + return handler(request); + } + throw new Error(`Unknown tool: ${request.params.name}`); +}; + diff --git a/src/oracle/images/ai-pair-programming.png b/src/oracle/images/ai-pair-programming.png new file mode 100644 index 0000000000..535cc464bc Binary files /dev/null and b/src/oracle/images/ai-pair-programming.png differ diff --git a/src/oracle/images/antigravity.png b/src/oracle/images/antigravity.png new file mode 100644 index 0000000000..532e4ee05b Binary files /dev/null and b/src/oracle/images/antigravity.png differ diff --git a/src/oracle/images/demo-prompts.gif b/src/oracle/images/demo-prompts.gif new file mode 100644 index 0000000000..81b6dd4c71 Binary files /dev/null and b/src/oracle/images/demo-prompts.gif differ diff --git a/src/oracle/images/gemini-cli-both-plans.png b/src/oracle/images/gemini-cli-both-plans.png new file mode 100644 index 0000000000..38141f40cd Binary files /dev/null and b/src/oracle/images/gemini-cli-both-plans.png differ diff --git a/src/oracle/images/gemini-cli-connect.png b/src/oracle/images/gemini-cli-connect.png new file mode 100644 index 0000000000..5761547a7b Binary files /dev/null and b/src/oracle/images/gemini-cli-connect.png differ diff --git a/src/oracle/images/gemini-cli-explain-1.png b/src/oracle/images/gemini-cli-explain-1.png new file mode 100644 index 0000000000..9c0ccf1988 Binary files /dev/null and b/src/oracle/images/gemini-cli-explain-1.png differ diff --git a/src/oracle/images/gemini-cli-explain-2.png b/src/oracle/images/gemini-cli-explain-2.png new file mode 100644 index 0000000000..c5377ee555 Binary files /dev/null and b/src/oracle/images/gemini-cli-explain-2.png differ diff --git a/src/oracle/images/gemini-cli-new-query.png b/src/oracle/images/gemini-cli-new-query.png new file mode 100644 index 0000000000..9dfcb4f15c Binary files /dev/null and b/src/oracle/images/gemini-cli-new-query.png differ diff --git a/src/oracle/images/gemini-cli-query.png b/src/oracle/images/gemini-cli-query.png new file mode 100644 index 0000000000..200d7edeb4 Binary files /dev/null and b/src/oracle/images/gemini-cli-query.png differ diff --git a/src/oracle/images/gemini-cli-stats-1.png b/src/oracle/images/gemini-cli-stats-1.png new file mode 100644 index 0000000000..0a5275a8a3 Binary files /dev/null and b/src/oracle/images/gemini-cli-stats-1.png differ diff --git a/src/oracle/images/gemini-cli-stats-2.png b/src/oracle/images/gemini-cli-stats-2.png new file mode 100644 index 0000000000..f0ce710638 Binary files /dev/null and b/src/oracle/images/gemini-cli-stats-2.png differ diff --git a/src/oracle/images/gemini-cli-visualize.png b/src/oracle/images/gemini-cli-visualize.png new file mode 100644 index 0000000000..af675f7a31 Binary files /dev/null and b/src/oracle/images/gemini-cli-visualize.png differ diff --git a/src/oracle/index.ts b/src/oracle/index.ts new file mode 100644 index 0000000000..db4f6bd172 --- /dev/null +++ b/src/oracle/index.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { runServer } from "./server.js"; + +runServer().catch(console.error); \ No newline at end of file diff --git a/src/oracle/oracle-ai-pair-programing.md b/src/oracle/oracle-ai-pair-programing.md new file mode 100644 index 0000000000..e93c695579 --- /dev/null +++ b/src/oracle/oracle-ai-pair-programing.md @@ -0,0 +1,57 @@ +# Oracle AI Pair Programming: A New Paradigm + +![Oracle AI Pair Programming](./images/ai-pair-programming.png) + +## The Thesis: Beyond Vive Coding + +Instead of viewing AI-assisted development as "vive coding," we propose a more accurate and powerful metaphor: **AI Pair Programming**. With an AI agent integrated into your development environment and powered by the [Model Context Protocol (MCP) server](https://github.com/marcelo-ochoa/servers/tree/main/src/oracle), the experience is not one of performing for an audience, but of collaborating with a partner. + +## What is "Vive Coding"? + +Vive coding typically refers to the practice of writing code in real-time, often for an audience, such as in a presentation, a tutorial, or a streaming session. The focus is on the performative act of coding itself. While engaging, this term doesn't fully capture the interactive and collaborative nature of working with a modern AI assistant. + +## The Power of Traditional Pair Programming + +Pair programming is an agile software development technique in which two programmers work together at one workstation. One, the "driver," writes code while the other, the "navigator," reviews each line of code as it is typed in. The two programmers switch roles frequently. This methodology leads to: + +* Higher code quality +* Better transfer of knowledge between team members +* Increased discipline and focus +* Reduced risk of errors + +## AI as Your Pair Programming Partner + +The paradigm of AI Pair Programming reframes the developer's interaction with AI. If we replace one of the programmers in the traditional pair programming model with a sophisticated AI agent, we get a more accurate description of the modern AI-assisted development workflow. + +In this model: + +* **The Developer is the Driver:** You are in control, writing code, making the final decisions, and steering the direction of the project. +* **The AI is the Navigator:** The AI agent, with the deep context provided by the [MCP server](https://github.com/marcelo-ochoa/servers/tree/main/src/oracle), acts as your navigator. It can: + * Suggest code completions and entire functions. + * Identify potential bugs and offer solutions. + * Answer questions about the codebase or external libraries. + * Perform database-related tasks (querying, explaining plans, checking stats) directly within the editor. + * Refactor code for better readability and performance. + * Valid for all developer roles (junior, semi-senior, senior, DBA, etc) + +This is not just about generating code; it's a continuous, interactive dialogue between the developer and the AI, mirroring the collaborative synergy of a human pair. + +## Benefits of Oracle AI Pair Programming + +Adopting this model offers significant benefits: + +* **Increased Productivity:** Offload repetitive tasks and get instant suggestions to accelerate development cycles. +* **Improved Code Quality:** Leverage the AI's ability to spot errors, suggest best practices, and ensure consistency. +* **Seamless Database Integration:** The [MCP server](https://github.com/marcelo-ochoa/servers/tree/main/src/oracle) provides the AI with real-time context about the Oracle database, allowing you to interact with it using natural language without leaving your IDE. +* **Enhanced Learning:** The AI can explain complex code or database concepts, acting as a patient and knowledgeable mentor. +* **Reduced Cognitive Load:** With the AI handling routine checks and information retrieval, you can focus on the more creative and complex aspects of problem-solving. + +## Conclusion + +Viewing AI-assisted development through the lens of "live coding" is limiting. The **Oracle AI Pair Programming** model offers a richer, more accurate, and more powerful framework. It emphasizes collaboration, not performance, and highlights the true potential of an AI agent integrated deeply into the development workflow via the MCP server. By embracing the AI as a partner, developers can write better code, faster, and with greater confidence. + +## Related Links + +- [Pair Programming - Wikipedia](https://en.wikipedia.org/wiki/Pair_programming) +- [Vive Coding - Wikipedia](https://en.wikipedia.org/wiki/Vibe_coding) +- [Oracle SQLcl MCP Server](https://docs.oracle.com/en/database/oracle/sql-developer-command-line/25.3/sqcug/sqlcl-mcp-server.html) diff --git a/src/oracle/package-lock.json b/src/oracle/package-lock.json new file mode 100644 index 0000000000..1c127fcc36 --- /dev/null +++ b/src/oracle/package-lock.json @@ -0,0 +1,3400 @@ +{ + "name": "@marcelo-ochoa/server-oracle", + "version": "0.7.4", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@marcelo-ochoa/server-oracle", + "version": "0.7.4", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.19.1", + "oracledb": "^6.1.0" + }, + "bin": { + "mcp-server-oracle": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^22", + "@types/oracledb": "^6.5.1", + "@vitest/coverage-v8": "^2.1.8", + "shx": "^0.3.4", + "typescript": "^5.6.2", + "vitest": "^2.1.8" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.2.tgz", + "integrity": "sha512-6rqTdFt67AAAzln3NOKsXRmv5ZzPkgbfaebKBqUbts7vK1GZudqnrun5a8d3M/h955cam9RHZ6Jb4Y1XhnmFPg==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", + "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.18.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.13.tgz", + "integrity": "sha512-Bo45YKIjnmFtv6I1TuC8AaHBbqXtIo+Om5fE4QiU1Tj8QR/qt+8O3BAtOimG5IFmwaWiPmB3Mv3jtYzBA4Us2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/oracledb": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@types/oracledb/-/oracledb-6.10.0.tgz", + "integrity": "sha512-dRaEYKRkJhSSM/uKrscE7zXC5D75JSkBgdye5kSxzTRwrMAUI5V675cD3fqCdMuSOiGo2K9Ng3CjjRI4rzeuAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", + "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.7", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.12", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "2.1.9", + "vitest": "2.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/oracledb": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/oracledb/-/oracledb-6.10.0.tgz", + "integrity": "sha512-kGUumXmrEWbSpBuKJyb9Ip3rXcNgKK6grunI3/cLPzrRvboZ6ZoLi9JQ+z6M/RIG924tY8BLflihL4CKKQAYMA==", + "hasInstallScript": true, + "license": "(Apache-2.0 OR UPL-1.0)", + "engines": { + "node": ">=14.17" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", + "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.5", + "@rollup/rollup-android-arm64": "4.52.5", + "@rollup/rollup-darwin-arm64": "4.52.5", + "@rollup/rollup-darwin-x64": "4.52.5", + "@rollup/rollup-freebsd-arm64": "4.52.5", + "@rollup/rollup-freebsd-x64": "4.52.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", + "@rollup/rollup-linux-arm-musleabihf": "4.52.5", + "@rollup/rollup-linux-arm64-gnu": "4.52.5", + "@rollup/rollup-linux-arm64-musl": "4.52.5", + "@rollup/rollup-linux-loong64-gnu": "4.52.5", + "@rollup/rollup-linux-ppc64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-musl": "4.52.5", + "@rollup/rollup-linux-s390x-gnu": "4.52.5", + "@rollup/rollup-linux-x64-gnu": "4.52.5", + "@rollup/rollup-linux-x64-musl": "4.52.5", + "@rollup/rollup-openharmony-arm64": "4.52.5", + "@rollup/rollup-win32-arm64-msvc": "4.52.5", + "@rollup/rollup-win32-ia32-msvc": "4.52.5", + "@rollup/rollup-win32-x64-gnu": "4.52.5", + "@rollup/rollup-win32-x64-msvc": "4.52.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.3", + "shelljs": "^0.8.5" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} \ No newline at end of file diff --git a/src/oracle/package.json b/src/oracle/package.json new file mode 100644 index 0000000000..c5b28a24dc --- /dev/null +++ b/src/oracle/package.json @@ -0,0 +1,48 @@ +{ + "name": "@marcelo-ochoa/server-oracle", + "mcpName": "io.github.marcelo-ochoa/oracle", + "version": "1.0.7", + "repository": { + "type": "git", + "url": "https://github.com/marcelo-ochoa/servers.git", + "subfolder": "src/oracle" + }, + "description": "An MCP server for Oracle databases.", + "keywords": [ + "read-only-mcp", + "oracle-database", + "ai-agent", + "llm-tool", + "oci", + "rag" + ], + "license": "MIT", + "author": "Marcelo Fabian Ochoa", + "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.24.2", + "@toon-format/toon": "^1.0.0", + "oracledb": "^6.1.0" + }, + "devDependencies": { + "@types/node": "^22", + "@types/oracledb": "^6.5.1", + "@vitest/coverage-v8": "^2.1.8", + "shx": "^0.3.4", + "typescript": "^5.6.2", + "vitest": "^2.1.8" + } +} \ No newline at end of file diff --git a/src/oracle/resources.ts b/src/oracle/resources.ts new file mode 100644 index 0000000000..0ef3c2fa2d --- /dev/null +++ b/src/oracle/resources.ts @@ -0,0 +1,69 @@ +import { ListResourcesRequest, ReadResourceRequest } from "@modelcontextprotocol/sdk/types.js"; +import { withConnection, isPoolInitialized } from "./db.js"; +import oracledb from "oracledb"; + +const SCHEMA_PATH = "schema"; + +export const listResourcesHandler = async (request: ListResourcesRequest) => { + if (!isPoolInitialized()) { + return { resources: [] }; + } + if (!process.env.ORACLE_USER) { + throw new Error("ORACLE_USER environment variable is not set"); + } + const resourceBaseUrl = new URL("oracle://" + process.env.ORACLE_USER.toUpperCase()); + resourceBaseUrl.protocol = "oracle:"; + resourceBaseUrl.password = ""; + + return await withConnection(async (connection) => { + 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`, + })), + }; + }); +}; + + +export const readResourceHandler = async (request: ReadResourceRequest) => { + 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"); + } + + return await withConnection(async (connection) => { + const result = await connection.execute<{ METADATA: string }>( + `select json_serialize(dbms_developer.get_metadata (name => UPPER(:tableName))) as metadata from dual`, + [tableName], + { + outFormat: oracledb.OUT_FORMAT_OBJECT, + fetchInfo: { "METADATA": { type: oracledb.STRING } } + } + ); + + const metadata = result.rows?.[0]?.METADATA || "{}"; + + return { + contents: [ + { + uri: request.params.uri, + mimeType: "application/json", + text: metadata, + }, + ], + isError: false, + }; + }); +}; diff --git a/src/oracle/server.json b/src/oracle/server.json new file mode 100644 index 0000000000..4d31144111 --- /dev/null +++ b/src/oracle/server.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.marcelo-ochoa/oracle", + "description": "MCP server for interacting with Oracle databases", + "repository": { + "url": "https://github.com/marcelo-ochoa/servers", + "source": "github", + "subfolder": "src/oracle" + }, + "version": "1.0.7", + "packages": [ + { + "registryType": "npm", + "identifier": "@marcelo-ochoa/server-oracle", + "version": "1.0.7", + "transport": { + "type": "stdio" + }, + "packageArguments": [ + { + "type": "positional", + "valueHint": "connectionString", + "description": "Oracle connection string", + "isRequired": false + } + ], + "environmentVariables": [ + { + "description": "Oracle user name", + "isRequired": false, + "format": "string", + "isSecret": false, + "name": "ORACLE_USER" + }, + { + "description": "Oracle password", + "isRequired": false, + "format": "string", + "isSecret": true, + "name": "ORACLE_PASSWORD" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/oracle/server.ts b/src/oracle/server.ts new file mode 100644 index 0000000000..4fe21cd4f8 --- /dev/null +++ b/src/oracle/server.ts @@ -0,0 +1,107 @@ +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import { initializePool } from "./db.js"; +import { listResourcesHandler, readResourceHandler, callToolHandler } from "./handlers.js"; +import { tools } from "./tools.js"; + +// Create server instance +const server = new McpServer({ + name: "oracle-server", + version: "1.0.7", +}); + +const prompts = [ + { name: "orcl-query: Execute Query", description: "orcl-query select * from COUNTRIES" }, + { name: "orcl-explain: Explain Query", description: "orcl-explain select * from COUNTRIES" }, + { name: "orcl-stats: Table Statistics", description: "orcl-stats COUNTRIES" }, + { name: "orcl-connect: Database Connection", description: "orcl-connect to Oracle using an string like host.docker.internal:1521/freepdb1 user name and password" }, + { name: "orcl-awr: Performance Report", description: "orcl-awr with optional sql_id, requires SELECT_CATALOG_ROLE and grant execute on DBMS_WORKLOAD_REPOSITORY package" } +]; + +// Register Prompts +server.registerPrompt("orcl-prompts", { + description: "List available Oracle prompts" +}, async () => ({ + messages: [ + { + role: "assistant", + content: { + type: "text", + text: "Available Oracle prompts:\n" + prompts.map(p => `- ${p.name}: ${p.description}`).join("\n") + } + } + ] +})); + +// Register Resource Templates +const resourceTemplate = new ResourceTemplate("oracle://{user}/{table_name}/schema", { + list: async () => listResourcesHandler({} as any) +}); +server.registerResource( + "Table Schema", + resourceTemplate, + { description: "Schema information for an Oracle database table including column names and data types" }, + async (uri) => { + return readResourceHandler({ params: { uri: uri.href } } as any); + } +); + +// Register Tools +tools.forEach(tool => { + // Basic mapping of JSON schema to Zod for simple cases + let inputSchema: any = z.object({}); + if (tool.inputSchema && tool.inputSchema.properties) { + const shape: Record = {}; + for (const [key, prop] of Object.entries(tool.inputSchema.properties)) { + let field: any = z.any(); + if ((prop as any).type === "string") { + field = z.string(); + } + if ((prop as any).description) { + field = field.describe((prop as any).description); + } + if (tool.inputSchema.required && !tool.inputSchema.required.includes(key)) { + field = field.optional(); + } else if (!tool.inputSchema.required) { + field = field.optional(); + } + shape[key] = field; + } + inputSchema = z.object(shape); + } + + server.registerTool( + tool.name, + { + description: tool.description, + inputSchema: inputSchema + }, + async (args: any) => { + return callToolHandler({ params: { name: tool.name, arguments: args } } as any); + } + ); +}); + +export async function runServer() { + const args = process.argv.slice(2); + const connectionString = args[0]; + + if (connectionString) { + if (!process.env.ORACLE_USER) { + console.error("Error: Environment variable ORACLE_USER must be set."); + process.exit(1); + } + await initializePool(connectionString); // Initialize the pool before starting the server + } else { + console.error("Warning: No Oracle connection string provided. Use orcl-connect tool before using other functionality."); + } + const transport = new StdioServerTransport(); + await server.connect(transport); + + process.stdin.on("close", () => { + console.error("Oracle MCP Server closed"); + server.close(); + process.exit(0); + }); +} diff --git a/src/oracle/tools.ts b/src/oracle/tools.ts new file mode 100644 index 0000000000..cd57144893 --- /dev/null +++ b/src/oracle/tools.ts @@ -0,0 +1,109 @@ +export const tools = [ + { + name: "orcl-query", + description: "This tool executes SQL queries in a READY ONLY session connected to an Oracle database. If no active connection exists, it uses MCP server registration argument and environment variables ORACLE_USER and ORACLE_PASSWORD.\n\nYou should:\n\n\tExecute the provided SQL query.\n\n\tReturn the results in Json format.\n\nArgs:\n\n\tsql: The SQL query to execute.\n\n\nThe `model` argument should specify only the name and version of the LLM (Large Language Model) you are using, with no additional information.\nThe `mcp_client` argument should specify only the name of the MCP (Model Context Protocol) client you are using, with no additional information.\n\nReturns:\n\n\tJson-formatted query results.\nFor every SQL query you generate, please include a comment at the beginning of the SELECT statement (or other main SQL command) that identifies the LLM model name and version you are using. Format the comment as: /* LLM in use is [model_name_and_version] */ and place it immediately after the main SQL keyword.\nFor example:\n\nSELECT /* LLM in use is claude-sonnet-4 */ column1, column2 FROM table_name;\n\nPlease apply this format consistently to all SQL queries you generate, using your actual model name and version in the comment\n", + inputSchema: { + type: "object", + properties: { + sql: { + type: "string", + description: "The SQL query to execute" + }, + mcp_client: { + "type": "string", + "description": "Specify the name and version of the MCP client implementation being used (e.g. Copilot, Claude, Cline...)", + "default": "UNKNOWN-MCP-CLIENT" + }, + model: { + "type": "string", + "description": "The name (and version) of the language model being used by the MCP client to process requests (e.g. gpt-4.1, claude-sonnet-4, llama4...)", + "default": "UNKNOWN-LLM" + } + }, + required: ["sql", "mcp_client", "model"] + }, + }, + { + name: "orcl-explain", + description: "Generate and display the execution plan for a given SQL query using Oracle's EXPLAIN PLAN command. This tool helps you understand how Oracle will execute your query, including information about table access methods, join operations, indexes used, estimated costs, and cardinality estimates.\n\nArgs:\n\n\tsql: The SQL query to explain.\n\n\nThe `model` argument should specify only the name and version of the LLM (Large Language Model) you are using, with no additional information.\nThe `mcp_client` argument should specify only the name of the MCP (Model Context Protocol) client you are using, with no additional information.\n\nReturns:\n\n\tDetailed execution plan showing how Oracle will process the query, including operation types, object names, costs, bytes, and cardinality estimates.\n", + inputSchema: { + type: "object", + properties: { + sql: { + type: "string", + description: "The SQL query to explain" + }, + mcp_client: { + "type": "string", + "description": "Specify the name and version of the MCP client implementation being used (e.g. Copilot, Claude, Cline...)", + "default": "UNKNOWN-MCP-CLIENT" + }, + model: { + "type": "string", + "description": "The name (and version) of the language model being used by the MCP client to process requests (e.g. gpt-4.1, claude-sonnet-4, llama4...)", + "default": "UNKNOWN-LLM" + } + }, + required: ["sql", "mcp_client", "model"] + }, + }, + { + name: "orcl-stats", + description: "Get comprehensive statistics for a specific Oracle database object (table, index, or view). This tool retrieves detailed information including row counts, block statistics, segment size, index information, column statistics, partition details, and other metadata that can help optimize queries and understand data distribution.\n\nArgs:\n\n\tname: The name of the database object to get statistics for.\n\n\nThe `model` argument should specify only the name and version of the LLM (Large Language Model) you are using, with no additional information.\nThe `mcp_client` argument should specify only the name of the MCP (Model Context Protocol) client you are using, with no additional information.\n\nReturns:\n\n\tJson-formatted object statistics including row counts, block usage, size information, indexes, partitions, and column details with histograms and data distribution metrics.\n", + inputSchema: { + type: "object", + properties: { + name: { + type: "string", + description: "SQL Object to get stats" + }, + mcp_client: { + "type": "string", + "description": "Specify the name and version of the MCP client implementation being used (e.g. Copilot, Claude, Cline...)", + "default": "UNKNOWN-MCP-CLIENT" + }, + model: { + "type": "string", + "description": "The name (and version) of the language model being used by the MCP client to process requests (e.g. gpt-4.1, claude-sonnet-4, llama4...)", + "default": "UNKNOWN-LLM" + } + }, + required: ["name", "mcp_client", "model"] + }, + }, + { + name: "orcl-connect", + description: "Provides an interface to connect to a specified database. If a database connection is already active, prompt the user for confirmation before switching to the new connection.\n\n\nThe `model` argument should only be used to specify the name and version of the LLM (Large Language Model) you are using, with no additional information.\nThe `mcp_client` argument should specify only the name of the MCP (Model Context Protocol) client you are using, with no additional information.\n", + inputSchema: { + type: "object", + properties: { + connectionString: { + "type": "string", + "description": "The Oracle connect string (e.g. host.docker.internal:1521/freepdb1", + "default": "none, this parameter is required" + }, + user: { + "type": "string", + "description": "The Oracle user (e.g. scott", + "default": "none, this parameter is required" + }, + password: { + "type": "string", + "description": "The Oracle password (e.g. tiger", + "default": "none, this parameter is required" + }, + }, + required: ["connectionString", "user", "password"], + }, + }, + { + name: "orcl-awr", + description: "Generate an Automatic Workload Repository (AWR) report or AWR SQL report for Oracle database performance analysis. AWR reports provide comprehensive performance metrics including database statistics, wait events, top SQL statements, system resources, and performance recommendations.\n\nIf no sql_id is provided, generates a full AWR database report with:\n- Database instance information and configuration\n- Load profile and instance efficiency metrics\n- Top wait events and time model statistics\n- SQL statistics ordered by various metrics (elapsed time, CPU time, executions, etc.)\n- Segment statistics and I/O statistics\n- Memory and SGA statistics\n- System and session statistics\n\nIf a sql_id is provided, generates an AWR SQL report focused on that specific SQL statement with:\n- SQL text and execution statistics\n- Execution plans and plan history\n- Bind variable information\n- Wait events specific to the SQL\n- Performance metrics over time\n\nArgs:\n\n\tsql_id (optional): The SQL ID for generating an AWR SQL report. If omitted, generates a full AWR database report.\n\nReturns:\n\n\tComprehensive performance report in text or HTML format with actionable insights for database tuning and optimization.\n", + inputSchema: { + type: "object", + properties: { + sql_id: { type: "string" }, + }, + }, + }, +]; diff --git a/src/oracle/tools/awr.ts b/src/oracle/tools/awr.ts new file mode 100644 index 0000000000..63de8baec6 --- /dev/null +++ b/src/oracle/tools/awr.ts @@ -0,0 +1,93 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { withConnection } from "../db.js"; +import oracledb from "oracledb"; + +export const awrHandler = async (request: CallToolRequest) => { + try { + return await withConnection(async (connection) => { + // Step 1: Get dbid + const dbidResult = await connection.execute<{ DBID: number }> + (`SELECT dbid FROM v$database`, [], { outFormat: oracledb.OUT_FORMAT_OBJECT }) + const dbid = dbidResult.rows?.[0]?.DBID; + if (!dbid) { + return { + content: [{ type: "text", text: "Could not retrieve DBID from v$database." }], + isError: true, + }; + } + // Step 2: Get begin_snap_id and end_snap_id + const snapResult = await connection.execute<{ + BEGIN_SNAP_ID: number; + END_SNAP_ID: number; + }>( + `WITH multi_instance_snaps AS ( + SELECT snap_id + FROM dba_hist_snapshot + GROUP BY snap_id + HAVING COUNT(*) > 1 + ), + recent_snaps AS ( + SELECT snap_id + FROM multi_instance_snaps + ORDER BY snap_id DESC + FETCH FIRST 2 ROWS ONLY + ) + SELECT + MIN(s1.snap_id) AS begin_snap_id, + MAX(s2.snap_id) AS end_snap_id, + s1.startup_time + FROM + dba_hist_snapshot s1 + JOIN + dba_hist_snapshot s2 + ON s1.startup_time = s2.startup_time + AND s1.snap_id < s2.snap_id + WHERE + s1.snap_id IN (SELECT snap_id FROM recent_snaps) + AND s2.snap_id IN (SELECT snap_id FROM recent_snaps) + AND s1.begin_interval_time >= SYSDATE - 1 + AND s2.begin_interval_time >= SYSDATE - 1 + GROUP BY s1.startup_time + ORDER BY end_snap_id DESC + FETCH FIRST 1 ROW ONLY`, + [], + { outFormat: oracledb.OUT_FORMAT_OBJECT } + ); + const begin_snap_id = snapResult.rows?.[0]?.BEGIN_SNAP_ID; + const end_snap_id = snapResult.rows?.[0]?.END_SNAP_ID; + if (!begin_snap_id || !end_snap_id) { + return { + content: [{ type: "text", text: "Could not retrieve snapshot IDs." }], + isError: true, + }; + } + // Step 3: Generate AWR report + const sql_id = request.params.arguments?.sql_id; + let awrResult; + if (!sql_id) { + awrResult = await connection.execute( + `SELECT output FROM TABLE(dbms_workload_repository.awr_report_text(:dbid, 1, :begin_snap_id, :end_snap_id))`, + [dbid, begin_snap_id, end_snap_id], + { outFormat: oracledb.OUT_FORMAT_OBJECT } + ); + } else { + awrResult = await connection.execute( + `SELECT output FROM TABLE(dbms_workload_repository.awr_sql_report_text(:dbid, 1, :begin_snap_id, :end_snap_id, :sql_id))`, + [dbid, begin_snap_id, end_snap_id, sql_id], + { outFormat: oracledb.OUT_FORMAT_OBJECT } + ); + } + // The report is a multi-row text output, concatenate all rows + const reportText = awrResult.rows?.map((r: any) => r.OUTPUT).join("\n") ?? "No report output."; + return { + content: [{ type: "text", text: reportText }], + isError: false, + }; + }); + } catch (error: any) { + return { + content: [{ type: "text", text: `Error generating AWR report: ${error?.message ?? error}` }], + isError: true, + }; + } +}; diff --git a/src/oracle/tools/connect.ts b/src/oracle/tools/connect.ts new file mode 100644 index 0000000000..f14cf6d3a5 --- /dev/null +++ b/src/oracle/tools/connect.ts @@ -0,0 +1,34 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { initializePool, closePool } from "../db.js"; + +export const connectHandler = async (request: CallToolRequest) => { + const newConnectionString = request.params.arguments?.connectionString; + const newUser = request.params.arguments?.user; + const newPassword = request.params.arguments?.password; + if ( + typeof newConnectionString !== "string" || !newConnectionString || + typeof newUser !== "string" || !newUser || + typeof newPassword !== "string" || !newPassword + ) { + return { + content: [{ type: "text", text: "Missing or invalid connectionString, user, or password argument." }], + isError: true, + }; + } + try { + await closePool(); + // Override env vars for this session + process.env.ORACLE_USER = newUser; + process.env.ORACLE_PASSWORD = newPassword; + await initializePool(newConnectionString); + return { + content: [{ type: "text", text: `Successfully connected to new Oracle DB: ${newConnectionString} as user ${newUser}` }], + isError: false, + }; + } catch (err) { + return { + content: [{ type: "text", text: `Failed to connect: ${err}` }], + isError: true, + }; + } +}; diff --git a/src/oracle/tools/explain.ts b/src/oracle/tools/explain.ts new file mode 100644 index 0000000000..406e2f6c06 --- /dev/null +++ b/src/oracle/tools/explain.ts @@ -0,0 +1,19 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { encode } from "@toon-format/toon"; +import { withConnection } from "../db.js"; +import oracledb from "oracledb"; + +export const explainHandler = async (request: CallToolRequest) => { + const sql = typeof request.params.arguments?.sql === "string" + ? request.params.arguments.sql.replace(/;\s*$/, "") + : ""; + + return await withConnection(async (connection) => { + 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: encode(result.rows) }], + isError: false, + }; + }); +}; diff --git a/src/oracle/tools/query.ts b/src/oracle/tools/query.ts new file mode 100644 index 0000000000..16f01a847c --- /dev/null +++ b/src/oracle/tools/query.ts @@ -0,0 +1,19 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { encode } from "@toon-format/toon"; +import { withConnection } from "../db.js"; +import oracledb from "oracledb"; + +export const queryHandler = async (request: CallToolRequest) => { + const sql = typeof request.params.arguments?.sql === "string" + ? request.params.arguments.sql.replace(/;\s*$/, "") + : ""; + + return await withConnection(async (connection) => { + await connection.execute("SET TRANSACTION READ ONLY"); + const result = await connection.execute(sql, [], { outFormat: oracledb.OUT_FORMAT_OBJECT }); + return { + content: [{ type: "text", text: encode(result.rows) }], + isError: false, + }; + }); +}; diff --git a/src/oracle/tools/stats.ts b/src/oracle/tools/stats.ts new file mode 100644 index 0000000000..e5c6736dc0 --- /dev/null +++ b/src/oracle/tools/stats.ts @@ -0,0 +1,64 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { withConnection } from "../db.js"; +import oracledb from "oracledb"; + +export const statsHandler = async (request: CallToolRequest) => { + let owner = "UPPER(USER)"; + let table = request.params.arguments?.name as string; + if (table.includes(".")) { + const parts = table.split("."); + owner = `'${parts[0].toUpperCase()}'`; + table = parts[1]; + } + + return await withConnection(async (connection) => { + const result = await connection.execute<{ STATS_JSON: string }>(`SELECT JSON_OBJECT( + 'table_stats' VALUE ( + SELECT JSON_OBJECT( + 'owner' VALUE owner, + 'table_name' VALUE table_name, + 'num_rows' VALUE num_rows, + 'blocks' VALUE blocks, + 'empty_blocks' VALUE empty_blocks, + 'avg_row_len' VALUE avg_row_len, + 'last_analyzed' VALUE TO_CHAR(last_analyzed, 'YYYY-MM-DD HH24:MI:SS') + ) + FROM all_tab_statistics + WHERE owner = ${owner} AND table_name = UPPER(:tableName) + ), + 'index_stats' VALUE ( + SELECT JSON_ARRAYAGG( + JSON_OBJECT( + 'index_name' VALUE index_name, + 'blevel' VALUE blevel, + 'leaf_blocks' VALUE leaf_blocks, + 'distinct_keys' VALUE distinct_keys, + 'num_rows' VALUE num_rows, + 'clustering_factor' VALUE clustering_factor, + 'last_analyzed' VALUE TO_CHAR(last_analyzed, 'YYYY-MM-DD HH24:MI:SS') + ) + ) + FROM all_ind_statistics + WHERE table_owner = ${owner} AND table_name = UPPER(:tableName) + ), + 'column_stats' VALUE ( + SELECT JSON_ARRAYAGG( + JSON_OBJECT( + 'column_name' VALUE column_name, + 'num_distinct' VALUE num_distinct, + 'density' VALUE density, + 'histogram' VALUE histogram, + 'last_analyzed' VALUE TO_CHAR(last_analyzed, 'YYYY-MM-DD HH24:MI:SS') + ) + ) + FROM all_tab_col_statistics + WHERE owner = ${owner} AND table_name = UPPER(:tableName) + ) + ) AS stats_json + FROM dual`, [table, table, table], { outFormat: oracledb.OUT_FORMAT_OBJECT }); + return { + content: [{ type: "text", text: result.rows?.[0]?.STATS_JSON, mimeType: "application/json" }], + isError: false, + }; + }); +}; diff --git a/src/oracle/tsconfig.json b/src/oracle/tsconfig.json new file mode 100644 index 0000000000..2ce5843e0b --- /dev/null +++ b/src/oracle/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": ".", + "moduleResolution": "NodeNext", + "module": "NodeNext" + }, + "include": ["./**/*.ts"] +} diff --git a/src/postgres/.dockerignore b/src/postgres/.dockerignore new file mode 100644 index 0000000000..8d2924846b --- /dev/null +++ b/src/postgres/.dockerignore @@ -0,0 +1,3 @@ +dist +node_modules +*.js.map diff --git a/src/postgres/AWR_example.md b/src/postgres/AWR_example.md new file mode 100644 index 0000000000..2db050c18f --- /dev/null +++ b/src/postgres/AWR_example.md @@ -0,0 +1,169 @@ +## PostgreSQL AWR in action + +**Sample prompts**: +- Generate a performance report using pg-awr +- Find top 5 SQL statement by CPU and IO usage, suggest optimization if any +- Model Claude Sonnet 4.5 (Thinking) + +Here is an example of a performance report generated by the `pg-awr` tool for a production Moodle database, highlighting critical performance issues and optimization opportunities: + +### **Database Configuration** +- **Database**: moodle +- **Active Connections**: 2 +- **Transactions Committed**: 13,281 +- **Transactions Rolled Back**: 11 + +### **Performance Metrics** + +#### **Buffer Cache** 🔴 **Critical Issue** +- **Cache Hit Ratio**: 39.32% ⚠️ (Very Low - significant disk I/O) +- **Blocks Read**: 5.16M (from disk) +- **Blocks Hit**: 3.34M (from cache) +- **Target**: >95% for optimal performance + +#### **Temporary Files** 🔴 **Critical Issue** +- **Temp Files**: 346 ⚠️ (Very High) +- **Temp Bytes**: 3.8 GB ⚠️ (Indicates insufficient work_mem) +- **Deadlocks**: 0 ✅ + +#### **Data Activity** +- **Tuples Returned**: 158.8M +- **Tuples Fetched**: 406,157 +- **Tuples Inserted**: 32.8M +- **Tuples Updated**: 5,545 +- **Tuples Deleted**: 14 + +### **Top 5 Tables by CPU Usage (Sequential Scans)** + +#### **1. mdl_logstore_standard_log** 🔴 **Critical** +- **Sequential Scans**: 10 scans +- **Rows Read via Seq Scan**: 87.4M rows +- **Average Rows per Scan**: 8.7M rows +- **Index Scans**: Only 6 (very low) +- **Issue**: Massive full table scans with minimal index usage + +**Optimization**: +```sql +-- Time-based queries +CREATE INDEX CONCURRENTLY idx_logstore_timecreated +ON mdl_logstore_standard_log(timecreated) +WHERE timecreated IS NOT NULL; + +-- User-based queries +CREATE INDEX CONCURRENTLY idx_logstore_userid_time +ON mdl_logstore_standard_log(userid, timecreated); + +-- Context-based queries +CREATE INDEX CONCURRENTLY idx_logstore_contextid +ON mdl_logstore_standard_log(contextid) +WHERE contextid IS NOT NULL; +``` + +#### **2. mdl_grade_grades_history** 🔴 **Critical** +- **Sequential Scans**: 20 scans +- **Rows Read**: 15.7M rows +- **Average per Scan**: 786K rows +- **Index Scans**: 0 (no indexes being used) + +**Optimization**: +```sql +CREATE INDEX CONCURRENTLY idx_grades_hist_userid_time +ON mdl_grade_grades_history(userid, timemodified); + +CREATE INDEX CONCURRENTLY idx_grades_hist_itemid +ON mdl_grade_grades_history(itemid); +``` + +#### **3. mdl_question_attempt_steps** ⚠️ **High** +- **Sequential Scans**: 8 scans +- **Rows Read**: 6.3M rows +- **Index Scans**: 0 + +**Optimization**: +```sql +CREATE INDEX CONCURRENTLY idx_attempt_steps_questionattemptid +ON mdl_question_attempt_steps(questionattemptid); + +CREATE INDEX CONCURRENTLY idx_attempt_steps_sequencenumber +ON mdl_question_attempt_steps(questionattemptid, sequencenumber); +``` + +#### **4. mdl_question_attempt_step_data** ⚠️ **High** +- **Sequential Scans**: 4 scans +- **Rows Read**: 5.0M rows +- **Average per Scan**: 1.24M rows + +**Optimization**: +```sql +CREATE INDEX CONCURRENTLY idx_attempt_step_data_attemptstepid +ON mdl_question_attempt_step_data(attemptstepid); +``` + +#### **5. mdl_analytics_predict_samples** ⚠️ **High** +- **Sequential Scans**: 6 scans +- **Rows Read**: 4.7M rows + +**Optimization**: +```sql +CREATE INDEX CONCURRENTLY idx_analytics_samples_modelid_time +ON mdl_analytics_predict_samples(modelid, timecreated, rangeindex); +``` + +### **Unused Indexes** 🟡 **Medium Priority** + +Found **10 unused indexes** consuming ~2.5 GB of disk space: +- `mdl_logsstanlog_useconconcr_ix` (985 MB) - 0 scans +- `mdl_logsstanlog_tim_ix` (375 MB) - 0 scans +- `mdl_logsstanlog_con_ix` (375 MB) - 0 scans +- `mdl_logsstanlog_id_pk` (375 MB) - 0 scans +- And 6 more... + +**Action**: Review and drop unused indexes to improve write performance and save disk space. + +### **Recommendations** + +#### 🔴 **Critical Priority** + +1. **Increase Buffer Cache** (shared_buffers) + - Current cache hit ratio: 39.32% (should be >95%) + - **Action**: Increase `shared_buffers` to at least 25% of system RAM + - Example: If you have 16GB RAM, set `shared_buffers = 4GB` + +2. **Increase Work Memory** (work_mem) + - 346 temporary files created, consuming 3.8 GB + - **Action**: Increase `work_mem` to at least 64MB + - This will reduce disk I/O for sorts and joins + +3. **Add Missing Indexes** + - Tables are experiencing massive sequential scans (87M+ rows) + - **Action**: Create indexes as shown in optimization suggestions above + +#### 🟡 **Medium Priority** + +4. **Remove Unused Indexes** + - 10 indexes consuming ~2.5 GB with 0 scans + - **Action**: Evaluate and drop unused indexes + +5. **Enable pg_stat_statements** + - Get query-level statistics for better optimization + ```sql + -- Add to postgresql.conf + shared_preload_libraries = 'pg_stat_statements' + pg_stat_statements.track = all + ``` + +### **Configuration Recommendations** + +Add to `postgresql.conf`: +```ini +shared_buffers = 4GB # Increase from current (adjust based on RAM) +work_mem = 64MB # Increase from current +effective_cache_size = 12GB # Set to ~75% of system RAM +maintenance_work_mem = 512MB # For faster index creation/vacuum +``` + +**Estimated Impact**: These optimizations should: +- Improve cache hit ratio from 39% to >90% +- Reduce sequential scans by 80%+ +- Eliminate excessive temporary file usage +- Free up 2.5 GB of disk space diff --git a/src/postgres/CHANGELOG.md b/src/postgres/CHANGELOG.md new file mode 100644 index 0000000000..51c8f84d39 --- /dev/null +++ b/src/postgres/CHANGELOG.md @@ -0,0 +1,161 @@ +## Change Log + +### 2026-03-11 +- **chore**: Bump server version to 1.0.8 + - Updated version to 1.0.8 across package.json, server.json, and server.ts + - Migrated to `McpServer` API from deprecated `Server` class + - Refactored resources into separate `resources.ts` for better modularity + +### 2026-03-07 +- **chore**: Bump server version to 1.0.7 + - Updated version to 1.0.7 across package.json, server.json, and server.ts + - Refactored resources into separate `resources.ts` for better modularity. + + +### 2026-02-10 +- **feat**: Add SSL/Encryption support to PostgreSQL connections + - Modified `initializePool` in `db.ts` to support `sslmode` in connection strings + - Added support for `PG_SSL=true` environment variable to enforce encryption + - Updated `pg.Pool` configuration to allow self-signed certificates (`rejectUnauthorized: false`) for cloud compatibility + - Updated `pg-connect` tool description and `README.md` with encryption setup instructions +- **chore**: Bump server version to 1.0.6 + - Updated version to 1.0.6 across package.json, server.json, and server.ts + +### 2026-01-22 +- **chore**: Bump server version to 1.0.5 + - Updated version to 1.0.5 across package.json, server.json, and server.ts + - Refactored prompt names to be more descriptive for better CLI visibility + +- **chore**: Bump server version to 1.0.4 + - Updated version to 1.0.4 across package.json, server.json, and server.ts + +### 2026-01-07 +- **feat**: Make initial connection string optional at startup + - Modified `runServer` to allow server startup without a database URL + - Added warning message when starting without a connection string + - Updated error messages to guide users to use the `pg-connect` tool + - Updated README with documentation for optional connection string and `pg-connect` tool usage + +### 2025-12-12 +- **chore**: Bump server version to 1.0.3 + - Updated version to 1.0.3 across package.json, server.json, and server.ts + - Added link to Demos.md in README for comprehensive usage examples + - Published package @marcelo-ochoa/server-postgres@1.0.3 to npm registry + - Rebuilt Docker image mochoa/mcp-postgres with updated functionality + +- **chore**: Bump server version to 1.0.2 + - Updated version to 1.0.2 across package.json, server.json, and server.ts + - Updated LICENSE link in README to point to GitHub repository + - Published package @marcelo-ochoa/server-postgres@1.0.2 to npm registry + +- **feat**: Enhanced pg-stats tool to support schema-prefixed table names + - Modified `pg-stats` handler to accept `schema.table_name` syntax (e.g., `hr.employees`) + - Added automatic schema parsing from table name + - Defaults to 'public' schema if no schema prefix is provided + - Updated all statistics queries to use dynamic schema parameter + - Improved column statistics, index statistics, and table statistics retrieval + +- **docs**: Add comprehensive demo documentation and HR sample schema + - Added Demos.md with usage examples for: + - Claude Desktop (Docker and NPX configurations) + - Docker AI integration + - Gemini CLI usage + - Antigravity Code Editor setup + - Added HR schema and data SQL scripts for PostgreSQL: + - `hr_schema_postgres.sql` - Complete HR schema with tables (regions, countries, locations, departments, jobs, employees, job_history) + - `hr_data_postgres.sql` - Sample data for HR schema + - Schema-aware implementation using `hr` schema namespace + - Foreign key constraints and indexes for performance + - Comprehensive table documentation with comments + - Enhanced README with better documentation organization + - Added resource templates showing schema-aware table access patterns + +### 2025-12-03 +- **feat**: Add prompts capability and list handler to PostgreSQL server + - Updated version to 1.0.1 + - Added `prompts: {}` capability to server configuration + - Imported zod library for schema validation + - Implemented `PromptsListRequestSchema` using zod for request validation + - Added prompts array with 5 PostgreSQL-specific prompt templates: + - `pg-query` - Example query execution + - `pg-explain` - Query execution plan analysis + - `pg-stats` - Table statistics retrieval + - `pg-connect` - Database connection instructions + - `pg-awr` - Performance report generation (requires pg_stat_statements extension) + - Added request handler for `prompts/list` endpoint + - Published package @marcelo-ochoa/server-postgres@1.0.1 to npm registry + - Rebuilt Docker image mochoa/mcp-postgres with updated functionality + +### 2025-12-01 +- **feat**: Bump server version to 1.0.0 + - Updated version to 1.0.0 across package.json, server.json, and server.ts + - Added AWR_example.md with comprehensive performance analysis for production Moodle database + - Added CHANGELOG.md for better change tracking + - Enhanced pg-awr tool with improved reporting capabilities + - Updated README with PostgreSQL AWR in action section + +### 2025-11-27 +- **chore**: Bump patch version in server.json + - Minor version update for server configuration + +- **feat**: Add `ListResourceTemplates` handler to PostgreSQL server + - Enhanced server capabilities with resource template listing + - Updated package dependencies + +- **feat**: Add server.json definitions and update versions for PostgreSQL server + - Added server.json with MCP server metadata and schema + - Updated version to 0.6.5 + - Added mcpName field to package.json + - Configured environment variables (PG_USER, PG_PASSWORD) in server definition + +### 2025-11-26 +- **docs**: Update README to include recent server version bumps, URL simplification, and graceful shutdown + - Updated documentation with latest changes and improvements + +- **chore**: Bump PostgreSQL server version to 0.6.4 + - Updated package versions to reflect recent improvements + - Synchronized package-lock.json with new versions + +- **feat**: Simplify database resource URLs and add graceful server shutdown on stdin close + - Simplified resource URL format from `postgres://dbname/table/schema` to cleaner format + - Added graceful shutdown handling for improved stability + - Enhanced db.ts with better connection management + +### 2025-11-27 +- **feat**: Add server.json definitions and update versions for MySQL and PostgreSQL servers + - Added server.json with MCP server metadata and schema + - Updated version to 0.6.5 + - Added mcpName field to package.json + - Configured environment variables (POSTGRES_USER, POSTGRES_PASSWORD) in server definition + +### 2025-11-26 +- **docs**: Update READMEs to include recent server version bumps, URL simplification, and graceful shutdown + - Updated documentation with latest changes and improvements + +- **chore**: Bump MySQL and PostgreSQL server versions to 0.1.1 and 0.6.4 + - Updated package versions to reflect recent improvements + - Synchronized package-lock.json with new versions + +- **feat**: Simplify database resource URLs and add graceful server shutdown on stdin close + - Simplified resource URL format for better usability + - Added graceful shutdown handling for improved stability + +### 2025-11-25 +- **docs**: Add change logs to Oracle and Postgres READMEs + - Detailed new features such as secure Postgres authentication + - Documented Toon format encoding integration + - Added Antigravity Code Editor integration instructions + +### 2025-11-22 +- **feat**: Implement secure PostgreSQL authentication via environment variables and update tool descriptions + - Added `PG_USER` and `PG_PASSWORD` environment variables for secure credential management + - Updated `pg-connect` tool to accept explicit user/password arguments + - Enhanced tool descriptions for better clarity and documentation + +### 2025-11-20 +- **feat**: Add initial Postgres server implementation, integrate ModelContextProtocol SDK, and update Oracle tools + - Complete refactoring of PostgreSQL MCP server + - Modularized code structure with separate tool handlers + - Implemented `pg-stats`, `pg-connect`, `pg-explain`, and `pg-awr` tools + - Renamed query tool to `pg-query` to avoid naming collisions + - Added Docker image build support for postgres service diff --git a/src/postgres/Demos.md b/src/postgres/Demos.md new file mode 100644 index 0000000000..e0ff6fae3b --- /dev/null +++ b/src/postgres/Demos.md @@ -0,0 +1,242 @@ +# Demos + +Some sample usage scenarios are shown below: + +## This Demo is using the HR Schema + +The HR schema is a sample schema that is not included in the PostgreSQL distribution. It is a simple schema that contains a few tables and some sample data. + +To start a sample docker postgres container, run the following command: + +```sh +% docker run -d --name some-postgres -e POSTGRES_PASSWORD=pg_2025 -p 5432:5432 postgres +``` + +To load the HR schema, run the following command: + +```sh +% cat hr_schema_postgres.sql|docker exec -i some-postgres psql -U postgres -d postgres +``` + +To load the HR data, run the following command: + +```sh +% cat hr_data_postgres.sql|docker exec -i some-postgres psql -U postgres -d postgres +% docker exec -i some-postgres psql -U postgres -d postgres -c "ANALYZE hr.countries; ANALYZE hr.locations; ANALYZE hr.departments;" +``` + +## 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) +* Credentials are passed via environment variables `PG_USER` and `PG_PASSWORD` + +```json +{ + "mcpServers": { + "postgres": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "PG_USER=postgres", + "-e", + "PG_PASSWORD=pg_2025", + "mochoa/mcp-postgres", + "postgresql://host.docker.internal:5432/postgres"] + } + } +} +``` + +### NPX + +```json +{ + "mcpServers": { + "postgres": { + "command": "npx", + "args": [ + "-y", + "@marcelo-ochoa/server-postgres", + "postgresql://localhost:5432/postgres" + ], + "env": { + "PG_USER": "postgres", + "PG_PASSWORD": "pg_2025" + } + } + } +} +``` + +Replace `/postgres` with your database name. + +### Demo Prompts + +Sample prompts using the converted HR schema for PostgreSQL. + +- pg-connect to postgresql://host.docker.internal:5432/postgres using postgres as user and pg_2025 as password +- pg-query SELECT c.country_name, l.city, COUNT(d.department_id) +FROM hr.countries c +JOIN hr.locations l ON c.country_id = l.country_id +JOIN hr.departments d ON l.location_id = d.location_id +WHERE d.department_id IN + (SELECT e.department_id FROM hr.employees e + GROUP BY e.department_id + HAVING COUNT(e.department_id) > 5) +GROUP BY c.country_name, l.city +- pg-explain the execution plan +- visualize above execution plan in text mode +- pg-stats of hr.countries, hr.locations and hr.departments +- based on above table and index stats rewrite above query with a better execution plan +- visualize original and rewritten execution plan +- load resource postgresql://hr/countries/schema +- pg-awr + +## Using Docker AI + +[Ask Gordon](https://docs.docker.com/desktop/features/gordon/) is an AI assistant designed to streamline your Docker workflow by providing contextual assistance tailored to your local environment. Currently in Beta and available in Docker Desktop version 4.38.0 or later, Ask Gordon offers intelligent support for various Docker-related tasks. + +```sh +% cd src/postgres +% docker ai 'pg-stats for table countries' + + • Calling stats ✔️ + + Here are the statistics for the COUNTRIES table: + + ### Table Statistics: + + • Schema: public + • Table Name: countries + • Number of Rows: 25 + • Size: 8192 bytes + • Sequential Scans: 3 + + ### Index Statistics: + + • Index Name: country_c_id_pk + • Size: 16384 bytes + • Scans: 0 + + ### Column Statistics: + + 1. country_id: + + • Type: character(2) + • Nullable: NO + + 2. country_name: + + • Type: character varying(60) + • Nullable: YES + + 3. region_id: + + • Type: integer + • Nullable: YES +``` + +Using this sample gordon-mcp.yml file in a current directory: + +```yml +services: + time: + image: mcp/time + postgres: + image: mochoa/mcp-postgres + command: ["postgresql://host.docker.internal:5432/postgres"] + environment: + - PG_USER=postgres + - PG_PASSWORD=pg_2025 +``` + +## Using Gemini CLI + +[Gemini CLI](https://github.com/google-gemini/gemini-cli/) +is an open-source AI agent that brings the power of Gemini directly +into your terminal. It provides lightweight access to Gemini, giving you the +most direct path from your prompt to our model. + +Using this sample settings.json file at ~/.gemini/ directory: + +```json +{ + "mcpServers": { + "postgres": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "PG_USER=postgres", + "-e", + "PG_PASSWORD=pg_2025", + "mochoa/mcp-postgres", + "postgresql://host.docker.internal:5432/postgres" + ] + } + }, + "security": { + "auth": { + "selectedType": "gemini-api-key" + } + }, + "ui": { + "theme": "ANSI" + }, + "selectedAuthType": "gemini-api-key", + "theme": "Dracula" +} +``` + +### Sample prompts with Gemini CLI + +- connect to postgresql://host.docker.internal:5432/postgres using postgres as user and pg_2025 as password using postgres mcp server +- pg-query SELECT c.country_name, l.city, COUNT(d.department_id) + FROM countries c + JOIN locations l ON c.country_id = l.country_id + JOIN departments d ON l.location_id = d.location_id + WHERE d.department_id IN +   (SELECT e.department_id FROM employees e +    GROUP BY e.department_id +    HAVING COUNT(e.department_id) > 5) + GROUP BY c.country_name, l.city +- pg-explain the execution plan +- visualize above execution plan in text mode +- pg-stats of countries, locations and departments +- based on above table and index stats rewrite above query with a better execution plan +- visualize original and rewritten execution plan + +## Using Antigravity Code Editor + +Put this in `~/.gemini/antigravity/mcp_config.json` + +```json +{ + "mcpServers": { + "postgres": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "PG_USER=postgres", + "-e", + "PG_PASSWORD=pg_2025", + "mochoa/mcp-postgres", + "postgresql://host.docker.internal:5432/postgres" + ] + } + }, + "inputs": [] +} +``` diff --git a/src/postgres/Dockerfile b/src/postgres/Dockerfile new file mode 100644 index 0000000000..d3740777ae --- /dev/null +++ b/src/postgres/Dockerfile @@ -0,0 +1,29 @@ +FROM node:slim AS builder + +COPY src/postgres /app +COPY tsconfig.json /tsconfig.json + +WORKDIR /app + +RUN --mount=type=cache,target=/root/.npm npm install + +RUN npm run build + +RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev + +FROM dhi.io/node:26-alpine-sfw-ent-dev AS release + +# Update and upgrade to fix OS-level vulnerabilities +RUN apk update && apk upgrade --no-cache + +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 /usr/bin/npm ci --ignore-scripts --omit-dev + +ENTRYPOINT ["node", "dist/index.js"] \ No newline at end of file diff --git a/src/postgres/LICENSE b/src/postgres/LICENSE new file mode 100644 index 0000000000..4a93985763 --- /dev/null +++ b/src/postgres/LICENSE @@ -0,0 +1,216 @@ +The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0. + +Contributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License. + +No rights beyond those granted by the applicable original license are conveyed for such contributions. + +--- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright + owner or by an individual or Legal Entity authorized to submit on behalf + of the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +--- + +MIT License + +Copyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +Creative Commons Attribution 4.0 International (CC-BY-4.0) + +Documentation in this project (excluding specifications) is licensed under +CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for +the full license text. diff --git a/src/postgres/README.md b/src/postgres/README.md new file mode 100644 index 0000000000..d97ed6043b --- /dev/null +++ b/src/postgres/README.md @@ -0,0 +1,235 @@ +# PostgreSQL + +A Model Context Protocol server that provides read-only access to PostgreSQL databases. This server enables LLMs to inspect database schemas and execute read-only queries. + +## Components + +### Tools + +- **pg-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 + +- **pg-stats** + - Get statistics for a specific table + - Input: `name` (string): The name of the table to get statistics for + +- **pg-explain** + - Explain Plan for a given SQL query + - Input: `sql` (string): The SQL query to explain + +- **pg-connect** + - Connect to a PostgreSQL database + - Inputs: + - `connectionString` (string): The PostgreSQL connection string without credentials (e.g. postgresql://host:port/dbname or host:port/dbname) + - `user` (string): The PostgreSQL username + - `password` (string): The PostgreSQL password + +Example: + pg-connect host.docker.internal:5432/postgres postgres pg_2025 + +- **pg-awr** + - Generate a PostgreSQL performance report similar to Oracle AWR. Includes database statistics, top queries (requires pg_stat_statements extension), table/index statistics, connection info, and optimization recommendations. + +### Resources + +The server provides schema information for each table in the database: + +- **Table Schemas** (`postgres:///
/schema`) + - JSON schema information for each table + - Includes column names and data types + - Automatically discovered from database metadata + +## Change Log + +See [Change Log](https://github.com/marcelo-ochoa/servers/blob/main/src/postgres/CHANGELOG.md) for the history of changes. + +## Configuration + +The PostgreSQL server uses environment variables or the `pg-connect` tool for secure credential management: + +- **`PG_USER`**: PostgreSQL username (optional if using `pg-connect`) +- **`PG_PASSWORD`**: PostgreSQL password (optional if using `pg-connect`) + +### Connection String + +The connection string should contain only the host, port, and database information (without embedded credentials). Providing it as a command-line argument is **optional**. If omitted at startup, you must use the `pg-connect` tool to establish a connection before using other functionality. + +**Supported connection string formats:** +- `postgresql://host:port/dbname` +- `host:port/dbname` + +To enable encryption (SSL), append `?sslmode=require` to the connection string or set the `PG_SSL` environment variable to `true`. + +### 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 PostgreSQL server is running on the host network (e.g., localhost) +* Credentials are passed via environment variables `PG_USER` and `PG_PASSWORD` + +```json +{ + "mcpServers": { + "postgres": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", "PG_USER=myuser", + "-e", "PG_PASSWORD=mypassword", + "mochoa/mcp-postgres" + ] + } + } +} +``` + +Note: You can still provide the connection string as a final argument if you want to connect automatically on startup: `"args": [..., "mochoa/mcp-postgres", "postgresql://host.docker.internal:5432/mydb"]`. + +### NPX + +```json +{ + "mcpServers": { + "postgres": { + "command": "npx", + "args": [ + "-y", + "@marcelo-ochoa/server-postgres", + "postgresql://localhost:5432/mydb" + ], + "env": { + "PG_USER": "myuser", + "PG_PASSWORD": "mypassword" + } + } + } +} +``` + +Replace `/mydb` with your database name. + +**Note**: Replace the following placeholders with your actual values: +- `myuser` and `mypassword` with your PostgreSQL credentials +- `localhost:5432` with your PostgreSQL server host and port +- `mydb` with your database name + +### Usage with VS Code + +For quick installation, use one of the one-click install buttons below... + +[![Install with NPX in VS Code](https://img.shields.io/badge/VS_Code-NPM-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=postgres&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22pg_url%22%2C%22description%22%3A%22PostgreSQL%20URL%20(e.g.%20postgresql%3A%2F%2Fuser%3Apass%40localhost%3A5432%2Fmydb)%22%7D%5D&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-postgres%22%2C%22%24%7Binput%3Apg_url%7D%22%5D%7D) [![Install with NPX in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-NPM-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=postgres&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22pg_url%22%2C%22description%22%3A%22PostgreSQL%20URL%20(e.g.%20postgresql%3A%2F%2Fuser%3Apass%40localhost%3A5432%2Fmydb)%22%7D%5D&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-postgres%22%2C%22%24%7Binput%3Apg_url%7D%22%5D%7D&quality=insiders) + +[![Install with Docker in VS Code](https://img.shields.io/badge/VS_Code-Docker-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=postgres&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22pg_url%22%2C%22description%22%3A%22PostgreSQL%20URL%20(e.g.%20postgresql%3A%2F%2Fuser%3Apass%40host.docker.internal%3A5432%2Fmydb)%22%7D%5D&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22mcp%2Fpostgres%22%2C%22%24%7Binput%3Apg_url%7D%22%5D%7D) [![Install with Docker in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Docker-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=postgres&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22pg_url%22%2C%22description%22%3A%22PostgreSQL%20URL%20(e.g.%20postgresql%3A%2F%2Fuser%3Apass%40host.docker.internal%3A5432%2Fmydb)%22%7D%5D&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22mcp%2Fpostgres%22%2C%22%24%7Binput%3Apg_url%7D%22%5D%7D&quality=insiders) + +For manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`. + +Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others. + +> Note that the `mcp` key is not needed in the `.vscode/mcp.json` file. + +### Docker + +**Note**: When using Docker and connecting to a PostgreSQL server on your host machine, use `host.docker.internal` instead of `localhost` in the connection URL. + +```json +{ + "mcp": { + "inputs": [ + { + "type": "promptString", + "id": "pg_user", + "description": "PostgreSQL username" + }, + { + "type": "promptString", + "id": "pg_password", + "description": "PostgreSQL password", + "password": true + } + ], + "servers": { + "postgres": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", "PG_USER=${input:pg_user}", + "-e", "PG_PASSWORD=${input:pg_password}", + "mochoa/mcp-postgres", + "postgresql://localhost:5432/mydb" + ] + } + } + } +} +``` + +Note: You can add an input for `pg_url` and append it to `args` if you want to connect on startup. + +### NPX + +```json +{ + "mcp": { + "inputs": [ + { + "type": "promptString", + "id": "pg_user", + "description": "PostgreSQL username" + }, + { + "type": "promptString", + "id": "pg_password", + "description": "PostgreSQL password", + "password": true + } + ], + "servers": { + "postgres": { + "command": "npx", + "args": [ + "-y", + "@marcelo-ochoa/server-postgres", + "postgresql://localhost:5432/mydb" + ], + "env": { + "PG_USER": "${input:pg_user}", + "PG_PASSWORD": "${input:pg_password}" + } + } + } + } +} +``` + +## PostgreSQL AWR in action + +See [PostgreSQL AWR in action](https://github.com/marcelo-ochoa/servers/blob/main/src/postgres/AWR_example.md) for an example of a performance report generated by the `pg-awr` tool for a production Moodle database, highlighting critical performance issues and optimization opportunities. + +## Demos + +See [Demos](https://github.com/marcelo-ochoa/servers/blob/main/src/postgres/Demos.md) for usage examples with Claude Desktop, Docker AI, Gemini CLI, and Antigravity Code Editor. + +## Building + +Docker: + +```sh +docker build -t mochoa/mcp-postgres -f src/postgres/Dockerfile . +``` + +## Sources + +As usual the code of this extension is at [GitHub](https://github.com/marcelo-ochoa/servers), feel free to suggest changes and make contributions, note that I am a beginner developer of React and TypeScript so contributions to make this UI better are welcome. + +## 📜 License + +This project is licensed under the Apache License, Version 2.0 for new contributions, with existing code under MIT - see the [LICENSE](https://github.com/marcelo-ochoa/servers/blob/main/src/postgres/LICENSE) file for details. + diff --git a/src/postgres/db.ts b/src/postgres/db.ts new file mode 100644 index 0000000000..293903cfff --- /dev/null +++ b/src/postgres/db.ts @@ -0,0 +1,107 @@ +import pg from "pg"; + +let pool: pg.Pool | undefined = undefined; + +let resourceBaseUrl: URL | undefined = undefined; + +export async function initializePool(connectionString: string) { + const dbUser = process.env.PG_USER; + const dbPassword = process.env.PG_PASSWORD; + + if (!dbUser || !dbPassword) { + console.error( + "Error: Environment variables PG_USER and PG_PASSWORD must be set.", + ); + process.exit(1); + } + + // Parse the connection string to extract host, port, and database + // Expected format: postgresql://host:port/dbname or host:port/dbname + let host: string; + let port: number; + let database: string; + let useSsl = process.env.PG_SSL === "true"; + + try { + // Try parsing as URL first + if (connectionString.startsWith('postgresql://') || connectionString.startsWith('postgres://')) { + const url = new URL(connectionString); + host = url.hostname; + port = url.port ? parseInt(url.port) : 5432; + database = url.pathname.slice(1); // Remove leading '/' + const sslmode = url.searchParams.get('sslmode'); + if (sslmode && sslmode !== 'disable') { + useSsl = true; + } + } else { + // Split connection string from potential query parameters + const [baseConn, queryStr] = connectionString.split('?'); + // Parse format: host:port/dbname + const match = baseConn.match(/^([^:]+):(\d+)\/(.+)$/); + if (!match) { + throw new Error("Invalid connection string format. Expected: host:port/dbname or postgresql://host:port/dbname"); + } + host = match[1]; + port = parseInt(match[2]); + database = match[3]; + if (queryStr && queryStr.includes('sslmode=') && !queryStr.includes('sslmode=disable')) { + useSsl = true; + } + } + } catch (err) { + console.error("Error parsing connection string:", err); + process.exit(1); + } + + pool = new pg.Pool({ + user: dbUser, + password: dbPassword, + host, + port, + database, + ssl: useSsl ? { rejectUnauthorized: false } : undefined, + }); + + // Test connection + const client = await pool.connect(); + client.release(); + + // Build resource base URL without credentials + const url = new URL(`postgresql://${database}`); + resourceBaseUrl = url; +} + +export function isPoolInitialized(): boolean { + return pool !== undefined; +} + +export function getPool(): pg.Pool { + if (!pool) { + throw new Error("Postgres connection pool not initialized. Use pg-connect tool first."); + } + return pool; +} + +export function getResourceBaseUrl(): URL { + if (!resourceBaseUrl) { + throw new Error("Resource Base URL not initialized. Use pg-connect tool first."); + } + return resourceBaseUrl; +} + +export async function withConnection(callback: (client: pg.PoolClient) => Promise): Promise { + const pool = getPool(); + const client = await pool.connect(); + try { + return await callback(client); + } finally { + client.release(); + } +} + +export async function closePool() { + if (pool) { + await pool.end(); + pool = undefined; + } +} diff --git a/src/postgres/handlers.ts b/src/postgres/handlers.ts new file mode 100644 index 0000000000..139bc30624 --- /dev/null +++ b/src/postgres/handlers.ts @@ -0,0 +1,25 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { queryHandler } from "./tools/query.js"; +import { statsHandler } from "./tools/stats.js"; +import { connectHandler } from "./tools/connect.js"; +import { explainHandler } from "./tools/explain.js"; +import { awrHandler } from "./tools/awr.js"; + +export { listResourcesHandler, readResourceHandler } from "./resources.js"; + +const toolHandlers: Record Promise> = { + "pg-query": queryHandler, + "pg-stats": statsHandler, + "pg-explain": explainHandler, + "pg-connect": connectHandler, + "pg-awr": awrHandler, +}; + +export const callToolHandler = async (request: CallToolRequest) => { + const handler = toolHandlers[request.params.name]; + if (handler) { + return handler(request); + } + throw new Error(`Unknown tool: ${request.params.name}`); +}; + diff --git a/src/postgres/hr_data_postgres.sql b/src/postgres/hr_data_postgres.sql new file mode 100644 index 0000000000..79afe20e03 --- /dev/null +++ b/src/postgres/hr_data_postgres.sql @@ -0,0 +1,303 @@ +-- ============================================================================ +-- PostgreSQL Data Insert Script for HR Schema +-- Generated from Oracle HR Schema Data +-- Date: 2025-12-11 +-- Total Records: 218 rows across 7 tables +-- ============================================================================ + +-- Disable triggers temporarily for faster inserts +SET search_path TO hr; +SET session_replication_role = 'replica'; + +-- ============================================================================ +-- Table: REGIONS (5 rows) +-- ============================================================================ +INSERT INTO regions (region_id, region_name) VALUES +(10, 'Europe'), +(20, 'Americas'), +(30, 'Asia'), +(40, 'Oceania'), +(50, 'Africa'); + +-- ============================================================================ +-- Table: COUNTRIES (25 rows) +-- ============================================================================ +INSERT INTO countries (country_id, country_name, region_id) VALUES +('AR', 'Argentina', 20), +('AU', 'Australia', 40), +('BE', 'Belgium', 10), +('BR', 'Brazil', 20), +('CA', 'Canada', 20), +('CH', 'Switzerland', 10), +('CN', 'China', 30), +('DE', 'Germany', 10), +('DK', 'Denmark', 10), +('EG', 'Egypt', 50), +('FR', 'France', 10), +('GB', 'United Kingdom of Great Britain and Northern Ireland', 10), +('IL', 'Israel', 30), +('IN', 'India', 30), +('IT', 'Italy', 10), +('JP', 'Japan', 30), +('KW', 'Kuwait', 30), +('ML', 'Malaysia', 30), +('MX', 'Mexico', 20), +('NG', 'Nigeria', 50), +('NL', 'Netherlands', 10), +('SG', 'Singapore', 30), +('US', 'United States of America', 20), +('ZM', 'Zambia', 50), +('ZW', 'Zimbabwe', 50); + +-- ============================================================================ +-- Table: LOCATIONS (23 rows) +-- ============================================================================ +INSERT INTO locations (location_id, street_address, postal_code, city, state_province, country_id) VALUES +(1000, '1297 Via Cola di Rie', '00989', 'Roma', NULL, 'IT'), +(1100, '93091 Calle della Testa', '10934', 'Venice', NULL, 'IT'), +(1200, '2017 Shinjuku-ku', '1689', 'Tokyo', 'Tokyo Prefecture', 'JP'), +(1300, '9450 Kamiya-cho', '6823', 'Hiroshima', NULL, 'JP'), +(1400, '2014 Jabberwocky Rd', '26192', 'Southlake', 'Texas', 'US'), +(1500, '2011 Interiors Blvd', '99236', 'South San Francisco', 'California', 'US'), +(1600, '2007 Zagora St', '50090', 'South Brunswick', 'New Jersey', 'US'), +(1700, '2004 Charade Rd', '98199', 'Seattle', 'Washington', 'US'), +(1800, '147 Spadina Ave', 'M5V 2L7', 'Toronto', 'Ontario', 'CA'), +(1900, '6092 Boxwood St', 'YSW 9T2', 'Whitehorse', 'Yukon', 'CA'), +(2000, '40-5-12 Laogianggen', '190518', 'Beijing', NULL, 'CN'), +(2100, '1298 Vileparle (E)', '490231', 'Bombay', 'Maharashtra', 'IN'), +(2200, '12-98 Victoria Street', '2901', 'Sydney', 'New South Wales', 'AU'), +(2300, '198 Clementi North', '540198', 'Singapore', NULL, 'SG'), +(2400, '8204 Arthur St', NULL, 'London', NULL, 'GB'), +(2500, 'Magdalen Centre, The Oxford Science Park', 'OX9 9ZB', 'Oxford', 'Oxford', 'GB'), +(2600, '9702 Chester Road', '09629850293', 'Stretford', 'Manchester', 'GB'), +(2700, 'Schwanthalerstr. 7031', '80925', 'Munich', 'Bavaria', 'DE'), +(2800, 'Rua Frei Caneca 1360 ', '01307-002', 'Sao Paulo', 'Sao Paulo', 'BR'), +(2900, '20 Rue des Corps-Saints', '1730', 'Geneva', 'Geneve', 'CH'), +(3000, 'Murtenstrasse 921', '3095', 'Bern', 'BE', 'CH'), +(3100, 'Pieter Breughelstraat 837', '3029SK', 'Utrecht', 'Utrecht', 'NL'), +(3200, 'Mariano Escobedo 9991', '11932', 'Mexico City', 'Distrito Federal', 'MX'); + +-- ============================================================================ +-- Table: JOBS (19 rows) +-- ============================================================================ +INSERT INTO jobs (job_id, job_title, min_salary, max_salary) VALUES +('AC_ACCOUNT', 'Public Accountant', 4200, 9000), +('AC_MGR', 'Accounting Manager', 8200, 16000), +('AD_ASST', 'Administration Assistant', 3000, 6000), +('AD_PRES', 'President', 20080, 40000), +('AD_VP', 'Administration Vice President', 15000, 30000), +('FI_ACCOUNT', 'Accountant', 4200, 9000), +('FI_MGR', 'Finance Manager', 8200, 16000), +('HR_REP', 'Human Resources Representative', 4000, 9000), +('IT_PROG', 'Programmer', 4000, 10000), +('MK_MAN', 'Marketing Manager', 9000, 15000), +('MK_REP', 'Marketing Representative', 4000, 9000), +('PR_REP', 'Public Relations Representative', 4500, 10500), +('PU_CLERK', 'Purchasing Clerk', 2500, 5500), +('PU_MAN', 'Purchasing Manager', 8000, 15000), +('SA_MAN', 'Sales Manager', 10000, 20080), +('SA_REP', 'Sales Representative', 6000, 12008), +('SH_CLERK', 'Shipping Clerk', 2500, 5500), +('ST_CLERK', 'Stock Clerk', 2008, 5000), +('ST_MAN', 'Stock Manager', 5500, 8500); + +-- ============================================================================ +-- Table: DEPARTMENTS (27 rows) +-- Note: manager_id will be updated after employees are inserted +-- ============================================================================ +INSERT INTO departments (department_id, department_name, manager_id, location_id) VALUES +(10, 'Administration', NULL, 1700), +(20, 'Marketing', NULL, 1800), +(30, 'Purchasing', NULL, 1700), +(40, 'Human Resources', NULL, 2400), +(50, 'Shipping', NULL, 1500), +(60, 'IT', NULL, 1400), +(70, 'Public Relations', NULL, 2700), +(80, 'Sales', NULL, 2500), +(90, 'Executive', NULL, 1700), +(100, 'Finance', NULL, 1700), +(110, 'Accounting', NULL, 1700), +(120, 'Treasury', NULL, 1700), +(130, 'Corporate Tax', NULL, 1700), +(140, 'Control And Credit', NULL, 1700), +(150, 'Shareholder Services', NULL, 1700), +(160, 'Benefits', NULL, 1700), +(170, 'Manufacturing', NULL, 1700), +(180, 'Construction', NULL, 1700), +(190, 'Contracting', NULL, 1700), +(200, 'Operations', NULL, 1700), +(210, 'IT Support', NULL, 1700), +(220, 'NOC', NULL, 1700), +(230, 'IT Helpdesk', NULL, 1700), +(240, 'Government Sales', NULL, 1700), +(250, 'Retail Sales', NULL, 1700), +(260, 'Recruiting', NULL, 1700), +(270, 'Payroll', NULL, 1700); + +-- ============================================================================ +-- Table: EMPLOYEES (107 rows) +-- ============================================================================ +INSERT INTO employees (employee_id, first_name, last_name, email, phone_number, hire_date, job_id, salary, commission_pct, manager_id, department_id) VALUES +(100, 'Steven', 'King', 'SKING', '1.515.555.0100', '2013-06-17', 'AD_PRES', 24000.00, NULL, NULL, 90), +(101, 'Neena', 'Yang', 'NYANG', '1.515.555.0101', '2015-09-21', 'AD_VP', 17000.00, NULL, 100, 90), +(102, 'Lex', 'Garcia', 'LGARCIA', '1.515.555.0102', '2011-01-13', 'AD_VP', 17000.00, NULL, 100, 90), +(103, 'Alexander', 'James', 'AJAMES', '1.590.555.0103', '2016-01-03', 'IT_PROG', 9000.00, NULL, 102, 60), +(104, 'Bruce', 'Miller', 'BMILLER', '1.590.555.0104', '2017-05-21', 'IT_PROG', 6000.00, NULL, 103, 60), +(105, 'David', 'Williams', 'DWILLIAMS', '1.590.555.0105', '2015-06-25', 'IT_PROG', 4800.00, NULL, 103, 60), +(106, 'Valli', 'Jackson', 'VJACKSON', '1.590.555.0106', '2016-02-05', 'IT_PROG', 4800.00, NULL, 103, 60), +(107, 'Diana', 'Nguyen', 'DNGUYEN', '1.590.555.0107', '2017-02-07', 'IT_PROG', 4200.00, NULL, 103, 60), +(108, 'Nancy', 'Gruenberg', 'NGRUENBE', '1.515.555.0108', '2012-08-17', 'FI_MGR', 12008.00, NULL, 101, 100), +(109, 'Daniel', 'Faviet', 'DFAVIET', '1.515.555.0109', '2012-08-16', 'FI_ACCOUNT', 9000.00, NULL, 108, 100), +(110, 'John', 'Chen', 'JCHEN', '1.515.555.0110', '2015-09-28', 'FI_ACCOUNT', 8200.00, NULL, 108, 100), +(111, 'Ismael', 'Sciarra', 'ISCIARRA', '1.515.555.0111', '2015-09-30', 'FI_ACCOUNT', 7700.00, NULL, 108, 100), +(112, 'Jose Manuel', 'Urman', 'JMURMAN', '1.515.555.0112', '2016-03-07', 'FI_ACCOUNT', 7800.00, NULL, 108, 100), +(113, 'Luis', 'Popp', 'LPOPP', '1.515.555.0113', '2017-12-07', 'FI_ACCOUNT', 6900.00, NULL, 108, 100), +(114, 'Den', 'Li', 'DLI', '1.515.555.0114', '2012-12-07', 'PU_MAN', 11000.00, NULL, 100, 30), +(115, 'Alexander', 'Khoo', 'AKHOO', '1.515.555.0115', '2013-05-18', 'PU_CLERK', 3100.00, NULL, 114, 30), +(116, 'Shelli', 'Baida', 'SBAIDA', '1.515.555.0116', '2015-12-24', 'PU_CLERK', 2900.00, NULL, 114, 30), +(117, 'Sigal', 'Tobias', 'STOBIAS', '1.515.555.0117', '2015-07-24', 'PU_CLERK', 2800.00, NULL, 114, 30), +(118, 'Guy', 'Himuro', 'GHIMURO', '1.515.555.0118', '2016-11-15', 'PU_CLERK', 2600.00, NULL, 114, 30), +(119, 'Karen', 'Colmenares', 'KCOLMENA', '1.515.555.0119', '2017-08-10', 'PU_CLERK', 2500.00, NULL, 114, 30), +(120, 'Matthew', 'Weiss', 'MWEISS', '1.650.555.0120', '2014-07-18', 'ST_MAN', 8000.00, NULL, 100, 50), +(121, 'Adam', 'Fripp', 'AFRIPP', '1.650.555.0121', '2015-04-10', 'ST_MAN', 8200.00, NULL, 100, 50), +(122, 'Payam', 'Kaufling', 'PKAUFLIN', '1.650.555.0122', '2013-05-01', 'ST_MAN', 7900.00, NULL, 100, 50), +(123, 'Shanta', 'Vollman', 'SVOLLMAN', '1.650.555.0123', '2015-10-10', 'ST_MAN', 6500.00, NULL, 100, 50), +(124, 'Kevin', 'Mourgos', 'KMOURGOS', '1.650.555.0124', '2017-11-16', 'ST_MAN', 5800.00, NULL, 100, 50), +(125, 'Julia', 'Nayer', 'JNAYER', '1.650.555.0125', '2015-03-16', 'ST_CLERK', 3200.00, NULL, 120, 50), +(126, 'Irene', 'Mikkilineni', 'IMIKKILI', '1.650.555.0126', '2016-09-28', 'ST_CLERK', 2700.00, NULL, 120, 50), +(127, 'James', 'Landry', 'JLANDRY', '1.650.555.0127', '2017-01-14', 'ST_CLERK', 2400.00, NULL, 120, 50), +(128, 'Steven', 'Markle', 'SMARKLE', '1.650.555.0128', '2018-03-08', 'ST_CLERK', 2200.00, NULL, 120, 50), +(129, 'Laura', 'Bissot', 'LBISSOT', '1.650.555.0129', '2015-08-20', 'ST_CLERK', 3300.00, NULL, 121, 50), +(130, 'Mozhe', 'Atkinson', 'MATKINSO', '1.650.555.0130', '2015-10-30', 'ST_CLERK', 2800.00, NULL, 121, 50), +(131, 'James', 'Marlow', 'JAMRLOW', '1.650.555.0131', '2015-02-16', 'ST_CLERK', 2500.00, NULL, 121, 50), +(132, 'TJ', 'Olson', 'TJOLSON', '1.650.555.0132', '2017-04-10', 'ST_CLERK', 2100.00, NULL, 121, 50), +(133, 'Jason', 'Mallin', 'JMALLIN', '1.650.555.0133', '2014-06-14', 'ST_CLERK', 3300.00, NULL, 122, 50), +(134, 'Michael', 'Rogers', 'MROGERS', '1.650.555.0134', '2016-08-26', 'ST_CLERK', 2900.00, NULL, 122, 50), +(135, 'Ki', 'Gee', 'KGEE', '1.650.555.0135', '2017-12-12', 'ST_CLERK', 2400.00, NULL, 122, 50), +(136, 'Hazel', 'Philtanker', 'HPHILTAN', '1.650.555.0136', '2018-02-06', 'ST_CLERK', 2200.00, NULL, 122, 50), +(137, 'Renske', 'Ladwig', 'RLADWIG', '1.650.555.0137', '2013-07-14', 'ST_CLERK', 3600.00, NULL, 123, 50), +(138, 'Stephen', 'Stiles', 'SSTILES', '1.650.555.0138', '2015-10-26', 'ST_CLERK', 3200.00, NULL, 123, 50), +(139, 'John', 'Seo', 'JSEO', '1.650.555.0139', '2016-02-12', 'ST_CLERK', 2700.00, NULL, 123, 50), +(140, 'Joshua', 'Patel', 'JPATEL', '1.650.555.0140', '2016-04-06', 'ST_CLERK', 2500.00, NULL, 123, 50), +(141, 'Trenna', 'Rajs', 'TRAJS', '1.650.555.0141', '2013-10-17', 'ST_CLERK', 3500.00, NULL, 124, 50), +(142, 'Curtis', 'Davies', 'CDAVIES', '1.650.555.0142', '2015-01-29', 'ST_CLERK', 3100.00, NULL, 124, 50), +(143, 'Randall', 'Matos', 'RMATOS', '1.650.555.0143', '2016-03-15', 'ST_CLERK', 2600.00, NULL, 124, 50), +(144, 'Peter', 'Vargas', 'PVARGAS', '1.650.555.0144', '2016-07-09', 'ST_CLERK', 2500.00, NULL, 124, 50), +(145, 'John', 'Russell', 'JRUSSEL', '1.011.555.0145', '2014-10-01', 'SA_MAN', 14000.00, 0.40, 100, 80), +(146, 'Karen', 'Partners', 'KPARTNER', '1.011.555.0146', '2015-01-05', 'SA_MAN', 13500.00, 0.30, 100, 80), +(147, 'Alberto', 'Errazuriz', 'AERRAZUR', '1.011.555.0147', '2015-03-10', 'SA_MAN', 12000.00, 0.30, 100, 80), +(148, 'Gerald', 'Cambrault', 'GCAMBRAU', '1.011.555.0148', '2017-10-15', 'SA_MAN', 11000.00, 0.30, 100, 80), +(149, 'Eleni', 'Zlotkey', 'EZLOTKEY', '1.011.555.0149', '2018-01-29', 'SA_MAN', 10500.00, 0.20, 100, 80), +(150, 'Peter', 'Tucker', 'PTUCKER', '1.011.555.0145', '2015-01-30', 'SA_REP', 10000.00, 0.30, 145, 80), +(151, 'David', 'Bernstein', 'DBERNSTE', '1.011.555.0146', '2015-03-24', 'SA_REP', 9500.00, 0.25, 145, 80), +(152, 'Peter', 'Hall', 'PHALL', '1.011.555.0147', '2015-08-20', 'SA_REP', 9000.00, 0.25, 145, 80), +(153, 'Christopher', 'Olsen', 'COLSEN', '1.011.555.0148', '2016-03-30', 'SA_REP', 8000.00, 0.20, 145, 80), +(154, 'Nanette', 'Cambrault', 'NCAMBRAU', '1.011.555.0149', '2016-12-09', 'SA_REP', 7500.00, 0.20, 145, 80), +(155, 'Oliver', 'Tuvault', 'OTUVAULT', '1.011.555.0150', '2017-11-23', 'SA_REP', 7000.00, 0.15, 145, 80), +(156, 'Janette', 'Smith', 'JSMITH', '1.011.555.0146', '2016-02-10', 'SA_REP', 10000.00, 0.35, 146, 80), +(157, 'Patrick', 'Sully', 'PSULLY', '1.011.555.0146', '2016-03-04', 'SA_REP', 9500.00, 0.35, 146, 80), +(158, 'Allan', 'McEwen', 'AMCEWEN', '1.011.555.0147', '2016-08-01', 'SA_REP', 9000.00, 0.35, 146, 80), +(159, 'Lindsey', 'Johnson', 'LJOHNSON', '1.011.555.0148', '2017-03-10', 'SA_REP', 8000.00, 0.30, 146, 80), +(160, 'Louise', 'Doran', 'LDORAN', '1.011.555.0149', '2017-12-15', 'SA_REP', 7500.00, 0.30, 146, 80), +(161, 'Sarath', 'Sewall', 'SSEWALL', '1.011.555.0150', '2016-11-03', 'SA_REP', 7000.00, 0.25, 146, 80), +(162, 'Clara', 'Vishney', 'CVISHNEY', '1.011.555.0147', '2015-11-11', 'SA_REP', 10500.00, 0.25, 147, 80), +(163, 'Danielle', 'Greene', 'DGREENE', '1.011.555.0148', '2017-03-19', 'SA_REP', 9500.00, 0.15, 147, 80), +(164, 'Mattea', 'Marvins', 'MMARVINS', '1.011.555.0149', '2018-01-24', 'SA_REP', 7200.00, 0.10, 147, 80), +(165, 'David', 'Lee', 'DLEE', '1.011.555.0150', '2018-02-23', 'SA_REP', 6800.00, 0.10, 147, 80), +(166, 'Sundar', 'Ande', 'SANDE', '1.011.555.0151', '2018-03-24', 'SA_REP', 6400.00, 0.10, 147, 80), +(167, 'Amit', 'Banda', 'ABANDA', '1.011.555.0152', '2018-04-21', 'SA_REP', 6200.00, 0.10, 147, 80), +(168, 'Lisa', 'Ozer', 'LOZER', '1.011.555.0148', '2015-03-11', 'SA_REP', 11500.00, 0.25, 148, 80), +(169, 'Harrison', 'Bloom', 'HBLOOM', '1.011.555.0149', '2016-03-23', 'SA_REP', 10000.00, 0.20, 148, 80), +(170, 'Tayler', 'Fox', 'TFOX', '1.011.555.0150', '2016-01-24', 'SA_REP', 9600.00, 0.20, 148, 80), +(171, 'William', 'Smith', 'WSMITH', '1.011.555.0151', '2017-02-23', 'SA_REP', 7400.00, 0.15, 148, 80), +(172, 'Elizabeth', 'Bates', 'EBATES', '1.011.555.0152', '2017-03-24', 'SA_REP', 7300.00, 0.15, 148, 80), +(173, 'Sundita', 'Kumar', 'SKUMAR', '1.011.555.0153', '2018-04-21', 'SA_REP', 6100.00, 0.10, 148, 80), +(174, 'Ellen', 'Abel', 'EABEL', '1.011.555.0149', '2014-05-11', 'SA_REP', 11000.00, 0.30, 149, 80), +(175, 'Alyssa', 'Hutton', 'AHUTTON', '1.011.555.0150', '2015-03-19', 'SA_REP', 8800.00, 0.25, 149, 80), +(176, 'Jonathon', 'Taylor', 'JTAYLOR', '1.011.555.0151', '2016-03-24', 'SA_REP', 8600.00, 0.20, 149, 80), +(177, 'Jack', 'Livingston', 'JLIVINGS', '1.011.555.0152', '2016-04-23', 'SA_REP', 8400.00, 0.20, 149, 80), +(178, 'Kimberely', 'Grant', 'KGRANT', '1.011.555.0153', '2017-05-24', 'SA_REP', 7000.00, 0.15, 149, NULL), +(179, 'Charles', 'Johnson', 'CJOHNSON', '1.011.555.0154', '2018-01-04', 'SA_REP', 6200.00, 0.10, 149, 80), +(180, 'Winston', 'Taylor', 'WTAYLOR', '1.650.555.0145', '2016-01-24', 'SH_CLERK', 3200.00, NULL, 120, 50), +(181, 'Jean', 'Fleaur', 'JFLEAUR', '1.650.555.0146', '2016-02-23', 'SH_CLERK', 3100.00, NULL, 120, 50), +(182, 'Martha', 'Sullivan', 'MSULLIVA', '1.650.555.0147', '2017-06-21', 'SH_CLERK', 2500.00, NULL, 120, 50), +(183, 'Girard', 'Geoni', 'GGEONI', '1.650.555.0148', '2018-02-03', 'SH_CLERK', 2800.00, NULL, 120, 50), +(184, 'Nandita', 'Sarchand', 'NSARCHAN', '1.650.555.0149', '2014-01-27', 'SH_CLERK', 4200.00, NULL, 121, 50), +(185, 'Alexis', 'Bull', 'ABULL', '1.650.555.0150', '2015-02-20', 'SH_CLERK', 4100.00, NULL, 121, 50), +(186, 'Julia', 'Dellinger', 'JDELLING', '1.650.555.0151', '2016-06-24', 'SH_CLERK', 3400.00, NULL, 121, 50), +(187, 'Anthony', 'Cabrio', 'ACABRIO', '1.650.555.0152', '2017-02-07', 'SH_CLERK', 3000.00, NULL, 121, 50), +(188, 'Kelly', 'Chung', 'KCHUNG', '1.650.555.0153', '2015-06-14', 'SH_CLERK', 3800.00, NULL, 122, 50), +(189, 'Jennifer', 'Dilly', 'JDILLY', '1.650.555.0154', '2015-08-13', 'SH_CLERK', 3600.00, NULL, 122, 50), +(190, 'Timothy', 'Venzl', 'TVENZL', '1.650.555.0155', '2016-07-11', 'SH_CLERK', 2900.00, NULL, 122, 50), +(191, 'Randall', 'Perkins', 'RPERKINS', '1.650.555.0156', '2017-12-19', 'SH_CLERK', 2500.00, NULL, 122, 50), +(192, 'Sarah', 'Bell', 'SBELL', '1.650.555.0157', '2014-02-04', 'SH_CLERK', 4000.00, NULL, 123, 50), +(193, 'Britney', 'Everett', 'BEVERETT', '1.650.555.0158', '2015-03-03', 'SH_CLERK', 3900.00, NULL, 123, 50), +(194, 'Samuel', 'McLeod', 'SMCLEOD', '1.650.555.0159', '2016-07-01', 'SH_CLERK', 3200.00, NULL, 123, 50), +(195, 'Vance', 'Jones', 'VJONES', '1.650.555.0160', '2017-03-17', 'SH_CLERK', 2800.00, NULL, 123, 50), +(196, 'Alana', 'Walsh', 'AWALSH', '1.650.555.0161', '2016-04-24', 'SH_CLERK', 3100.00, NULL, 124, 50), +(197, 'Kevin', 'Feeney', 'KFEENEY', '1.650.555.0162', '2016-05-23', 'SH_CLERK', 3000.00, NULL, 124, 50), +(198, 'Donald', 'OConnell', 'DOCONNEL', '1.650.555.0163', '2017-06-21', 'SH_CLERK', 2600.00, NULL, 124, 50), +(199, 'Douglas', 'Grant', 'DGRANT', '1.650.555.0164', '2018-01-13', 'SH_CLERK', 2600.00, NULL, 124, 50), +(200, 'Jennifer', 'Whalen', 'JWHALEN', '1.515.555.0165', '2013-09-17', 'AD_ASST', 4400.00, NULL, 101, 10), +(201, 'Michael', 'Martinez', 'MMARTINE', '1.515.555.0166', '2014-02-17', 'MK_MAN', 13000.00, NULL, 100, 20), +(202, 'Pat', 'Davis', 'PDAVIS', '1.603.555.0167', '2015-08-17', 'MK_REP', 6000.00, NULL, 201, 20), +(203, 'Susan', 'Jacobs', 'SJACOBS', '1.515.555.0168', '2012-06-07', 'HR_REP', 6500.00, NULL, 101, 40), +(204, 'Hermann', 'Brown', 'HBROWN', '1.515.555.0169', '2012-06-07', 'PR_REP', 10000.00, NULL, 101, 70), +(205, 'Shelley', 'Higgins', 'SHIGGINS', '1.515.555.0170', '2012-06-07', 'AC_MGR', 12008.00, NULL, 101, 110), +(206, 'William', 'Gietz', 'WGIETZ', '1.515.555.0171', '2012-06-07', 'AC_ACCOUNT', 8300.00, NULL, 205, 110); + +-- ============================================================================ +-- Update DEPARTMENTS with manager_id values +-- ============================================================================ +UPDATE departments SET manager_id = 200 WHERE department_id = 10; +UPDATE departments SET manager_id = 201 WHERE department_id = 20; +UPDATE departments SET manager_id = 114 WHERE department_id = 30; +UPDATE departments SET manager_id = 203 WHERE department_id = 40; +UPDATE departments SET manager_id = 121 WHERE department_id = 50; +UPDATE departments SET manager_id = 103 WHERE department_id = 60; +UPDATE departments SET manager_id = 204 WHERE department_id = 70; +UPDATE departments SET manager_id = 145 WHERE department_id = 80; +UPDATE departments SET manager_id = 100 WHERE department_id = 90; +UPDATE departments SET manager_id = 108 WHERE department_id = 100; +UPDATE departments SET manager_id = 205 WHERE department_id = 110; + +-- ============================================================================ +-- Table: JOB_HISTORY (10 rows) +-- ============================================================================ +INSERT INTO job_history (employee_id, start_date, end_date, job_id, department_id) VALUES +(101, '2007-09-21', '2011-10-27', 'AC_ACCOUNT', 110), +(101, '2011-10-28', '2015-03-15', 'AC_MGR', 110), +(102, '2011-01-13', '2016-07-24', 'IT_PROG', 60), +(114, '2016-03-24', '2017-12-31', 'ST_CLERK', 50), +(122, '2017-01-01', '2017-12-31', 'ST_CLERK', 50), +(176, '2016-03-24', '2016-12-31', 'SA_REP', 80), +(176, '2017-01-01', '2017-12-31', 'SA_MAN', 80), +(200, '2005-09-17', '2011-06-17', 'AD_ASST', 90), +(200, '2012-07-01', '2016-12-31', 'AC_ACCOUNT', 90), +(201, '2014-02-17', '2017-12-19', 'MK_REP', 20); + +-- Re-enable triggers +SET session_replication_role = 'origin'; + +-- ============================================================================ +-- Verify row counts +-- ============================================================================ +SELECT 'regions' as table_name, COUNT(*) as row_count FROM regions +UNION ALL +SELECT 'countries', COUNT(*) FROM countries +UNION ALL +SELECT 'locations', COUNT(*) FROM locations +UNION ALL +SELECT 'jobs', COUNT(*) FROM jobs +UNION ALL +SELECT 'departments', COUNT(*) FROM departments +UNION ALL +SELECT 'employees', COUNT(*) FROM employees +UNION ALL +SELECT 'job_history', COUNT(*) FROM job_history +ORDER BY table_name; + +-- ============================================================================ +-- End of data insert script +-- Total rows inserted: 218 +-- ============================================================================ diff --git a/src/postgres/hr_schema_postgres.sql b/src/postgres/hr_schema_postgres.sql new file mode 100644 index 0000000000..27146c5fee --- /dev/null +++ b/src/postgres/hr_schema_postgres.sql @@ -0,0 +1,185 @@ +-- ============================================================================ +-- PostgreSQL DDL Script for HR Schema +-- Generated from Oracle HR Schema +-- Date: 2025-12-11 +-- ============================================================================ + +-- ============================================================================ +-- Schema: HR +-- Description: Create HR schema and switch to it +-- ============================================================================ +CREATE SCHEMA IF NOT EXISTS hr; +SET search_path TO hr; + +-- Drop tables if they exist (in reverse order of dependencies) +DROP TABLE IF EXISTS job_history CASCADE; +DROP TABLE IF EXISTS employees CASCADE; +DROP TABLE IF EXISTS departments CASCADE; +DROP TABLE IF EXISTS jobs CASCADE; +DROP TABLE IF EXISTS locations CASCADE; +DROP TABLE IF EXISTS countries CASCADE; +DROP TABLE IF EXISTS regions CASCADE; + +-- ============================================================================ +-- Table: REGIONS +-- Description: Stores region information (e.g., Americas, Europe, Asia) +-- ============================================================================ +CREATE TABLE regions ( + region_id INTEGER NOT NULL, + region_name VARCHAR(25), + CONSTRAINT reg_id_pk PRIMARY KEY (region_id) +); + +-- ============================================================================ +-- Table: COUNTRIES +-- Description: Stores country information with region association +-- ============================================================================ +CREATE TABLE countries ( + country_id CHAR(2) NOT NULL, + country_name VARCHAR(60), + region_id INTEGER, + CONSTRAINT country_c_id_pk PRIMARY KEY (country_id), + CONSTRAINT countr_reg_fk FOREIGN KEY (region_id) + REFERENCES regions(region_id) +); + +-- ============================================================================ +-- Table: LOCATIONS +-- Description: Stores physical location information for offices +-- ============================================================================ +CREATE TABLE locations ( + location_id INTEGER NOT NULL, + street_address VARCHAR(40), + postal_code VARCHAR(12), + city VARCHAR(30) NOT NULL, + state_province VARCHAR(25), + country_id CHAR(2), + CONSTRAINT loc_id_pk PRIMARY KEY (location_id), + CONSTRAINT loc_c_id_fk FOREIGN KEY (country_id) + REFERENCES countries(country_id) +); + +-- ============================================================================ +-- Table: JOBS +-- Description: Stores job titles and salary ranges +-- ============================================================================ +CREATE TABLE jobs ( + job_id VARCHAR(10) NOT NULL, + job_title VARCHAR(35) NOT NULL, + min_salary INTEGER, + max_salary INTEGER, + CONSTRAINT job_id_pk PRIMARY KEY (job_id) +); + +-- ============================================================================ +-- Table: DEPARTMENTS +-- Description: Stores department information +-- Note: MANAGER_ID FK is added after EMPLOYEES table is created +-- ============================================================================ +CREATE TABLE departments ( + department_id INTEGER NOT NULL, + department_name VARCHAR(30) NOT NULL, + manager_id INTEGER, + location_id INTEGER, + CONSTRAINT dept_id_pk PRIMARY KEY (department_id), + CONSTRAINT dept_loc_fk FOREIGN KEY (location_id) + REFERENCES locations(location_id) +); + +-- ============================================================================ +-- Table: EMPLOYEES +-- Description: Stores employee information +-- ============================================================================ +CREATE TABLE employees ( + employee_id INTEGER NOT NULL, + first_name VARCHAR(20), + last_name VARCHAR(25) NOT NULL, + email VARCHAR(25) NOT NULL, + phone_number VARCHAR(20), + hire_date DATE NOT NULL, + job_id VARCHAR(10) NOT NULL, + salary NUMERIC(8,2), + commission_pct NUMERIC(2,2), + manager_id INTEGER, + department_id INTEGER, + CONSTRAINT emp_emp_id_pk PRIMARY KEY (employee_id), + CONSTRAINT emp_email_uk UNIQUE (email), + CONSTRAINT emp_salary_min CHECK (salary > 0), + CONSTRAINT emp_dept_fk FOREIGN KEY (department_id) + REFERENCES departments(department_id), + CONSTRAINT emp_job_fk FOREIGN KEY (job_id) + REFERENCES jobs(job_id), + CONSTRAINT emp_manager_fk FOREIGN KEY (manager_id) + REFERENCES employees(employee_id) +); + +-- ============================================================================ +-- Add MANAGER_ID foreign key to DEPARTMENTS table +-- (Circular reference with EMPLOYEES table) +-- ============================================================================ +ALTER TABLE departments + ADD CONSTRAINT dept_mgr_fk FOREIGN KEY (manager_id) + REFERENCES employees(employee_id); + +-- ============================================================================ +-- Table: JOB_HISTORY +-- Description: Stores employee job history +-- ============================================================================ +CREATE TABLE job_history ( + employee_id INTEGER NOT NULL, + start_date DATE NOT NULL, + end_date DATE NOT NULL, + job_id VARCHAR(10) NOT NULL, + department_id INTEGER, + CONSTRAINT jhist_emp_id_st_date_pk PRIMARY KEY (employee_id, start_date), + CONSTRAINT jhist_date_interval CHECK (end_date > start_date), + CONSTRAINT jhist_emp_fk FOREIGN KEY (employee_id) + REFERENCES employees(employee_id), + CONSTRAINT jhist_job_fk FOREIGN KEY (job_id) + REFERENCES jobs(job_id), + CONSTRAINT jhist_dept_fk FOREIGN KEY (department_id) + REFERENCES departments(department_id) +); + +-- ============================================================================ +-- Create indexes for better query performance +-- ============================================================================ +CREATE INDEX emp_department_ix ON employees(department_id); +CREATE INDEX emp_job_ix ON employees(job_id); +CREATE INDEX emp_manager_ix ON employees(manager_id); +CREATE INDEX emp_name_ix ON employees(last_name, first_name); +CREATE INDEX dept_location_ix ON departments(location_id); +CREATE INDEX jhist_job_ix ON job_history(job_id); +CREATE INDEX jhist_employee_ix ON job_history(employee_id); +CREATE INDEX jhist_department_ix ON job_history(department_id); +CREATE INDEX loc_city_ix ON locations(city); +CREATE INDEX loc_state_province_ix ON locations(state_province); +CREATE INDEX loc_country_ix ON locations(country_id); + +-- ============================================================================ +-- Comments on tables +-- ============================================================================ +COMMENT ON TABLE regions IS 'Regions table that contains region numbers and names'; +COMMENT ON TABLE countries IS 'Country table with country ID and associated region ID'; +COMMENT ON TABLE locations IS 'Locations table with addresses of company offices'; +COMMENT ON TABLE departments IS 'Departments table showing department details'; +COMMENT ON TABLE jobs IS 'Jobs table with job titles and salary ranges'; +COMMENT ON TABLE employees IS 'Employees table containing employee details'; +COMMENT ON TABLE job_history IS 'Job history table tracking employee job changes'; + +-- ============================================================================ +-- Comments on columns +-- ============================================================================ +COMMENT ON COLUMN employees.employee_id IS 'Primary key of employees table'; +COMMENT ON COLUMN employees.email IS 'Email address - must be unique'; +COMMENT ON COLUMN employees.salary IS 'Monthly salary - must be greater than zero'; +COMMENT ON COLUMN employees.commission_pct IS 'Commission percentage (0.00 to 0.99)'; +COMMENT ON COLUMN departments.department_id IS 'Primary key of departments table'; +COMMENT ON COLUMN departments.manager_id IS 'Manager ID of a department. Foreign key to employee_id'; +COMMENT ON COLUMN job_history.employee_id IS 'Foreign key to employee_id in employees table'; +COMMENT ON COLUMN job_history.start_date IS 'Start date of the job - part of composite primary key'; +COMMENT ON COLUMN job_history.end_date IS 'End date of the job - must be greater than start_date'; + +-- ============================================================================ +-- End of script +-- ============================================================================ diff --git a/src/postgres/index.ts b/src/postgres/index.ts new file mode 100644 index 0000000000..d54ba436e1 --- /dev/null +++ b/src/postgres/index.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +import { runServer } from "./server.js"; + +runServer().catch(console.error); diff --git a/src/postgres/package.json b/src/postgres/package.json new file mode 100644 index 0000000000..ccec3937eb --- /dev/null +++ b/src/postgres/package.json @@ -0,0 +1,44 @@ +{ + "name": "@marcelo-ochoa/server-postgres", + "mcpName": "io.github.marcelo-ochoa/postgres", + "version": "1.0.8", + "repository": { + "type": "git", + "url": "https://github.com/marcelo-ochoa/servers.git", + "subfolder": "src/postgres" + }, + "description": "An MCP server for PostgreSQL databases.", + "keywords": [ + "read-only-mcp", + "postgres-database", + "ai-agent", + "llm-tool", + "rag" + ], + "license": "MIT", + "author": "Marcelo Fabian Ochoa", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/marcelo-ochoa/servers/issues", + "type": "module", + "bin": { + "mcp-server-postgres": "dist/index.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc && shx chmod +x dist/*.js", + "prepare": "npm run build", + "watch": "tsc --watch" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.24.2", + "@toon-format/toon": "^1.0.0", + "pg": "^8.13.0" + }, + "devDependencies": { + "@types/pg": "^8.11.10", + "shx": "^0.3.4", + "typescript": "^5.6.2" + } +} \ No newline at end of file diff --git a/src/postgres/resources.ts b/src/postgres/resources.ts new file mode 100644 index 0000000000..86f64cb690 --- /dev/null +++ b/src/postgres/resources.ts @@ -0,0 +1,90 @@ +import { ListResourcesRequest, ReadResourceRequest } from "@modelcontextprotocol/sdk/types.js"; +import { withConnection, isPoolInitialized } from "./db.js"; + +const SCHEMA_PATH = "schema"; + +export const listResourcesHandler = async (request: ListResourcesRequest) => { + if (!isPoolInitialized()) { + return { resources: [] }; + } + return await withConnection(async (client) => { + const result = await client.query( + "SELECT table_name, table_schema FROM information_schema.tables WHERE table_schema NOT IN ('information_schema', 'pg_catalog')", + ); + return { + resources: result.rows.map((row: any) => ({ + uri: `postgres://${row.table_schema}/${row.table_name}/${SCHEMA_PATH}`, + mimeType: "application/json", + name: `"${row.table_schema}"."${row.table_name}" database schema`, + })), + }; + }); +}; + +export const readResourceHandler = async (request: ReadResourceRequest) => { + const resourceUrl = new URL(request.params.uri); + + const pathComponents = resourceUrl.pathname.split("/"); + const schema = pathComponents.pop(); + const tableName = pathComponents.pop(); + const schemaName = resourceUrl.hostname; + + if (schema !== SCHEMA_PATH) { + throw new Error("Invalid resource URI"); + } + + return await withConnection(async (client) => { + const columnsResult = await client.query( + `SELECT + column_name, + data_type, + is_nullable, + column_default, + character_maximum_length, + numeric_precision, + numeric_scale + FROM information_schema.columns + WHERE table_name = $1 AND table_schema = $2 + ORDER BY ordinal_position`, + [tableName, schemaName], + ); + + const indexesResult = await client.query( + `SELECT + ix.relname as index_name, + i.indisunique as is_unique, + a.attname as column_name + FROM + pg_class t, + pg_class ix, + pg_index i, + pg_attribute a, + pg_namespace n + WHERE + t.oid = i.indrelid + AND ix.oid = i.indexrelid + AND a.attrelid = t.oid + AND a.attnum = ANY(i.indkey) + AND t.relkind = 'r' + AND t.relname = $1 + AND n.oid = t.relnamespace + AND n.nspname = $2 + ORDER BY + ix.relname`, + [tableName, schemaName] + ); + + return { + contents: [ + { + uri: request.params.uri, + mimeType: "application/json", + text: JSON.stringify({ + columns: columnsResult.rows, + indexes: indexesResult.rows + }, null, 2), + }, + ], + }; + }); +}; diff --git a/src/postgres/server.json b/src/postgres/server.json new file mode 100644 index 0000000000..7fc5b18e67 --- /dev/null +++ b/src/postgres/server.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.marcelo-ochoa/postgres", + "description": "MCP server for interacting with PostgreSQL databases", + "repository": { + "url": "https://github.com/marcelo-ochoa/servers", + "source": "github", + "subfolder": "src/postgres" + }, + "version": "1.0.8", + "packages": [ + { + "registryType": "npm", + "identifier": "@marcelo-ochoa/server-postgres", + "version": "1.0.8", + "transport": { + "type": "stdio" + }, + "packageArguments": [ + { + "type": "positional", + "valueHint": "connectionString", + "description": "PostgreSQL connection string", + "isRequired": false + } + ], + "environmentVariables": [ + { + "description": "PostgreSQL user", + "isRequired": false, + "format": "string", + "isSecret": false, + "name": "POSTGRES_USER" + }, + { + "description": "PostgreSQL password", + "isRequired": false, + "format": "string", + "isSecret": true, + "name": "POSTGRES_PASSWORD" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/postgres/server.ts b/src/postgres/server.ts new file mode 100644 index 0000000000..9c8eb09543 --- /dev/null +++ b/src/postgres/server.ts @@ -0,0 +1,109 @@ +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import { initializePool } from "./db.js"; +import { listResourcesHandler, readResourceHandler, callToolHandler } from "./handlers.js"; +import { tools } from "./tools.js"; + +// Create server instance +const server = new McpServer({ + name: "postgres-server", + version: "1.0.8", +}); + +const prompts = [ + { name: "pg-query: Execute Query", description: "pg-query select * from test" }, + { name: "pg-explain: Explain Query", description: "pg-explain select * from test" }, + { name: "pg-stats: Table Statistics", description: "pg-stats test" }, + { name: "pg-connect: Database Connection", description: "pg-connect to PostgreSQL using a connection string like host.docker.internal:5432/dbname with user name and password" }, + { name: "pg-awr: Performance Report", description: "pg-awr to generate a PostgreSQL performance report (requires pg_stat_statements extension)" } +]; + +// Register Prompts +server.registerPrompt("postgres-prompts", { + description: "List available Postgres prompts" +}, async () => ({ + messages: [ + { + role: "assistant", + content: { + type: "text", + text: "Available Postgres prompts:\n" + prompts.map(p => `- ${p.name}: ${p.description}`).join("\n") + } + } + ] +})); + +// Register Resource Templates +const resourceTemplate = new ResourceTemplate("postgres://{schema}/{table}/schema", { + list: async () => listResourcesHandler({} as any) +}); +server.registerResource( + "Table Schema", + resourceTemplate, + { description: "Schema information for a PostgreSQL database table including column names and data types" }, + async (uri) => { + return readResourceHandler({ params: { uri: uri.toString() } } as any); + } +); + +// Register Tools +tools.forEach(tool => { + // Basic mapping of JSON schema to Zod for simple cases + let inputSchema: any = z.object({}); + if (tool.inputSchema && tool.inputSchema.properties) { + const shape: Record = {}; + for (const [key, prop] of Object.entries(tool.inputSchema.properties)) { + let field: any = z.any(); + if ((prop as any).type === "string") { + field = z.string(); + } + if ((prop as any).description) { + field = field.describe((prop as any).description); + } + if (tool.inputSchema.required && !tool.inputSchema.required.includes(key)) { + field = field.optional(); + } else if (!tool.inputSchema.required) { + field = field.optional(); + } + shape[key] = field; + } + inputSchema = z.object(shape); + } + + server.registerTool( + tool.name, + { + description: tool.description, + inputSchema: inputSchema + }, + async (args: any) => { + return callToolHandler({ params: { name: tool.name, arguments: args } } as any); + } + ); +}); + +export async function runServer() { + const args = process.argv.slice(2); + const databaseUrl = args[0]; + + if (databaseUrl) { + try { + await initializePool(databaseUrl); + } catch (error) { + console.error("Failed to initialize database pool:", error); + process.exit(1); + } + } else { + console.error("Warning: No database URL provided. Use pg-connect tool before using other functionality."); + } + + const transport = new StdioServerTransport(); + await server.connect(transport); + + process.stdin.on("close", () => { + console.error("Postgres MCP Server closed"); + server.close(); + process.exit(0); + }); +} diff --git a/src/postgres/tools.ts b/src/postgres/tools.ts new file mode 100644 index 0000000000..25d879ec8d --- /dev/null +++ b/src/postgres/tools.ts @@ -0,0 +1,125 @@ +export const tools = [ + { + name: "pg-query", + description: "This tool executes SQL queries in a READ ONLY session connected to a PostgreSQL database. If no active connection exists, it uses MCP server registration argument and environment variables PG_USER and PG_PASSWORD.\n\nYou should:\n\n\tExecute the provided SQL query.\n\n\tReturn the results in Toon format.\n\nArgs:\n\n\tsql: The SQL query to execute.\n\n\nThe `model` argument should specify only the name and version of the LLM (Large Language Model) you are using, with no additional information.\nThe `mcp_client` argument should specify only the name of the MCP (Model Context Protocol) client you are using, with no additional information.\n\nReturns:\n\n\tJson-formatted query results.\nFor every SQL query you generate, please include a comment at the beginning of the SELECT statement (or other main SQL command) that identifies the LLM model name and version you are using. Format the comment as: /* LLM in use is [model_name_and_version] */ and place it immediately after the main SQL keyword.\nFor example:\n\nSELECT /* LLM in use is claude-sonnet-4 */ column1, column2 FROM table_name;\n\nPlease apply this format consistently to all SQL queries you generate, using your actual model name and version in the comment\n", + inputSchema: { + type: "object", + properties: { + sql: { + type: "string", + description: "The SQL query to execute" + }, + mcp_client: { + "type": "string", + "description": "Specify the name and version of the MCP client implementation being used (e.g. Copilot, Claude, Cline...)", + "default": "UNKNOWN-MCP-CLIENT" + }, + model: { + "type": "string", + "description": "The name (and version) of the language model being used by the MCP client to process requests (e.g. gpt-4.1, claude-sonnet-4, llama4...)", + "default": "UNKNOWN-LLM" + } + }, + required: ["sql", "mcp_client", "model"] + }, + }, + { + name: "pg-stats", + description: "Get comprehensive statistics for a specific PostgreSQL table. This tool retrieves detailed information including row counts, table size, index information, column statistics, and other metadata that can help optimize queries and understand data distribution.\n\nArgs:\n\n\tname: The name of the table to get statistics for.\n\n\nThe `model` argument should specify only the name and version of the LLM (Large Language Model) you are using, with no additional information.\nThe `mcp_client` argument should specify only the name of the MCP (Model Context Protocol) client you are using, with no additional information.\n\nReturns:\n\n\tJson-formatted table statistics including row counts, size information, indexes, and column details.\n", + inputSchema: { + type: "object", + properties: { + name: { + type: "string", + description: "The name of the table to get statistics for" + }, + mcp_client: { + "type": "string", + "description": "Specify the name and version of the MCP client implementation being used (e.g. Copilot, Claude, Cline...)", + "default": "UNKNOWN-MCP-CLIENT" + }, + model: { + "type": "string", + "description": "The name (and version) of the language model being used by the MCP client to process requests (e.g. gpt-4.1, claude-sonnet-4, llama4...)", + "default": "UNKNOWN-LLM" + } + }, + required: ["name"] + }, + }, + { + name: "pg-explain", + description: "Generate and display the execution plan for a given SQL query using PostgreSQL's EXPLAIN command. This tool helps you understand how PostgreSQL will execute your query, including information about table scans, joins, indexes used, and estimated costs.\n\nArgs:\n\n\tsql: The SQL query to explain.\n\n\nThe `model` argument should specify only the name and version of the LLM (Large Language Model) you are using, with no additional information.\nThe `mcp_client` argument should specify only the name of the MCP (Model Context Protocol) client you are using, with no additional information.\n\nReturns:\n\n\tDetailed execution plan showing how PostgreSQL will process the query, including costs, row estimates, and access methods.\n", + inputSchema: { + type: "object", + properties: { + sql: { + type: "string", + description: "The SQL query to explain" + }, + mcp_client: { + "type": "string", + "description": "Specify the name and version of the MCP client implementation being used (e.g. Copilot, Claude, Cline...)", + "default": "UNKNOWN-MCP-CLIENT" + }, + model: { + "type": "string", + "description": "The name (and version) of the language model being used by the MCP client to process requests (e.g. gpt-4.1, claude-sonnet-4, llama4...)", + "default": "UNKNOWN-LLM" + } + }, + required: ["sql", "mcp_client", "model"] + }, + }, + { + name: "pg-connect", + description: "Provides an interface to connect to a specified PostgreSQL database. If a database connection is already active, the tool will close the existing connection before establishing a new one.\n\nThis tool accepts three required parameters:\n\n\tconnectionString: The PostgreSQL connection string without embedded credentials (e.g., postgresql://host:port/dbname or host:port/dbname). To enable encryption (SSL), append ?sslmode=require to the connection string.\n\tuser: The PostgreSQL username\n\tpassword: The PostgreSQL password\n\nThe credentials are stored in environment variables PG_USER and PG_PASSWORD for the session.\n\nThe `model` argument should only be used to specify the name and version of the LLM (Large Language Model) you are using, with no additional information.\nThe `mcp_client` argument should specify only the name of the MCP (Model Context Protocol) client you are using, with no additional information.\n", + inputSchema: { + type: "object", + properties: { + connectionString: { + type: "string", + description: "The PostgreSQL connection string (e.g. postgresql://host:port/dbname or host:port/dbname)" + }, + user: { + type: "string", + description: "The PostgreSQL user (e.g. postgres)" + }, + password: { + type: "string", + description: "The PostgreSQL password" + }, + mcp_client: { + "type": "string", + "description": "Specify the name and version of the MCP client implementation being used (e.g. Copilot, Claude, Cline...)", + "default": "UNKNOWN-MCP-CLIENT" + }, + model: { + "type": "string", + "description": "The name (and version) of the language model being used by the MCP client to process requests (e.g. gpt-4.1, claude-sonnet-4, llama4...)", + "default": "UNKNOWN-LLM" + } + }, + required: ["connectionString", "user", "password"] + }, + }, + { + name: "pg-awr", + description: "Generate a PostgreSQL performance report similar to Oracle AWR. Includes database statistics, top queries (requires pg_stat_statements extension), table/index statistics, connection info, and optimization recommendations.", + inputSchema: { + type: "object", + properties: { + mcp_client: { + "type": "string", + "description": "Specify the name and version of the MCP client implementation being used (e.g. Copilot, Claude, Cline...)", + "default": "UNKNOWN-MCP-CLIENT" + }, + model: { + "type": "string", + "description": "The name (and version) of the language model being used by the MCP client to process requests (e.g. gpt-4.1, claude-sonnet-4, llama4...)", + "default": "UNKNOWN-LLM" + } + }, + }, + }, +]; diff --git a/src/postgres/tools/awr.ts b/src/postgres/tools/awr.ts new file mode 100644 index 0000000000..3208bbe856 --- /dev/null +++ b/src/postgres/tools/awr.ts @@ -0,0 +1,296 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { withConnection } from "../db.js"; + +export const awrHandler = async (request: CallToolRequest) => { + try { + return await withConnection(async (client) => { + const report: any = { + timestamp: new Date().toISOString(), + database_statistics: {}, + top_queries: [], + top_queries_by_cpu: [], + top_queries_by_io: [], + table_statistics: [], + index_statistics: [], + connection_info: {}, + }; + + // Check if pg_stat_statements extension is available + const extCheck = await client.query(` + SELECT EXISTS ( + SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements' + ) as has_extension + `); + + const hasPgStatStatements = extCheck.rows[0].has_extension; + + // 1. Database-wide statistics + const dbStats = await client.query(` + SELECT + datname, + numbackends as active_connections, + xact_commit as transactions_committed, + xact_rollback as transactions_rolled_back, + blks_read as blocks_read, + blks_hit as blocks_hit, + CASE + WHEN (blks_read + blks_hit) > 0 + THEN ROUND(100.0 * blks_hit / (blks_read + blks_hit), 2) + ELSE 0 + END as cache_hit_ratio, + tup_returned as tuples_returned, + tup_fetched as tuples_fetched, + tup_inserted as tuples_inserted, + tup_updated as tuples_updated, + tup_deleted as tuples_deleted, + conflicts, + temp_files, + temp_bytes, + deadlocks, + blk_read_time, + blk_write_time + FROM pg_stat_database + WHERE datname = current_database() + `); + report.database_statistics = dbStats.rows[0]; + + // 2. Top queries by total time (if pg_stat_statements is available) + if (hasPgStatStatements) { + try { + const baseQuery = ` + SELECT + queryid, + LEFT(query, 100) as query_text, + calls, + ROUND(total_exec_time::numeric, 2) as total_time_ms, + ROUND(mean_exec_time::numeric, 2) as mean_time_ms, + ROUND(min_exec_time::numeric, 2) as min_time_ms, + ROUND(max_exec_time::numeric, 2) as max_time_ms, + ROUND(stddev_exec_time::numeric, 2) as stddev_time_ms, + rows as total_rows, + ROUND((100.0 * shared_blks_hit / NULLIF(shared_blks_hit + shared_blks_read, 0))::numeric, 2) as buffer_hit_ratio, + shared_blks_read, + shared_blks_hit, + shared_blks_dirtied, + shared_blks_written, + temp_blks_read, + temp_blks_written + FROM pg_stat_statements + WHERE dbid = (SELECT oid FROM pg_database WHERE datname = current_database()) + `; + + // Top by Total Time + const topQueries = await client.query(` + ${baseQuery} + ORDER BY total_exec_time DESC + LIMIT 20 + `); + report.top_queries = topQueries.rows; + + // Top by CPU (Rows Processed) + const topCpuQueries = await client.query(` + ${baseQuery} + ORDER BY rows DESC + LIMIT 5 + `); + report.top_queries_by_cpu = topCpuQueries.rows; + + // Top by IO (Blocks Read + Written) + const topIoQueries = await client.query(` + ${baseQuery} + ORDER BY (shared_blks_read + shared_blks_written) DESC + LIMIT 5 + `); + report.top_queries_by_io = topIoQueries.rows; + + } catch (error: any) { + report.top_queries_note = `pg_stat_statements extension exists but is not properly loaded. Error: ${error.message}. Add 'shared_preload_libraries = pg_stat_statements' to postgresql.conf and restart PostgreSQL.`; + } + } else { + const note = "pg_stat_statements extension not available. Install with: CREATE EXTENSION pg_stat_statements;"; + report.top_queries_note = note; + report.top_queries_by_cpu_note = note; + report.top_queries_by_io_note = note; + } + + // 3. Table statistics + const tableStats = await client.query(` + SELECT + schemaname, + relname as table_name, + seq_scan, + seq_tup_read, + idx_scan, + idx_tup_fetch, + n_tup_ins as inserts, + n_tup_upd as updates, + n_tup_del as deletes, + n_tup_hot_upd as hot_updates, + n_live_tup as live_tuples, + n_dead_tup as dead_tuples, + ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) as dead_tuple_ratio, + last_vacuum, + last_autovacuum, + last_analyze, + last_autoanalyze, + vacuum_count, + autovacuum_count, + analyze_count, + autoanalyze_count + FROM pg_stat_user_tables + ORDER BY seq_scan + COALESCE(idx_scan, 0) DESC + LIMIT 20 + `); + report.table_statistics = tableStats.rows; + + // 4. Index statistics + const indexStats = await client.query(` + SELECT + schemaname, + relname as table_name, + indexrelname as index_name, + idx_scan as index_scans, + idx_tup_read as tuples_read, + idx_tup_fetch as tuples_fetched, + pg_size_pretty(pg_relation_size(indexrelid)) as index_size + FROM pg_stat_user_indexes + ORDER BY idx_scan DESC + LIMIT 20 + `); + report.index_statistics = indexStats.rows; + + // 5. Connection and activity info + const connInfo = await client.query(` + SELECT + COUNT(*) as total_connections, + COUNT(*) FILTER (WHERE state = 'active') as active, + COUNT(*) FILTER (WHERE state = 'idle') as idle, + COUNT(*) FILTER (WHERE state = 'idle in transaction') as idle_in_transaction, + COUNT(*) FILTER (WHERE wait_event_type IS NOT NULL) as waiting, + MAX(EXTRACT(EPOCH FROM (now() - query_start))) as longest_query_seconds, + MAX(EXTRACT(EPOCH FROM (now() - xact_start))) as longest_transaction_seconds + FROM pg_stat_activity + WHERE datname = current_database() + `); + report.connection_info = connInfo.rows[0]; + + // 6. Background writer and checkpoint statistics + // In PostgreSQL 17+, checkpoint stats moved to pg_stat_checkpointer + // and buffer backend stats moved to pg_stat_io + const versionResult = await client.query('SHOW server_version_num'); + const versionNum = parseInt(versionResult.rows[0].server_version_num); + + if (versionNum >= 170000) { + // PostgreSQL 17+: Query multiple views + const checkpointerStats = await client.query(` + SELECT + num_timed as checkpoints_timed, + num_requested as checkpoints_requested, + write_time as checkpoint_write_time, + sync_time as checkpoint_sync_time, + buffers_written as buffers_checkpoint, + stats_reset + FROM pg_stat_checkpointer + `); + + const bgWriterStats = await client.query(` + SELECT + buffers_clean, + maxwritten_clean, + buffers_alloc + FROM pg_stat_bgwriter + `); + + // Get backend buffer stats from pg_stat_io + const ioStats = await client.query(` + SELECT + SUM(reads) FILTER (WHERE backend_type = 'client backend') as buffers_backend_read, + SUM(writes) FILTER (WHERE backend_type = 'client backend') as buffers_backend_write, + SUM(fsyncs) FILTER (WHERE backend_type = 'client backend') as buffers_backend_fsync + FROM pg_stat_io + `); + + report.bgwriter_statistics = { + ...checkpointerStats.rows[0], + ...bgWriterStats.rows[0], + ...ioStats.rows[0] + }; + } else { + // PostgreSQL < 17: Use pg_stat_bgwriter for everything + const bgWriterStats = await client.query(` + SELECT + checkpoints_timed, + checkpoints_req as checkpoints_requested, + checkpoint_write_time, + checkpoint_sync_time, + buffers_checkpoint, + buffers_clean, + maxwritten_clean, + buffers_backend, + buffers_backend_fsync, + buffers_alloc, + stats_reset + FROM pg_stat_bgwriter + `); + report.bgwriter_statistics = bgWriterStats.rows[0]; + } + + // 7. Unused indexes (potential optimization candidates) + const unusedIndexes = await client.query(` + SELECT + schemaname, + relname as table_name, + indexrelname as index_name, + idx_scan as scans, + pg_size_pretty(pg_relation_size(indexrelid)) as index_size + FROM pg_stat_user_indexes + WHERE idx_scan = 0 + AND indexrelname NOT LIKE '%_pkey' + ORDER BY pg_relation_size(indexrelid) DESC + LIMIT 10 + `); + report.unused_indexes = unusedIndexes.rows; + + // 8. Recommendations + const recommendations: string[] = []; + + // Check cache hit ratio + const cacheHitRatio = parseFloat(report.database_statistics.cache_hit_ratio || '0'); + if (cacheHitRatio < 99) { + recommendations.push(`Buffer cache hit ratio is ${cacheHitRatio}%. Consider increasing shared_buffers.`); + } + + // Check unused indexes + if (report.unused_indexes && report.unused_indexes.length > 0) { + recommendations.push(`Found ${report.unused_indexes.length} unused indexes. Consider removing them to improve write performance.`); + } + + // Check dead tuples + if (report.table_statistics) { + const highDeadTuples = report.table_statistics.filter((t: any) => parseFloat(t.dead_tuple_ratio || '0') > 10); + if (highDeadTuples.length > 0) { + recommendations.push(`${highDeadTuples.length} tables have >10% dead tuples. Check autovacuum settings.`); + } + } + + report.recommendations = recommendations; + + return { + content: [{ + type: "text", + text: JSON.stringify(report, null, 2), + mimeType: "application/json" + }], + isError: false, + }; + }); + } catch (error: any) { + return { + content: [{ + type: "text", + text: `Error generating PostgreSQL performance report: ${error?.message ?? error}` + }], + isError: true, + }; + } +}; diff --git a/src/postgres/tools/connect.ts b/src/postgres/tools/connect.ts new file mode 100644 index 0000000000..f5cffeb972 --- /dev/null +++ b/src/postgres/tools/connect.ts @@ -0,0 +1,37 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { initializePool, closePool } from "../db.js"; + +export const connectHandler = async (request: CallToolRequest) => { + const newConnectionString = request.params.arguments?.connectionString; + const newUser = request.params.arguments?.user; + const newPassword = request.params.arguments?.password; + + if ( + typeof newConnectionString !== "string" || !newConnectionString || + typeof newUser !== "string" || !newUser || + typeof newPassword !== "string" || !newPassword + ) { + return { + content: [{ type: "text", text: "Missing or invalid connectionString, user, or password argument." }], + isError: true, + }; + } + + try { + await closePool(); + // Override env vars for this session + process.env.PG_USER = newUser; + process.env.PG_PASSWORD = newPassword; + await initializePool(newConnectionString); + return { + content: [{ type: "text", text: `Successfully connected to Postgres DB: ${newConnectionString} as user ${newUser}` }], + isError: false, + }; + } catch (err) { + return { + content: [{ type: "text", text: `Failed to connect: ${err}` }], + isError: true, + }; + } +}; + diff --git a/src/postgres/tools/explain.ts b/src/postgres/tools/explain.ts new file mode 100644 index 0000000000..579163fd48 --- /dev/null +++ b/src/postgres/tools/explain.ts @@ -0,0 +1,16 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { withConnection } from "../db.js"; + +export const explainHandler = async (request: CallToolRequest) => { + const sql = typeof request.params.arguments?.sql === "string" + ? request.params.arguments.sql.replace(/;\s*$/, "") + : ""; + + return await withConnection(async (client) => { + const result = await client.query(`EXPLAIN (ANALYZE, VERBOSE, BUFFERS, FORMAT JSON) ${sql}`); + return { + content: [{ type: "text", text: JSON.stringify(result.rows[0], null, 2), mimeType: "application/json" }], + isError: false, + }; + }); +}; diff --git a/src/postgres/tools/query.ts b/src/postgres/tools/query.ts new file mode 100644 index 0000000000..3671f26df8 --- /dev/null +++ b/src/postgres/tools/query.ts @@ -0,0 +1,28 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { encode } from "@toon-format/toon"; +import { withConnection } from "../db.js"; + +export const queryHandler = async (request: CallToolRequest) => { + const sql = typeof request.params.arguments?.sql === "string" + ? request.params.arguments.sql.replace(/;\s*$/, "") + : ""; + + return await withConnection(async (client) => { + try { + await client.query("BEGIN TRANSACTION READ ONLY"); + const result = await client.query(sql); + return { + content: [{ type: "text", text: encode(result.rows) }], + isError: false, + }; + } catch (error) { + throw error; + } finally { + client + .query("ROLLBACK") + .catch((error) => + console.warn("Could not roll back transaction:", error), + ); + } + }); +}; diff --git a/src/postgres/tools/stats.ts b/src/postgres/tools/stats.ts new file mode 100644 index 0000000000..827baa0267 --- /dev/null +++ b/src/postgres/tools/stats.ts @@ -0,0 +1,65 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { withConnection } from "../db.js"; + +export const statsHandler = async (request: CallToolRequest) => { + let schema = 'public'; + let tableName = request.params.arguments?.name as string; + + if (tableName.includes('.')) { + const parts = tableName.split('.'); + schema = parts[0]; + tableName = parts[1]; + } + + return await withConnection(async (client) => { + const result = await client.query(` + SELECT json_build_object( + 'table_stats', ( + SELECT json_build_object( + 'schema_name', n.nspname, + 'table_name', c.relname, + 'num_rows', c.reltuples, + 'blocks', c.relpages, + 'last_analyzed', s.last_analyze + ) + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + LEFT JOIN pg_stat_user_tables s ON s.relid = c.oid + WHERE c.relname = $1 AND n.nspname = $2 + ), + 'index_stats', ( + SELECT json_agg( + json_build_object( + 'index_name', c2.relname, + 'num_rows', c2.reltuples, + 'blocks', c2.relpages, + 'index_size', pg_size_pretty(pg_relation_size(c2.oid)) + ) + ) + FROM pg_index i + JOIN pg_class c ON c.oid = i.indrelid + JOIN pg_class c2 ON c2.oid = i.indexrelid + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = $1 AND n.nspname = $2 + ), + 'column_stats', ( + SELECT json_agg( + json_build_object( + 'column_name', attname, + 'null_frac', null_frac, + 'avg_width', avg_width, + 'n_distinct', n_distinct + ) + ) + FROM pg_stats + WHERE tablename = $1 AND schemaname = $2 + ) + ) as stats_json + `, [tableName, schema]); + + return { + content: [{ type: "text", text: JSON.stringify(result.rows[0].stats_json, null, 2), mimeType: "application/json" }], + isError: false, + }; + }); +}; diff --git a/src/postgres/tsconfig.json b/src/postgres/tsconfig.json new file mode 100644 index 0000000000..ec5da15825 --- /dev/null +++ b/src/postgres/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "." + }, + "include": [ + "./**/*.ts" + ] +} diff --git a/src/qnap/.dockerignore b/src/qnap/.dockerignore new file mode 100644 index 0000000000..c30cc5bae2 --- /dev/null +++ b/src/qnap/.dockerignore @@ -0,0 +1,6 @@ +node_modules +dist +*.ts +tsconfig.json +Dockerfile +.dockerignore diff --git a/src/qnap/CHANGELOG.md b/src/qnap/CHANGELOG.md new file mode 100644 index 0000000000..a2ac85cd13 --- /dev/null +++ b/src/qnap/CHANGELOG.md @@ -0,0 +1,92 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [1.0.8] - 2026-03-11 + +### Changed +- **chore**: Bump server version to 1.0.8 + - Updated version to 1.0.8 across package.json, server.json, and server.ts + - Migrated to `McpServer` API from deprecated `Server` class + - Implemented `ResourceTemplate` for dynamic resource discovery + + +## [1.0.7] - 2026-03-07 + +### Changed +- **chore**: Bump server version to 1.0.7 + - Updated version to 1.0.7 across package.json, server.json, and server.ts + - Refactored error handling in `resources.ts` to throw `McpError` when connection is not established. + + +## [1.0.6] - 2026-03-02 + +### Changed +- **Improved API Security**: Switched to `URLSearchParams` for safe URL construction across all tools and resources, preventing potential encoding issues. +- **Enhanced Connection Feedback**: Improved error handling in `qnap-connect` and removed sensitive session information (SID) from the output. +- **Improved Error Messaging**: Standardized error responses to be more user-friendly and informative. + +## [1.0.5] - 2026-02-25 + +### Added +- **MCP Resources Support**: Introduced native support for MCP Resources. + - Exposes QNAP **Disks** as resources: `qnap://[ip]:[port]/disk/[disk-id]`. + - Exposes QNAP **Volumes** as resources: `qnap://[ip]:[port]/volume/[volume-id]`. + - Allows AI models to directly read structured JSON data for disk health and volume usage. +- **Enhanced Storage Metadata**: Updated internal parsing logic to include volume IDs, enabling more reliable resource addressing. +- **Resource Handlers**: Implemented `listResourcesHandler` and `readResourceHandler` for full MCP compatibility. + +## [1.0.4] - 2026-02-23 + +### Changed +- **Version Synchronization**: Updated versioning across `package.json`, `server.ts`, and `server.json` to 1.0.4. +- **Project Maintenance**: Verified server stability and report accuracy through automated backup and swarm status workflows. + +## [1.0.3] - 2026-02-12 + +### Added +- **Professional Tabular Output**: Standardized `qnap-dir` and `qnap-report` to use the `toon` encoding format for high-quality tabular data. +- **Enhanced qnap-report**: + - Now returns a professional **Markdown report** instead of raw JSON. + - Includes Disk Health and Storage Information in styled tables. + - Improved formatting for resource usage (CPU, Memory, Uptime). +- **Improved qnap-dir**: + - Automatically **sorts files by modification date** (newest first). + - Robust directory detection across different QNAP firmware versions (handling `isfolder`, `is_dir`, and `filetype` inconsistencies). + - Human-friendly file sizes (KB, MB, GB). + +## [1.0.2] - 2026-02-10 + +### Added +- **Enhanced qnap-report Tool**: Completely rewritten in native TypeScript for better performance and reliability. + - Replaced Python-based report collector with native Node.js implementation. + - Returns structured **JSON format** instead of plain text, perfect for AI parsing. + - Added detailed **Disk Health** (model, serial, capacity, temperature, health status). + - Added granular **Resource Usage** (CPU load, memory breakdown, uptime, system temperature). + - Added comprehensive **Storage Info** (volume labels, total/used/free space, usage percentages). + +## [1.0.1] - 2026-02-09 + +### Changed +- **Major refactoring for improved maintainability**: Restructured codebase following MySQL MCP server patterns + - Separated tool handlers into individual files in `tools/` directory (`connect.ts`, `report.ts`, `dir.ts`, `file_info.ts`) + - Simplified `handlers.ts` to act as a clean dispatcher using a handler registry pattern + - Refactored `tools.ts` to use array-based tool definitions for consistency + - Updated `server.ts` to use the new handler dispatcher + - Improved code organization with better separation of concerns + - Enhanced error handling and validation in individual tool handlers + - Added connection state management helper functions + +### Added +- **Prompts array support**: Added prompts capability following MikroTik MCP server pattern + - Implemented `prompts/list` request handler for better tool discoverability + - Added prompts for all available tools: `qnap-connect`, `qnap-report`, `qnap-dir`, `qnap-file-info` + - Enhanced server capabilities to include prompts interface + +## [1.0.0] - 2026-01-27 + +### Added +- Initial implementation of the QNAP MCP server. +- Tools: `qnap-connect`, `qnap-report`, `qnap-dir`, `qnap-file-info`. +- Support for QTS Legacy CGI API. + diff --git a/src/qnap/Demos.md b/src/qnap/Demos.md new file mode 100644 index 0000000000..01a1b783ca --- /dev/null +++ b/src/qnap/Demos.md @@ -0,0 +1,212 @@ +# Demos + +Some sample usage scenarios are shown below: + +## 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) +* Credentials are passed via environment variables `QNAP_USER` and `QNAP_PASSWORD` + +```json +{ + "mcpServers": { + "qnap": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "QNAP_USER=admin", + "-e", + "QNAP_PASSWORD=password", + "mochoa/mcp-qnap", + "http://10.1.1.241:8080"] + } + } +} +``` + +### NPX + +```json +{ + "mcpServers": { + "qnap": { + "command": "npx", + "args": [ + "-y", + "@marcelo-ochoa/server-qnap", + "http://10.1.1.241:8080" + ], + "env": { + "QNAP_USER": "admin", + "QNAP_PASSWORD": "password" + } + } + } +} +``` + +### Demo Prompts + +Sample prompts using above server. + +- qnap-connect http://10.1.1.241:8080 admin password +- qnap-report +- qnap-dir /Public +- qnap-file-info /Public/test.txt +- analyze backup files in several subdirectories of `/backup/stacks` on the QNAP NAS. For each subdirectory compare the two most recent backups (daily or weekly, identified by filenames like `alldb_backup-2.sql.gz` for weekly and `alldb_backup-09.sql.gz` for daily). The comparison should be based on file size, considering backups with similar sizes as correct. Generate a report indicating the status (OK or Error) for each directory based on this comparison. + +### Using Resources + +You can interact with QNAP resources directly: + +- `list resources from qnap` +- `read resource qnap://10.1.1.241:8080/volume/1` +- `show health for all disks on qnap using resources` +- `check storage usage for Docker volume using qnap resources` + + +### Backup Analysis Report Example + +Prompt: Using information about files of sub-folders from qnap-dir /backup/stacks + - ayudarg + - glpi + - invop + - kanboard + - mon + - moodle + - mygis + - ocular-vet + - patogis + - piexa + - web + - yact + +files into above sub-dirs are backups daily or weekly, for example: alldb_backup-2.sql.gz (meaning last backup of week 2 of current month) alldb_backup-09.sql.gz (meaning last daily backup) in some folder there are weekly backups and other are daily backups. please compare last (weekly/daily) with the previous one sorted descending by modification date using the size information of the file. A correct backup will be similar in size, for example 31.41 Mb and 31.33Mb. Make report in tabular format including the directory name, the two most recent backup files with their modification date and size, the size difference and the status (OK or Error) based on explained failed. + +Response: +I have verified the two most recent backup files for each directory, comparing their timestamps and file sizes. "Similar size" is interpreted as a difference of less than 5%, which indicates a successful backup chain. + +#### QNAP Backup Status Report +**Date:** 2026-02-09 + +| Directory | Frequency | Latest Backup | Previous Backup | Size Comparison | Status | +| :--- | :--- | :--- | :--- | :--- | :--- | +| **ayudarg** | Weekly | `alldb_backup-2.sql.gz`
(Feb 08, 868.7 KB) | `alldb_backup-1.sql.gz`
(Feb 01, 868.7 KB) | Diff: 2 bytes (~0%) | **OK** | +| **glpi** | Daily | `alldb_backup-09.sql.gz`
(Feb 09, 32.9 MB) | `alldb_backup-08.sql.gz`
(Feb 08, 32.8 MB) | Diff: ~89 KB (~0.2%) | **OK** | +| **invop** | Weekly | `alldb_backup-2.sql.gz`
(Feb 08, 1.41 MB) | `alldb_backup-1.sql.gz`
(Feb 01, 1.41 MB) | Diff: 1 byte (~0%) | **OK** | +| **kanboard** | Daily | `alldb_backup-09.sql.gz`
(Feb 09, 2.97 MB) | `alldb_backup-08.sql.gz`
(Feb 08, 2.92 MB) | Diff: ~45 KB (~1.5%) | **OK** | +| **mon** | Weekly | `alldb_backup-2.sql.gz`
(Feb 08, 33.6 MB) | `alldb_backup-1.sql.gz`
(Feb 01, 33.7 MB) | Diff: ~117 KB (~0.3%) | **OK** | +| **moodle** | Daily | `full-backup-09.sql.gz`
(Feb 09, 654 MB) | `full-backup-08.sql.gz`
(Feb 08, 653 MB) | Diff: ~234 KB (~0.03%) | **OK** | +| **mygis** | Weekly | `full-backup-2.dump`
(Feb 08, 314 KB) | `full-backup-1.dump`
(Feb 01, 314 KB) | Diff: 0 bytes (0%) | **OK** | +| **ocular-vet** | Weekly | `backup-OcularVetDB-2.dump`
(Feb 08, 251 KB) | `backup-OcularVetDB-1.dump`
(Feb 01, 251 KB) | Diff: ~623 bytes (0.2%) | **OK** | +| **patogis** | Weekly | `full-backup-2.dump`
(Feb 08, 438 KB) | `full-backup-1.dump`
(Feb 01, 438 KB) | Diff: 0 bytes (0%) | **OK** | +| **piexa** | Daily | `full-backup-09.sql.gz`
(Feb 09, 162 MB) | `full-backup-08.sql.gz`
(Feb 08, 163 MB) | Diff: ~2.2 MB (~0.1%) | **OK** | +| **web** | Daily | `wordpress_backup-09.sql.gz`
(Feb 09, 23.8 MB) | `wordpress_backup-08.sql.gz`
(Feb 08, 23.8 MB) | Diff: ~12 KB (~0.05%) | **OK** | +| **yact** | Weekly | `backup-YactDB-2.gz`
(Feb 08, 2.14 MB) | `backup-YactDB-1.gz`
(Feb 01, 2.14 MB) | Diff: 0 bytes (0%) | **OK** | + +#### Summary +All analyzed directories show consistent backup sizes and up-to-date timestamps (Feb 08 for weekly, Feb 09 for daily). No errors were detected. + +## Using Gemini CLI + +[Gemini CLI](https://github.com/google-gemini/gemini-cli/) +is an open-source AI agent that brings the power of Gemini directly +into your terminal. It provides lightweight access to Gemini, giving you the +most direct path from your prompt to our model. + +Using this sample settings.json file at ~/.gemini/ directory: + +```json +{ + "mcpServers": { + "qnap": { + "command": "npx", + "args": [ + "-y", + "@marcelo-ochoa/server-qnap", + "http://10.1.1.241:8080" + ], + "env": { + "QNAP_USER": "admin", + "QNAP_PASSWORD": "password" + } + } + }, + "security": { + "auth": { + "selectedType": "gemini-api-key" + } + }, + "ui": { + "theme": "ANSI" + }, + "selectedAuthType": "gemini-api-key", + "theme": "Dracula" +} +``` + +### Sample prompts with Gemini CLI + +- qnap-connect to http://10.1.1.241:8080 using admin as user and password as password using qnap mcp server +- qnap-report +- analyze backup files in several subdirectories of `/backup/stacks` on the QNAP NAS. For each subdirectory compare the two most recent backups (daily or weekly, identified by filenames like `alldb_backup-2.sql.gz` for weekly and `alldb_backup-09.sql.gz` for daily). The comparison should be based on file size, considering backups with similar sizes as correct. Generate a report indicating the status (OK or Error) for each directory based on this comparison. + +### qnap-report Output Example + +The `qnap-report` tool now returns a professionally formatted Markdown report with tabular data, making it easy for both humans and AI models to read. + +# QNAP System Report +**Timestamp:** 2/12/2026, 12:21 PM +**Host:** http://10.254.0.158:8080 + +## Resource Usage +- **CPU Usage:** 7.9 % +- **Memory:** 1262MB / 4075MB (31.0% used) +- **Uptime:** 29 days, 2 hours, 3 minutes +- **System Temperature:** 34°C + +## Disk Health +| Alias | Model | Serial | Capacity | Health | Temperature | +| :--- | :--- | :--- | :--- | :--- | :--- | +| 3.5" SATA HDD 1 | WD40EFAX-68JH4N1 | WD-WXV2A8296XZ5 | 3.64 TB | OK | 35°C | +| 3.5" SATA HDD 2 | WD40EFAX-68JH4N1 | WD-WXW2A82FPRD1 | 3.64 TB | OK | 34°C | +| ... | ... | ... | ... | ... | ... | + +## Storage Information +| Name | Total | Used | Free | Usage | +| :--- | :--- | :--- | :--- | :--- | +| System | 503.32 GB | 27.14 GB | 476.18 GB | 5.4% | +| Docker | 793.05 GB | 258.59 GB | 534.46 GB | 32.6% | +| ... | ... | ... | ... | ... | ... | + +## Using Antigravity Code Editor + +Put this in `~/.gemini/antigravity/mcp_config.json` + +```json +{ + "mcpServers": { + "qnap": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "QNAP_USER=admin", + "-e", + "QNAP_PASSWORD=password", + "mochoa/mcp-qnap", + "http://10.1.1.241:8080" + ] + } + } +} +``` diff --git a/src/qnap/Dockerfile b/src/qnap/Dockerfile new file mode 100644 index 0000000000..af2c81cf15 --- /dev/null +++ b/src/qnap/Dockerfile @@ -0,0 +1,30 @@ +FROM node:slim AS builder + +COPY src/qnap /app +COPY tsconfig.json /tsconfig.json + +WORKDIR /app + +RUN --mount=type=cache,target=/root/.npm npm install + +RUN npm run build + +RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev + +FROM dhi.io/node:26-alpine-sfw-ent-dev AS release + +# Update and upgrade to fix OS-level vulnerabilities +RUN apk update && apk upgrade --no-cache + +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 /usr/bin/npm ci --ignore-scripts --omit-dev + +ENTRYPOINT ["node", "dist/index.js"] + diff --git a/src/qnap/LICENSE b/src/qnap/LICENSE new file mode 100644 index 0000000000..4a93985763 --- /dev/null +++ b/src/qnap/LICENSE @@ -0,0 +1,216 @@ +The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0. + +Contributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License. + +No rights beyond those granted by the applicable original license are conveyed for such contributions. + +--- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright + owner or by an individual or Legal Entity authorized to submit on behalf + of the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +--- + +MIT License + +Copyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +Creative Commons Attribution 4.0 International (CC-BY-4.0) + +Documentation in this project (excluding specifications) is licensed under +CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for +the full license text. diff --git a/src/qnap/README.md b/src/qnap/README.md new file mode 100644 index 0000000000..2f3d935baa --- /dev/null +++ b/src/qnap/README.md @@ -0,0 +1,123 @@ +# QNAP MCP Server + +An MCP server implementation for QNAP NAS devices, providing tools to monitor system status, manage files, and generate reports. + +## Tools + +- `qnap-connect`: Connect to a QNAP NAS and obtain a session ID. + - `host`: The QNAP NAS URL (e.g., `http://10.1.1.241:8080`). + - `username`: Your admin username. + - `password`: Your admin password. +- `qnap-report`: Generate a comprehensive **Markdown system report** including CPU, memory, powered by `toon` tables for disks and volumes. Perfect for both human reading and AI analysis. +- `qnap-dir`: List the contents of a directory in a **professional tabular format**, automatically sorted by modification date. + - `path`: The path to list (e.g., `/Public`). +- `qnap-file-info`: Get detailed information about a specific file. + - `path`: The directory path. + - `filename`: The name of the file. + +## Resources + +The server exposes QNAP system components as MCP Resources, providing structured JSON data for monitoring: + +- **Disks**: `qnap://[host]:[port]/disk/[disk-id]` + - Real-time disk health, model, serial, and temperature. +- **Volumes**: `qnap://[host]:[port]/volume/[volume-id]` + - Detailed storage usage, capacity, and volume names. + +These resources allow AI models to monitor NAS health and storage levels without manually calling reporting tools. + +## Configuration + +### Environment Variables + +The server can use environment variables and startup arguments for automatic connection: + +- **`QNAP_USER`**: QNAP admin username. +- **`QNAP_PASSWORD`**: QNAP admin password. + +### Startup Arguments + +1. **`host`**: (Optional) URL of the QNAP NAS (e.g., `http://10.1.1.241:8080`). + +If the host and environment variables are provided, the server will attempt to connect automatically at startup. + +### Usage with Claude Desktop + +Add the following to your `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "qnap": { + "command": "npx", + "args": [ + "-y", + "@marcelo-ochoa/server-qnap", + "http://10.1.1.241:8080" + ], + "env": { + "QNAP_USER": "admin", + "QNAP_PASSWORD": "password" + } + } + } +} +``` + +using docker image + +```json +{ + "mcpServers": { + "qnap": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-e", + "QNAP_USER=admin", + "-e", + "QNAP_PASSWORD=password", + "mochoa/mcp-qnap", + "http://10.1.1.241:8080" + ] + } + } +} +``` + +## Development + +```bash +cd src/qnap +npm install +npm run build +``` + + +## Demos + +See [Demos](https://github.com/marcelo-ochoa/servers/blob/main/src/qnap/Demos.md) for usage examples with Claude Desktop, Gemini CLI, and Antigravity Code Editor. + +## Docker + +Building the container: + +```bash +docker build -t mochoa/mcp-qnap -f src/qnap/Dockerfile . +``` + +Running the container: + +```bash +docker run -i --rm -e QNAP_USER=admin -e QNAP_PASSWORD=password mochoa/mcp-qnap http://10.1.1.241:8080 +``` + +## Change Log + +See [Change Log](https://github.com/marcelo-ochoa/servers/blob/main/src/qnap/CHANGELOG.md) for the history of changes. + +## 📜 License + +This project is licensed under the Apache License, Version 2.0 for new contributions, with existing code under MIT - see the [LICENSE](https://github.com/marcelo-ochoa/servers/blob/main/src/qnap/LICENSE) file for details. diff --git a/src/qnap/REFACTORING.md b/src/qnap/REFACTORING.md new file mode 100644 index 0000000000..e604610674 --- /dev/null +++ b/src/qnap/REFACTORING.md @@ -0,0 +1,139 @@ +# QNAP MCP Server Refactoring Summary + +## Overview +The QNAP MCP server has been refactored to follow the same architectural patterns as the MySQL MCP server, improving code organization, maintainability, and readability. + +## Changes Made + +### 1. Tool Handlers Separation (`tools/` directory) +Created individual handler files for each tool, following the single responsibility principle: + +- **`tools/connect.ts`**: Handles QNAP NAS connection and authentication + - Manages connection state (host and session ID) + - Provides helper functions: `getNasHost()`, `getNasSid()`, `setNasConnection()`, `clearNasConnection()` + - Exports `fetchWithTimeout()` for use by other handlers + - Implements `connectHandler()` for the `qnap-connect` tool + - Implements `initializeApi()` for programmatic connection + +- **`tools/report.ts`**: Generates QNAP system reports + - Implements `reportHandler()` for the `qnap-report` tool + - Fetches system information and formats connection details + +- **`tools/dir.ts`**: Lists directory contents + - Implements `dirHandler()` for the `qnap-dir` tool + - Handles directory listing via QNAP file manager API + +- **`tools/file_info.ts`**: Retrieves file information + - Implements `fileInfoHandler()` for the `qnap-file-info` tool + - Fetches detailed file metadata + +### 2. Handler Dispatcher Pattern (`handlers.ts`) +Simplified the handlers file to act as a clean dispatcher: + +```typescript +const toolHandlers: Record Promise> = { + "qnap-connect": connectHandler, + "qnap-report": reportHandler, + "qnap-dir": dirHandler, + "qnap-file-info": fileInfoHandler, +}; + +export const callToolHandler = async (request: CallToolRequest) => { + const handler = toolHandlers[request.params.name]; + if (handler) { + return handler(request); + } + throw new Error(`Unknown tool: ${request.params.name}`); +}; +``` + +### 3. Tool Definitions (`tools.ts`) +Refactored to use a simple array-based structure matching MySQL pattern: + +**Before:** +```typescript +export const QNAP_CONNECT_TOOL: Tool = { ... }; +export const QNAP_REPORT_TOOL: Tool = { ... }; +export const TOOLS = [QNAP_CONNECT_TOOL, QNAP_REPORT_TOOL, ...]; +``` + +**After:** +```typescript +export const tools = [ + { name: "qnap-connect", description: "...", inputSchema: {...} }, + { name: "qnap-report", description: "...", inputSchema: {...} }, + ... +]; +``` + +### 4. Server Updates (`server.ts`) +Updated to use the new dispatcher pattern: + +**Before:** +```typescript +switch (request.params.name) { + case "qnap-connect": + return await handleConnect(request.params.arguments); + case "qnap-report": + return await handleReport(); + ... +} +``` + +**After:** +```typescript +return await callToolHandler(request); +``` + +## Benefits + +1. **Better Code Organization**: Each tool has its own file with focused responsibility +2. **Improved Maintainability**: Changes to one tool don't affect others +3. **Consistent Patterns**: Follows the same structure as MySQL MCP server +4. **Enhanced Readability**: Smaller, focused files are easier to understand +5. **Better Error Handling**: Each handler validates its own inputs +6. **Easier Testing**: Individual handlers can be tested in isolation +7. **Scalability**: Adding new tools is straightforward - just create a new handler file and register it + +## File Structure Comparison + +### Before: +``` +src/qnap/ +├── handlers.ts (183 lines - all logic inline) +├── tools.ts (58 lines - individual constants) +├── server.ts (84 lines - switch statement) +└── tools/ + └── qnap_report_collector.py +``` + +### After: +``` +src/qnap/ +├── handlers.ts (24 lines - clean dispatcher) +├── tools.ts (66 lines - array-based) +├── server.ts (70 lines - uses dispatcher) +└── tools/ + ├── connect.ts (connection logic + state management) + ├── report.ts (report generation) + ├── dir.ts (directory listing) + ├── file_info.ts (file information) + └── qnap_report_collector.py +``` + +## Migration Notes + +- All existing functionality is preserved +- No breaking changes to the API +- Connection state is now managed through helper functions +- Each handler validates its own inputs and returns consistent error structures +- Build process remains unchanged (`npm run build`) + +## Next Steps + +Consider these future enhancements: +1. Add TypeScript interfaces for QNAP API responses +2. Implement unit tests for individual handlers +3. Add more comprehensive error messages +4. Consider adding retry logic for network operations +5. Add logging/debugging capabilities diff --git a/src/qnap/handlers.ts b/src/qnap/handlers.ts new file mode 100644 index 0000000000..0e4119b8bc --- /dev/null +++ b/src/qnap/handlers.ts @@ -0,0 +1,24 @@ +import { CallToolRequest, ListResourcesRequest, ReadResourceRequest } from "@modelcontextprotocol/sdk/types.js"; +import { connectHandler, initializeApi } from "./tools/connect.js"; +import { reportHandler } from "./tools/report.js"; +import { dirHandler } from "./tools/dir.js"; +import { fileInfoHandler } from "./tools/file_info.js"; +import { listResourcesHandler, readResourceHandler } from "./resources.js"; + +const toolHandlers: Record Promise> = { + "qnap-connect": connectHandler, + "qnap-report": reportHandler, + "qnap-dir": dirHandler, + "qnap-file-info": fileInfoHandler, +}; + +export const callToolHandler = async (request: CallToolRequest) => { + const handler = toolHandlers[request.params.name]; + if (handler) { + return handler(request); + } + throw new Error(`Unknown tool: ${request.params.name}`); +}; + +export { initializeApi, listResourcesHandler, readResourceHandler }; + diff --git a/src/qnap/index.ts b/src/qnap/index.ts new file mode 100644 index 0000000000..30ecf76315 --- /dev/null +++ b/src/qnap/index.ts @@ -0,0 +1,8 @@ +#!/usr/bin/env node +import { QnapServer } from "./server.js"; + +const server = new QnapServer(); +server.run().catch((error) => { + console.error("Fatal error running server:", error); + process.exit(1); +}); diff --git a/src/qnap/package.json b/src/qnap/package.json new file mode 100644 index 0000000000..8e0b8911fc --- /dev/null +++ b/src/qnap/package.json @@ -0,0 +1,42 @@ +{ + "name": "@marcelo-ochoa/server-qnap", + "mcpName": "io.github.marcelo-ochoa/qnap", + "version": "1.0.8", + "repository": { + "type": "git", + "url": "https://github.com/marcelo-ochoa/servers.git", + "subfolder": "src/qnap" + }, + "description": "An MCP server for QNAP NAS API.", + "keywords": [ + "read-only-mcp", + "qnap", + "nas", + "ai-agent", + "llm-tool", + "rag" + ], + "license": "MIT", + "author": "Marcelo Fabian Ochoa", + "homepage": "https://modelcontextprotocol.io", + "bugs": "https://github.com/marcelo-ochoa/servers/issues", + "type": "module", + "bin": { + "mcp-server-qnap": "dist/index.js" + }, + "main": "dist/index.js", + "scripts": { + "build": "tsc && shx chmod +x dist/*.js", + "prepare": "npm run build", + "watch": "tsc --watch" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.24.2", + "@toon-format/toon": "^1.0.10" + }, + "devDependencies": { + "@types/node": "^22", + "shx": "^0.3.4", + "typescript": "^5.6.2" + } +} \ No newline at end of file diff --git a/src/qnap/resources.ts b/src/qnap/resources.ts new file mode 100644 index 0000000000..ddd09490b4 --- /dev/null +++ b/src/qnap/resources.ts @@ -0,0 +1,130 @@ +import { ListResourcesRequest, ReadResourceRequest, McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; +import { getNasHost, getNasSid, fetchWithTimeout } from "./tools/connect.js"; +import { parseDiskHealth, parseStorageInfo } from "./tools/report.js"; + +const getHostPort = (host: string): string => { + return host.replace(/^https?:\/\//, ''); +}; + +export const listResourcesHandler = async (request: ListResourcesRequest) => { + const host = getNasHost(); + const sid = getNasSid(); + + if (!host || !sid) { + return { resources: [] }; + } + + const hostPort = getHostPort(host); + + try { + // 1. Fetch Disk Info + const diskParams = new URLSearchParams({ + func: 'all_hd_data', + sid: sid + }); + const diskUrl = `${host}/cgi-bin/disk/qsmart.cgi?${diskParams.toString()}`; + const diskResp = await fetchWithTimeout(diskUrl); + const diskXml = await diskResp.text(); + const disks = parseDiskHealth(diskXml); + + // 2. Fetch Storage Info + const storageParams = new URLSearchParams({ + chart_func: 'disk_usage', + disk_select: 'all', + include: 'all', + sid: sid + }); + const storageUrl = `${host}/cgi-bin/management/chartReq.cgi?${storageParams.toString()}`; + const storageResp = await fetchWithTimeout(storageUrl); + const storageXml = await storageResp.text(); + const storageInfo = parseStorageInfo(storageXml); + + const resources: any[] = []; + + // Disks + disks.forEach((d) => { + const diskId = encodeURIComponent(d.Alias); + resources.push({ + uri: `qnap://${hostPort}/disk/${diskId}`, + mimeType: "application/json", + name: `Disk ${d.Alias}`, + description: `QNAP Disk ${d.Alias} (${d.Model}) - Health: ${d.Health}`, + }); + }); + + // Volumes + storageInfo.forEach((v) => { + const volumeId = encodeURIComponent(v.Id || v.Name); + resources.push({ + uri: `qnap://${hostPort}/volume/${volumeId}`, + mimeType: "application/json", + name: `Volume ${v.Name}`, + description: `QNAP Volume ${v.Name} - Usage: ${v.Usage}`, + }); + }); + + return { resources }; + } catch (error: any) { + throw new Error(`Error listing resources: ${error.message}`); + } +}; + +export const readResourceHandler = async (request: ReadResourceRequest) => { + const { uri } = request.params; + const host = getNasHost(); + const sid = getNasSid(); + + if (!host || !sid) { + throw new McpError(ErrorCode.InvalidRequest, "Not connected to QNAP. Use qnap-connect first."); + } + + const hostPort = getHostPort(host); + + try { + // Handle disk resource + const diskMatch = uri.match(/^qnap:\/\/[^\/]+\/disk\/(.+)$/); + if (diskMatch) { + const alias = decodeURIComponent(diskMatch[1]); + const diskParams = new URLSearchParams({ + func: 'all_hd_data', + sid: sid + }); + const diskUrl = `${host}/cgi-bin/disk/qsmart.cgi?${diskParams.toString()}`; + const diskResp = await fetchWithTimeout(diskUrl); + const diskXml = await diskResp.text(); + const disks = parseDiskHealth(diskXml); + const item = disks.find(d => d.Alias === alias); + + if (!item) throw new Error(`Disk not found: ${alias}`); + return { + contents: [{ uri, mimeType: "application/json", text: JSON.stringify(item, null, 2) }], + }; + } + + // Handle volume resource + const volumeMatch = uri.match(/^qnap:\/\/[^\/]+\/volume\/(.+)$/); + if (volumeMatch) { + const idOrName = decodeURIComponent(volumeMatch[1]); + const storageParams = new URLSearchParams({ + chart_func: 'disk_usage', + disk_select: 'all', + include: 'all', + sid: sid + }); + const storageUrl = `${host}/cgi-bin/management/chartReq.cgi?${storageParams.toString()}`; + const storageResp = await fetchWithTimeout(storageUrl); + const storageXml = await storageResp.text(); + const storageInfo = parseStorageInfo(storageXml); + const item = storageInfo.find(v => v.Id === idOrName || v.Name === idOrName); + + if (!item) throw new Error(`Volume not found: ${idOrName}`); + return { + contents: [{ uri, mimeType: "application/json", text: JSON.stringify(item, null, 2) }], + }; + } + + throw new Error(`Invalid resource URI: ${uri}`); + } catch (error: any) { + throw new Error(`Error reading resource: ${error.message}`); + } +}; diff --git a/src/qnap/server.json b/src/qnap/server.json new file mode 100644 index 0000000000..1a5a13ae8f --- /dev/null +++ b/src/qnap/server.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.marcelo-ochoa/qnap", + "description": "MCP server for QNAP NAS API", + "repository": { + "url": "https://github.com/marcelo-ochoa/servers", + "source": "github", + "subfolder": "src/qnap" + }, + "version": "1.0.8", + "packages": [ + { + "registryType": "npm", + "identifier": "@marcelo-ochoa/server-qnap", + "version": "1.0.8", + "transport": { + "type": "stdio" + } + } + ] +} \ No newline at end of file diff --git a/src/qnap/server.ts b/src/qnap/server.ts new file mode 100644 index 0000000000..0cf1768368 --- /dev/null +++ b/src/qnap/server.ts @@ -0,0 +1,124 @@ +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import { tools } from "./tools.js"; +import { callToolHandler, listResourcesHandler, readResourceHandler, initializeApi } from "./handlers.js"; + +const server = new McpServer({ + name: "qnap-mcp-server", + version: "1.0.8", +}); + +const promptsData = [ + { name: "qnap-connect: Connect to QNAP NAS", description: "connect to QNAP NAS using host, username and password" }, + { name: "qnap-report: System Report", description: "show a comprehensive system report with CPU, memory, and disk status" }, + { name: "qnap-dir: List Directory", description: "list contents of a directory on the QNAP NAS" }, + { name: "qnap-file-info: File Information", description: "get detailed information about a specific file" } +]; + +// Register Prompts +server.registerPrompt("qnap-prompts", { + description: "List available QNAP prompts" +}, async () => ({ + messages: [ + { + role: "assistant", + content: { + type: "text", + text: "Available QNAP prompts:\n" + promptsData.map(p => `- ${p.name}: ${p.description}`).join("\n") + } + } + ] +})); + +// Register Resource Templates +const diskTemplate = new ResourceTemplate("qnap://{host}/disk/{id}", { + list: async () => listResourcesHandler({} as any) +}); +const volumeTemplate = new ResourceTemplate("qnap://{host}/volume/{id}", { + list: async () => listResourcesHandler({} as any) +}); + +server.registerResource( + "Disk Info", + diskTemplate, + { description: "Information about a specific disk on the QNAP NAS" }, + async (uri: URL) => { + return readResourceHandler({ params: { uri: uri.toString() } } as any); + } +); + +server.registerResource( + "Volume Info", + volumeTemplate, + { description: "Information about a specific volume on the QNAP NAS" }, + async (uri: URL) => { + return readResourceHandler({ params: { uri: uri.toString() } } as any); + } +); + +// Register Tools +tools.forEach((tool: any) => { + // Basic mapping of JSON schema to Zod for simple cases + let inputSchema: any = z.object({}); + if (tool.inputSchema && tool.inputSchema.properties) { + const shape: Record = {}; + for (const [key, prop] of Object.entries(tool.inputSchema.properties)) { + let field: any = z.any(); + if ((prop as any).type === "string") { + field = z.string(); + } + + if ((prop as any).description) { + field = field.describe((prop as any).description); + } + + if (tool.inputSchema.required && !(tool.inputSchema.required as string[]).includes(key)) { + field = field.optional(); + } else if (!tool.inputSchema.required) { + field = field.optional(); + } + shape[key] = field; + } + inputSchema = z.object(shape); + } + + server.registerTool( + tool.name, + { + description: tool.description, + inputSchema: inputSchema + }, + async (args: any) => { + return callToolHandler({ params: { name: tool.name, arguments: args } } as any); + } + ); +}); + +export class QnapServer { + async run() { + const args = process.argv.slice(2); + const host = args[0]; + + if (host) { + try { + await initializeApi(host); + console.error(`Automatically connected to QNAP NAS at ${host}`); + } catch (error: any) { + console.error(`Failed to automatically connect to QNAP NAS: ${error.message}`); + } + } else { + console.error("Warning: No QNAP host provided as argument. Use qnap-connect tool before using other functionality."); + } + + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("QNAP MCP server running on stdio"); + + process.stdin.on("close", () => { + console.error("QNAP MCP Server closed"); + server.close(); + process.exit(0); + }); + } +} diff --git a/src/qnap/tools.ts b/src/qnap/tools.ts new file mode 100644 index 0000000000..df9afa916b --- /dev/null +++ b/src/qnap/tools.ts @@ -0,0 +1,65 @@ +export const tools = [ + { + name: "qnap-connect", + description: "Connect to a QNAP NAS and obtain a session ID.", + inputSchema: { + type: "object", + properties: { + host: { + type: "string", + description: "QNAP NAS host (e.g., http://10.1.1.241:8080)" + }, + username: { + type: "string", + description: "Username" + }, + password: { + type: "string", + description: "Password" + } + }, + required: ["host", "username", "password"] + } + }, + { + name: "qnap-report", + description: "Generate a QNAP system report including CPU, memory, disks and volume status.", + inputSchema: { + type: "object", + properties: {}, + required: [] + } + }, + { + name: "qnap-dir", + description: "List contents of a directory on the QNAP NAS.", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: "Directory path (e.g., /Public)" + } + }, + required: ["path"] + } + }, + { + name: "qnap-file-info", + description: "Get detailed information about a file on the QNAP NAS.", + inputSchema: { + type: "object", + properties: { + path: { + type: "string", + description: "Directory path where the file is located" + }, + filename: { + type: "string", + description: "Name of the file" + } + }, + required: ["path", "filename"] + } + } +]; diff --git a/src/qnap/tools/connect.ts b/src/qnap/tools/connect.ts new file mode 100644 index 0000000000..91a9591de8 --- /dev/null +++ b/src/qnap/tools/connect.ts @@ -0,0 +1,130 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; + +let nas_host: string | null = null; +let nas_sid: string | null = null; + +export function getNasHost(): string | null { + return nas_host; +} + +export function getNasSid(): string | null { + return nas_sid; +} + +export function setNasConnection(host: string, sid: string): void { + nas_host = host; + nas_sid = sid; +} + +export function clearNasConnection(): void { + nas_host = null; + nas_sid = null; +} + +export async function initializeApi(host: string, username?: string, password?: string) { + const user = username || process.env.QNAP_USER; + const pwd = password || process.env.QNAP_PASSWORD; + + if (!user || !pwd) { + throw new Error("Credentials not provided and QNAP_USER/QNAP_PASSWORD env variables not set."); + } + + // Directly perform the connection logic + const b64_pwd = Buffer.from(pwd).toString('base64'); + const url = `${host}/cgi-bin/authLogin.cgi?user=${user}&pwd=${b64_pwd}`; + + try { + const response = await fetchWithTimeout(url); + const text = await response.text(); + + // Extract SID using regex + const sidMatch = text.match(/<\/authSid>/); + const sid = sidMatch ? sidMatch[1] : null; + + if (sid) { + setNasConnection(host, sid); + } else { + throw new Error(`Login failed. Response: ${text}`); + } + } catch (error: any) { + throw new Error(`Error connecting to QNAP: ${error.message}`); + } +} + +async function fetchWithTimeout(url: string, options: any = {}, timeout = 60000) { + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeout); + + const headers = { + ...options.headers, + 'Referer': nas_host ? `${nas_host}/cgi-bin/index.cgi` : '', + }; + + if (nas_sid) { + (headers as any)['Cookie'] = `NAS_SID=${nas_sid}`; + } + + try { + const response = await fetch(url, { + ...options, + headers, + signal: controller.signal + }); + clearTimeout(id); + return response; + } catch (error) { + clearTimeout(id); + throw error; + } +} + +export async function connectHandler(request: CallToolRequest) { + const { host, username, password } = request.params.arguments || {}; + + if ( + typeof host !== "string" || !host || + typeof username !== "string" || !username || + typeof password !== "string" || !password + ) { + return { + content: [{ type: "text", text: "Missing or invalid host, username, or password argument." }], + isError: true, + }; + } + + const b64_pwd = Buffer.from(password).toString('base64'); + const params = new URLSearchParams({ + user: username, + pwd: b64_pwd + }); + const url = `${host}/cgi-bin/authLogin.cgi?${params.toString()}`; + + try { + const response = await fetchWithTimeout(url); + const text = await response.text(); + + // Extract SID using regex + const sidMatch = text.match(/<\/authSid>/); + const sid = sidMatch ? sidMatch[1] : null; + + if (sid) { + setNasConnection(host, sid); + return { + content: [{ type: "text", text: `Connected successfully to ${host}.` }], + isError: false, + }; + } else { + return { + content: [{ type: "text", text: "Login failed. Please check your credentials." }], + isError: true + }; + } + } catch (error: any) { + return { + content: [{ type: "text", text: "Error connecting to QNAP NAS. Please verify the host and network connectivity." }], + isError: true + }; + } +} + +export { fetchWithTimeout }; diff --git a/src/qnap/tools/dir.ts b/src/qnap/tools/dir.ts new file mode 100644 index 0000000000..59aa4ee6c6 --- /dev/null +++ b/src/qnap/tools/dir.ts @@ -0,0 +1,80 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { encode } from "@toon-format/toon"; +import { getNasHost, getNasSid, fetchWithTimeout } from "./connect.js"; + +export async function dirHandler(request: CallToolRequest) { + const { path } = request.params.arguments || {}; + const nas_host = getNasHost(); + const nas_sid = getNasSid(); + + if (!nas_host || !nas_sid) { + return { + content: [{ type: "text", text: "Not connected to QNAP. Use qnap-connect first." }], + isError: true + }; + } + + if (typeof path !== "string" || !path) { + return { + content: [{ type: "text", text: "Missing or invalid path argument." }], + isError: true, + }; + } + + try { + const params = new URLSearchParams({ + func: 'get_list', + path: path, + sid: nas_sid, + limit: '100', + start: '0' + }); + const url = `${nas_host}/cgi-bin/filemanager/utilRequest.cgi?${params.toString()}`; + const response = await fetchWithTimeout(url); + const data = await response.json() as any; + + const files = Array.isArray(data.datas) ? data.datas : []; + const sortedFiles = files.sort((a: any, b: any) => { + const timeA = parseInt(a.filestamp || a.epochmt || "0"); + const timeB = parseInt(b.filestamp || b.epochmt || "0"); + return timeB - timeA; + }); + + const formattedFiles = sortedFiles.map((file: any) => { + const isDir = file.isfolder === 1 || file.isfolder === "1" || file.is_dir === "1" || file.is_dir === 1; + const sizeInBytes = parseInt(file.filesize || "0"); + let size = "-"; + if (!isDir) { + if (sizeInBytes > 1024 * 1024 * 1024) { + size = (sizeInBytes / (1024 * 1024 * 1024)).toFixed(2) + " GB"; + } else if (sizeInBytes > 1024 * 1024) { + size = (sizeInBytes / (1024 * 1024)).toFixed(2) + " MB"; + } else { + size = (sizeInBytes / 1024).toFixed(2) + " KB"; + } + } + const timestamp = file.filestamp || file.epochmt; + const modified = timestamp ? new Date(parseInt(timestamp) * 1000).toLocaleString() : "-"; + + return { + Name: file.filename, + Type: isDir ? "DIR" : "FILE", + Size: size, + Modified: modified, + Owner: file.owner || "-", + Group: file.group || "-", + Permissions: file.privilege || "-" + }; + }); + + return { + content: [{ type: "text", text: encode(formattedFiles) }], + isError: false, + }; + } catch (error: any) { + return { + content: [{ type: "text", text: `Error listing directory: ${error.message}` }], + isError: true + }; + } +} diff --git a/src/qnap/tools/file_info.ts b/src/qnap/tools/file_info.ts new file mode 100644 index 0000000000..57af111627 --- /dev/null +++ b/src/qnap/tools/file_info.ts @@ -0,0 +1,45 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { getNasHost, getNasSid, fetchWithTimeout } from "./connect.js"; + +export async function fileInfoHandler(request: CallToolRequest) { + const { path, filename } = request.params.arguments || {}; + const nas_host = getNasHost(); + const nas_sid = getNasSid(); + + if (!nas_host || !nas_sid) { + return { + content: [{ type: "text", text: "Not connected to QNAP. Use qnap-connect first." }], + isError: true + }; + } + + if (typeof path !== "string" || !path || typeof filename !== "string" || !filename) { + return { + content: [{ type: "text", text: "Missing or invalid path or filename argument." }], + isError: true, + }; + } + + try { + const params = new URLSearchParams({ + func: 'stat', + sid: nas_sid, + path: path, + file_total: '1', + file_name: filename + }); + const url = `${nas_host}/cgi-bin/filemanager/utilRequest.cgi?${params.toString()}`; + const response = await fetchWithTimeout(url); + const text = await response.text(); + + return { + content: [{ type: "text", text: text }], + isError: false, + }; + } catch (error: any) { + return { + content: [{ type: "text", text: `Error getting file info: ${error.message}` }], + isError: true + }; + } +} diff --git a/src/qnap/tools/report.ts b/src/qnap/tools/report.ts new file mode 100644 index 0000000000..97a4a25f3e --- /dev/null +++ b/src/qnap/tools/report.ts @@ -0,0 +1,209 @@ +import { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { encode } from "@toon-format/toon"; +import { getNasHost, getNasSid, fetchWithTimeout } from "./connect.js"; + +/** + * Parses disk health information from QNAP XML response. + */ +export function parseDiskHealth(xml: string): any[] { + const disks: any[] = []; + const entryRegex = /(.*?)<\/entry>/gs; + let match; + + while ((match = entryRegex.exec(xml)) !== null) { + const entry = match[1]; + + const alias = entry.match(/<\/Disk_Alias>/)?.[1] || ""; + const health = entry.match(/<\/Health>/)?.[1] || ""; + const capacity = entry.match(/<\/Capacity>/)?.[1] || ""; + const tempC = entry.match(/<\/oC>/)?.[1] || ""; + const model = entry.match(/<\/Model>/)?.[1] || ""; + const serial = entry.match(/<\/Serial>/)?.[1] || ""; + + if (alias) { + disks.push({ + Alias: alias, + Model: model || "-", + Serial: serial || "-", + Capacity: capacity || "-", + Health: health || "Unknown", + Temperature: tempC ? `${tempC}°C` : "-" + }); + } + } + return disks; +} + +/** + * Parses resource usage information from QNAP XML response. + */ +export function parseResourceUsage(xml: string): any { + const cpuUsage = xml.match(/<\/cpu_usage>/)?.[1]?.trim() || ""; + const memTotalStr = xml.match(/<\/total_memory>/)?.[1]; + const memFreeStr = xml.match(/<\/free_memory>/)?.[1]; + + let memory: any = null; + if (memTotalStr && memFreeStr) { + const total = parseFloat(memTotalStr); + const free = parseFloat(memFreeStr); + const used = total - free; + const usedPct = total > 0 ? (used / total) * 100 : 0; + memory = { + totalMB: total, + freeMB: free, + usedMB: used, + usedPercent: usedPct.toFixed(1) + "%" + }; + } + + const day = xml.match(/<\/uptime_day>/)?.[1]; + const hour = xml.match(/<\/uptime_hour>/)?.[1]; + const min = xml.match(/<\/uptime_min>/)?.[1]; + + let uptime = ""; + if (day !== undefined && hour !== undefined && min !== undefined) { + uptime = `${day} days, ${hour} hours, ${min} minutes`; + } + + const sysTemp = xml.match(/([^<]+)<\/sys_tempc>/)?.[1]; + + return { + cpuUsage, + memory, + uptime, + systemTemperature: sysTemp ? `${sysTemp}°C` : "" + }; +} + +/** + * Parses storage/volume information from QNAP XML response. + */ +export function parseStorageInfo(xml: string): any[] { + const volumes: any[] = []; + const volLabels: Record = {}; + const volRegex = /(.*?)<\/volume>/gs; + let volMatch; + while ((volMatch = volRegex.exec(xml)) !== null) { + const vol = volMatch[1]; + const val = vol.match(/<\/volumeValue>/)?.[1]; + const label = vol.match(/<\/volumeLabel>/)?.[1]; + if (val && label) { + volLabels[val] = label; + } + } + + const useRegex = /(.*?)<\/volumeUse>/gs; + let useMatch; + while ((useMatch = useRegex.exec(xml)) !== null) { + const volUse = useMatch[1]; + const val = volUse.match(/<\/volumeValue>/)?.[1]; + const totalStr = volUse.match(/<\/total_size>/)?.[1]; + const freeStr = volUse.match(/<\/free_size>/)?.[1]; + + if (val) { + const name = volLabels[val] || `Volume ${val}`; + let usage: any = { Id: val, Name: name }; + + if (totalStr && freeStr) { + try { + const total = parseFloat(totalStr); + const free = parseFloat(freeStr); + const used = total - free; + const usedPct = total > 0 ? (used / total) * 100 : 0; + + usage = { + ...usage, + Total: (total / (1024 ** 3)).toFixed(2) + " GB", + Used: (used / (1024 ** 3)).toFixed(2) + " GB", + Free: (free / (1024 ** 3)).toFixed(2) + " GB", + Usage: usedPct.toFixed(1) + "%" + }; + } catch { + usage = { ...usage, Total: totalStr, Free: freeStr }; + } + } + volumes.push(usage); + } + } + return volumes; +} + +/** + * Handle qnap-report tool call. + */ +export async function reportHandler(request: CallToolRequest) { + const host = getNasHost(); + const sid = getNasSid(); + + if (!host || !sid) { + return { + content: [{ type: "text", text: "Not connected to QNAP. Use qnap-connect first." }], + isError: true + }; + } + + try { + // 1. Disk Health + const diskParams = new URLSearchParams({ + func: 'all_hd_data', + sid: sid + }); + const diskUrl = `${host}/cgi-bin/disk/qsmart.cgi?${diskParams.toString()}`; + const diskResp = await fetchWithTimeout(diskUrl); + const diskXml = await diskResp.text(); + const diskHealth = parseDiskHealth(diskXml); + + // 2. Resource Usage + const resParams = new URLSearchParams({ + subfunc: 'sysinfo', + hd: 'no', + multicpu: '1', + sid: sid + }); + const resUrl = `${host}/cgi-bin/management/manaRequest.cgi?${resParams.toString()}`; + const resResp = await fetchWithTimeout(resUrl); + const resXml = await resResp.text(); + const resourceUsage = parseResourceUsage(resXml); + + // 3. Storage Info + const storageParams = new URLSearchParams({ + chart_func: 'disk_usage', + disk_select: 'all', + include: 'all', + sid: sid + }); + const storageUrl = `${host}/cgi-bin/management/chartReq.cgi?${storageParams.toString()}`; + const storageResp = await fetchWithTimeout(storageUrl); + const storageXml = await storageResp.text(); + const storageInfo = parseStorageInfo(storageXml); + + // Format the output as a readable Markdown report + let output = `# QNAP System Report\n`; + output += `**Timestamp:** ${new Date().toLocaleString()}\n`; + output += `**Host:** ${host}\n\n`; + + output += `## Resource Usage\n`; + output += `- **CPU Usage:** ${resourceUsage.cpuUsage}\n`; + if (resourceUsage.memory) { + output += `- **Memory:** ${resourceUsage.memory.usedMB.toFixed(0)}MB / ${resourceUsage.memory.totalMB.toFixed(0)}MB (${resourceUsage.memory.usedPercent} used)\n`; + } + output += `- **Uptime:** ${resourceUsage.uptime}\n`; + output += `- **System Temperature:** ${resourceUsage.systemTemperature}\n\n`; + + output += `## Disk Health\n`; + output += encode(diskHealth) + "\n\n"; + + output += `## Storage Information\n`; + output += encode(storageInfo); + + return { + content: [{ type: "text", text: output }], + isError: false, + }; + } catch (error: any) { + return { + content: [{ type: "text", text: `Error generating report: ${error.message}` }], + isError: true + }; + } +} diff --git a/src/qnap/tsconfig.json b/src/qnap/tsconfig.json new file mode 100644 index 0000000000..ba6e6beb5f --- /dev/null +++ b/src/qnap/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "strict": true, + "lib": [ + "ESNext" + ], + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "*.ts" + ] +} \ No newline at end of file