diff --git a/.gitignore b/.gitignore index 2b3b64d..3911121 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ build/ dist/ *.log .env* +.last-response.json diff --git a/Dockerfile b/Dockerfile index 0f21a6d..074a355 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ WORKDIR /usr/src/app COPY package*.json ./ # Install dependencies -RUN npm install --ignore-scripts +RUN npm ci --ignore-scripts # Copy the rest of the project files COPY . . diff --git a/README.md b/README.md index 81531ec..c385801 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![MseeP.ai Security Assessment Badge](https://mseep.net/pr/imlewc-metabase-server-badge.png)](https://mseep.ai/app/imlewc-metabase-server) + # metabase-server MCP Server [![smithery badge](https://smithery.ai/badge/@imlewc/metabase-server)](https://smithery.ai/server/@imlewc/metabase-server) @@ -27,15 +29,35 @@ This is a TypeScript-based MCP server that implements integration with Metabase ## Configuration -Before running the server, you need to set the following environment variables: +Before running the server, you need to set environment variables for authentication. The server supports two methods: + +1. **API Key (Preferred):** + * `METABASE_URL`: The URL of your Metabase instance (e.g., `https://your-metabase-instance.com`). + * `METABASE_API_KEY`: Your Metabase API key. + +2. **Username/Password (Fallback):** + * `METABASE_URL`: The URL of your Metabase instance. + * `METABASE_USERNAME`: Your Metabase username. + * `METABASE_PASSWORD`: Your Metabase password. +The server will first check for `METABASE_API_KEY`. If it's set, API key authentication will be used. If `METABASE_API_KEY` is not set, the server will fall back to using `METABASE_USERNAME` and `METABASE_PASSWORD`. You must provide credentials for at least one of these methods. + +**Example setup:** + +Using API Key: +```bash +# Required environment variables +export METABASE_URL=https://your-metabase-instance.com +export METABASE_API_KEY=your_metabase_api_key +``` + +Or, using Username/Password: ```bash # Required environment variables export METABASE_URL=https://your-metabase-instance.com export METABASE_USERNAME=your_username export METABASE_PASSWORD=your_password ``` - You can set these environment variables in your shell profile or use a `.env` file with a package like `dotenv`. ## Development @@ -56,21 +78,26 @@ npm run watch ``` ## Installation +```bash +# Oneliner, suitable for CI environment +git clone https://github.com/imlewc/metabase-server.git && cd metabase-server && npm i && npm run build && npm link +``` To use with Claude Desktop, add the server config: On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json` On Windows: `%APPDATA%/Claude/claude_desktop_config.json` +API Key authentication is preferred; username/password is used as a fallback if `METABASE_API_KEY` is not set. + ```json { "mcpServers": { "metabase-server": { - "command": "/path/to/metabase-server/build/index.js", + "command": "metabase-server", "env": { "METABASE_URL": "https://your-metabase-instance.com", - "METABASE_USERNAME": "your_username", - "METABASE_PASSWORD": "your_password" + "METABASE_API_KEY": "your_metabase_api_key" } } } @@ -104,6 +131,59 @@ To install metabase-server for Claude Desktop automatically via [Smithery](https npx -y @smithery/cli install @imlewc/metabase-server --client claude ``` +### Installing via Claude Code CLI + +To install metabase-server for Claude Code CLI: + +```bash +claude mcp add metabase-server -s user \ + -e METABASE_URL="https://your-metabase-instance.com" \ + -e METABASE_USERNAME="your-email@example.com" \ + -e METABASE_PASSWORD="your-password" \ + -- npx -y @imlewc/metabase-server +``` + +Or using API Key (preferred): + +```bash +claude mcp add metabase-server -s user \ + -e METABASE_URL="https://your-metabase-instance.com" \ + -e METABASE_API_KEY="your-api-key" \ + -- npx -y @imlewc/metabase-server +``` + +Configuration will be written to `~/.claude.json`: + +```json +{ + "mcpServers": { + "metabase-server": { + "type": "stdio", + "command": "npx", + "args": ["-y", "@imlewc/metabase-server"], + "env": { + "METABASE_URL": "https://your-metabase-instance.com", + "METABASE_USERNAME": "your-email@example.com", + "METABASE_PASSWORD": "your-password" + } + } + } +} +``` + +#### Troubleshooting + +If MCP server fails to connect, check the logs: + +```bash +cat ~/.claude/debug/latest | grep -i metabase +``` + +Common issues: +- Environment variables not configured (`"env": {}` is empty) +- Invalid URL format (must include `https://`) +- Incorrect credentials + ### Debugging Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script: @@ -113,3 +193,40 @@ npm run inspector ``` The Inspector will provide a URL to access debugging tools in your browser. + +## Testing + +After configuring the environment variables as described in the "Configuration" section, you can manually test the server's authentication. The MCP Inspector (`npm run inspector`) is a useful tool for sending requests to the server. + +### 1. Testing with API Key Authentication + +1. Set the `METABASE_URL` and `METABASE_API_KEY` environment variables with your Metabase instance URL and a valid API key. +2. Ensure `METABASE_USERNAME` and `METABASE_PASSWORD` are unset or leave them, as the API key should take precedence. +3. Start the server: `npm run build && node build/index.js` (or use your chosen method for running the server, like via Claude Desktop config). +4. Check the server logs. You should see a message indicating that it's using API key authentication (e.g., "Using Metabase API Key for authentication."). +5. Using an MCP client or the MCP Inspector, try calling a tool, for example, `tools/call` with `{"name": "list_dashboards"}`. +6. Verify that the tool call is successful and you receive the expected data. + +### 2. Testing with Username/Password Authentication (Fallback) + +1. Ensure the `METABASE_API_KEY` environment variable is unset. +2. Set `METABASE_URL`, `METABASE_USERNAME`, and `METABASE_PASSWORD` with valid credentials for your Metabase instance. +3. Start the server. +4. Check the server logs. You should see a message indicating that it's using username/password authentication (e.g., "Using Metabase username/password for authentication." followed by "Authenticating with Metabase using username/password..."). +5. Using an MCP client or the MCP Inspector, try calling the `list_dashboards` tool. +6. Verify that the tool call is successful. + +### 3. Testing Authentication Failures + +* **Invalid API Key:** + 1. Set `METABASE_URL` and an invalid `METABASE_API_KEY`. Ensure `METABASE_USERNAME` and `METABASE_PASSWORD` variables are unset. + 2. Start the server. + 3. Attempt to call a tool (e.g., `list_dashboards`). The tool call should fail, and the server logs might indicate an authentication error from Metabase (e.g., "Metabase API error: Invalid X-API-Key"). +* **Invalid Username/Password:** + 1. Ensure `METABASE_API_KEY` is unset. Set `METABASE_URL` and invalid `METABASE_USERNAME`/`METABASE_PASSWORD`. + 2. Start the server. + 3. Attempt to call a tool. The tool call should fail due to failed session authentication. The server logs might show "Authentication failed" or "Failed to authenticate with Metabase". +* **Missing Credentials:** + 1. Unset `METABASE_API_KEY`, `METABASE_USERNAME`, and `METABASE_PASSWORD`. Set only `METABASE_URL`. + 2. Attempt to start the server. + 3. The server should fail to start and log an error message stating that authentication credentials (either API key or username/password) are required (e.g., "Either (METABASE_URL and METABASE_API_KEY) or (METABASE_URL, METABASE_USERNAME, and METABASE_PASSWORD) environment variables are required"). diff --git a/package-lock.json b/package-lock.json index c7f9c2c..fe743e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,9 @@ "name": "@simon/metabase-server", "version": "0.1.0", "dependencies": { - "@modelcontextprotocol/sdk": "^0.6.1", + "@modelcontextprotocol/sdk": "^1.25.2", "abort-controller": "^3.0.0", - "axios": "^1.8.1" + "axios": "^1.14.0" }, "bin": { "metabase-server": "build/index.js" @@ -21,20 +21,61 @@ "typescript": "^5.3.3" } }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@modelcontextprotocol/sdk": { - "version": "0.6.1", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/@modelcontextprotocol/sdk/-/sdk-0.6.1.tgz", - "integrity": "sha512-OkVXMix3EIbB5Z6yife2XTrSlOnVvCLR1Kg91I4pYFEsV9RbnoyQVScXCuVhGaZHOnTZgso8lMQN1Po2TadGKQ==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "license": "MIT", "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "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.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8" + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } } }, "node_modules/@types/axios": { "version": "0.14.4", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/@types/axios/-/axios-0.14.4.tgz", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.4.tgz", "integrity": "sha512-9JgOaunvQdsQ/qW2OPmE5+hCeUB52lQSolecrFrthct55QekhmXEwT203s20RL+UHtCQc15y3VXpby9E7Kkh/g==", "deprecated": "This is a stub types definition. axios provides its own type definitions, so you do not need this installed.", "dev": true, @@ -44,18 +85,18 @@ } }, "node_modules/@types/node": { - "version": "20.17.30", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/@types/node/-/node-20.17.30.tgz", - "integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==", + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" } }, "node_modules/abort-controller": { "version": "3.0.0", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/abort-controller/-/abort-controller-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "license": "MIT", "dependencies": { @@ -65,26 +106,122 @@ "node": ">=6.5" } }, + "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/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/asynckit": { "version": "0.4.0", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/asynckit/-/asynckit-0.4.0.tgz", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/bytes": { "version": "3.1.2", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/bytes/-/bytes-3.1.2.tgz", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", "engines": { @@ -93,7 +230,7 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "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": { @@ -104,9 +241,25 @@ "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/combined-stream": { "version": "1.0.8", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/combined-stream/-/combined-stream-1.0.8.tgz", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "license": "MIT", "dependencies": { @@ -116,18 +269,97 @@ "node": ">= 0.8" } }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/content-type": { "version": "1.0.5", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/content-type/-/content-type-1.0.5.tgz", + "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.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/delayed-stream": { "version": "1.0.0", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/delayed-stream/-/delayed-stream-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "license": "MIT", "engines": { @@ -136,7 +368,7 @@ }, "node_modules/depd": { "version": "2.0.0", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/depd/-/depd-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", "engines": { @@ -145,7 +377,7 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/dunder-proto/-/dunder-proto-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { @@ -157,9 +389,24 @@ "node": ">= 0.4" } }, + "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/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://simondata.jfrog.io/artifactory/api/npm/npm/es-define-property/-/es-define-property-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", "engines": { @@ -168,7 +415,7 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/es-errors/-/es-errors-1.3.0.tgz", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", "engines": { @@ -176,9 +423,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -189,7 +436,7 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", "dependencies": { @@ -202,19 +449,160 @@ "node": ">= 0.4" } }, + "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/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/event-target-shim": { "version": "5.0.1", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/event-target-shim/-/event-target-shim-5.0.1.tgz", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "license": "MIT", "engines": { "node": ">=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.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.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": "8.6.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.0.tgz", + "integrity": "sha512-XKJXDsASUOo0LLtFwW5hCcQGH0N4WQc/Rn8/Pvoia+TJFOkkFPvrtW9lZOeeNcxQJspvOIERMwiRLsVFlhHEkA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "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-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "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": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -232,23 +620,63 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" } }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/function-bind": { "version": "1.1.2", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/function-bind/-/function-bind-1.1.2.tgz", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", "funding": { @@ -257,7 +685,7 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { @@ -281,7 +709,7 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/get-proto/-/get-proto-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { @@ -294,7 +722,7 @@ }, "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/gopd/-/gopd-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { @@ -306,7 +734,7 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/has-symbols/-/has-symbols-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { @@ -318,7 +746,7 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "license": "MIT", "dependencies": { @@ -332,9 +760,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -343,107 +771,515 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.12.30", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.30.tgz", + "integrity": "sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "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" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/inherits/-/inherits-2.0.4.tgz", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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-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/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/math-intrinsics": { "version": "1.1.0", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "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.52.0", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "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": "2.1.35", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/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/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-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-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "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/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/safer-buffer": { "version": "2.1.2", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/safer-buffer/-/safer-buffer-2.1.2.tgz", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/setprototypeof/-/setprototypeof-1.2.0.tgz", + "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/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "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.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "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/statuses": { - "version": "2.0.1", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "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" @@ -451,17 +1287,48 @@ }, "node_modules/toidentifier": { "version": "1.0.1", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/toidentifier/-/toidentifier-1.0.1.tgz", + "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.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "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": { @@ -473,29 +1340,68 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "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://simondata.jfrog.io/artifactory/api/npm/npm/unpipe/-/unpipe-1.0.0.tgz", + "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/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/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/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.24.3", - "resolved": "https://simondata.jfrog.io/artifactory/api/npm/npm/zod/-/zod-3.24.3.tgz", - "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "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" + } } } } diff --git a/package.json b/package.json index 42af5ae..77322a3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@simon/metabase-server", "version": "0.1.0", - "description": "A Model Context Protocol server for Metabase", + "description": "MCP server for interacting with Metabase - provides AI assistants with tools to query, create, and manage dashboards, cards, and databases", "private": false, "type": "module", "bin": { @@ -11,15 +11,16 @@ "build" ], "scripts": { - "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\" && mkdir -p dist && cp build/index.js dist/index.js", + "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\" && mkdir -p dist && cp build/*.js dist/", + "test": "npm run build && node --test test/", "prepare": "npm run build", "watch": "tsc --watch", "inspector": "npx @modelcontextprotocol/inspector build/index.js" }, "dependencies": { - "@modelcontextprotocol/sdk": "^0.6.1", + "@modelcontextprotocol/sdk": "^1.25.2", "abort-controller": "^3.0.0", - "axios": "^1.8.1" + "axios": "^1.14.0" }, "devDependencies": { "@types/axios": "^0.14.4", diff --git a/src/formatters.ts b/src/formatters.ts new file mode 100644 index 0000000..068692c --- /dev/null +++ b/src/formatters.ts @@ -0,0 +1,292 @@ +/** + * Response formatters for Metabase MCP server + * Converts verbose JSON responses to concise markdown + */ + +interface Database { + id: number; + name: string; + engine: string; + initial_sync_status?: string; + is_sample?: boolean; + is_audit?: boolean; +} + +interface Dashboard { + id: number; + name: string; + collection_id?: number; + view_count?: number; + last_viewed_at?: string; + created_at?: string; + updated_at?: string; + archived?: boolean; +} + +interface Card { + id: number; + name: string; + collection_id?: number; + display?: string; + query_type?: string; + updated_at?: string; + archived?: boolean; + collection?: { name?: string }; + "last-edit-info"?: { timestamp?: string }; +} + +interface QueryResult { + data?: { + rows: any[][]; + cols: Array<{ name: string; display_name: string; base_type: string }>; + }; + status?: string; + row_count?: number; + running_time?: number; + database_id?: number; + error?: string; + error_type?: string; +} + +/** + * Format databases list as a markdown table + */ +export function formatDatabases(data: { data?: Database[]; total?: number } | Database[]): string { + const databases = Array.isArray(data) ? data : (data.data || []); + const total = Array.isArray(data) ? databases.length : (data.total || databases.length); + + if (databases.length === 0) { + return "No databases found."; + } + + let output = `# Metabase Databases (${total} total)\n\n`; + output += "| ID | Name | Engine | Status |\n"; + output += "|---|---|---|---|\n"; + + databases.forEach((db: Database) => { + const status = db.initial_sync_status || 'unknown'; + const flags = []; + if (db.is_sample) flags.push('sample'); + if (db.is_audit) flags.push('audit'); + const statusStr = flags.length > 0 ? `${status} (${flags.join(', ')})` : status; + + output += `| ${db.id} | ${db.name} | ${db.engine} | ${statusStr} |\n`; + }); + + return output; +} + +/** + * Format dashboards list as a markdown table + */ +export function formatDashboards(dashboards: Dashboard[]): string { + if (!Array.isArray(dashboards) || dashboards.length === 0) { + return "No dashboards found."; + } + + let output = `# Metabase Dashboards (${dashboards.length} total)\n\n`; + output += "| ID | Name | Collection | Views | Last Viewed | Archived |\n"; + output += "|---|---|---|---|---|---|\n"; + + dashboards.forEach((dash: Dashboard) => { + const collectionId = dash.collection_id || 'root'; + const views = dash.view_count || 0; + const lastViewed = dash.last_viewed_at + ? new Date(dash.last_viewed_at).toISOString().split('T')[0] + : 'never'; + const archived = dash.archived ? '✓' : ''; + + output += `| ${dash.id} | ${dash.name} | ${collectionId} | ${views} | ${lastViewed} | ${archived} |\n`; + }); + + return output; +} + +/** + * Format cards/questions list as a markdown table + */ +export function formatCards(cards: Card[]): string { + if (!Array.isArray(cards) || cards.length === 0) { + return "No cards found."; + } + + let output = `# Metabase Cards/Questions (${cards.length} total)\n\n`; + output += "| ID | Name | Collection | Type | Last Edited | Archived |\n"; + output += "|---|---|---|---|---|---|\n"; + + cards.forEach((card: Card) => { + const collectionId = card.collection_id || 'root'; + const type = card.display || card.query_type || 'unknown'; + const lastEdit = card["last-edit-info"]?.timestamp || card.updated_at; + const lastEditStr = lastEdit + ? new Date(lastEdit).toISOString().split('T')[0] + : 'unknown'; + const archived = card.archived ? '✓' : ''; + + output += `| ${card.id} | ${card.name} | ${collectionId} | ${type} | ${lastEditStr} | ${archived} |\n`; + }); + + return output; +} + +/** + * Format query execution results + */ +export function formatQueryResult(result: QueryResult, maxRows: number = 50): string { + let output = "# Query Execution Result\n\n"; + + // Add status information + output += "## Status\n\n"; + output += `- **Status**: ${result.status || 'unknown'}\n`; + output += `- **Rows**: ${result.row_count || 0}\n`; + output += `- **Execution Time**: ${result.running_time || 0}ms\n`; + output += `- **Database ID**: ${result.database_id || 'unknown'}\n\n`; + + // Handle errors + if (result.error || result.error_type) { + output += "## Error\n\n"; + output += `**Type**: ${result.error_type || 'unknown'}\n\n`; + output += `**Message**: ${result.error || 'No error message'}\n`; + return output; + } + + // Handle successful results + if (!result.data || !result.data.rows || result.data.rows.length === 0) { + output += "## Data\n\nNo rows returned.\n"; + return output; + } + + const { rows, cols } = result.data; + const displayRows = rows.slice(0, maxRows); + const truncated = rows.length > maxRows; + + output += "## Data\n\n"; + + // Create column headers + const headers = cols.map(col => col.display_name || col.name); + output += "| " + headers.join(" | ") + " |\n"; + output += "|" + headers.map(() => "---").join("|") + "|\n"; + + // Add data rows + displayRows.forEach((row: any[]) => { + const formattedRow = row.map(cell => { + if (cell === null || cell === undefined) return 'null'; + if (typeof cell === 'object') return JSON.stringify(cell); + return String(cell); + }); + output += "| " + formattedRow.join(" | ") + " |\n"; + }); + + if (truncated) { + const hiddenRows = rows.length - maxRows; + output += `\n**⚠️ Results truncated**: Showing first ${maxRows} of ${rows.length} rows (${hiddenRows} rows hidden)\n`; + output += `*Use \`max_rows: -1\` parameter to see all rows*\n`; + } + + // Add column types + output += "\n## Column Types\n\n"; + cols.forEach(col => { + output += `- **${col.display_name || col.name}**: ${col.base_type}\n`; + }); + + return output; +} + +/** + * Format dashboard cards list + */ +export function formatDashboardCards(cards: any[], tabs?: any[]): string { + if (!Array.isArray(cards) || cards.length === 0) { + return "No cards found in this dashboard."; + } + + const hasTabs = Array.isArray(tabs) && tabs.length > 0; + const tabMap: Record = {}; + if (hasTabs) { + tabs.forEach((tab: any) => { + tabMap[tab.id] = tab.name || `Tab ${tab.id}`; + }); + } + + let output = `# Dashboard Cards (${cards.length} total)\n\n`; + if (hasTabs) { + output += "| Dashcard ID | Card ID | Name | Visualization | Size | Tab |\n"; + output += "|---|---|---|---|---|---|\n"; + } else { + output += "| Dashcard ID | Card ID | Name | Visualization | Size |\n"; + output += "|---|---|---|---|---|\n"; + } + + cards.forEach((dashCard: any) => { + const card = dashCard.card || {}; + const dashcardId = dashCard.id || 'unknown'; + const cardId = card.id || 'unknown'; + const cardName = card.name || 'Untitled'; + const vizType = dashCard.visualization_settings?.["card.title"] + ? 'custom' + : (card.display || 'table'); + const size = `${dashCard.size_x || 4}×${dashCard.size_y || 4}`; + + if (hasTabs) { + const tabName = dashCard.dashboard_tab_id ? (tabMap[dashCard.dashboard_tab_id] || `Tab ${dashCard.dashboard_tab_id}`) : 'None'; + output += `| ${dashcardId} | ${cardId} | ${cardName} | ${vizType} | ${size} | ${tabName} |\n`; + } else { + output += `| ${dashcardId} | ${cardId} | ${cardName} | ${vizType} | ${size} |\n`; + } + }); + + if (hasTabs) { + output += `\n## Dashboard Tabs\n\n`; + output += "| Tab ID | Name | Position |\n"; + output += "|---|---|---|\n"; + tabs.forEach((tab: any, index: number) => { + output += `| ${tab.id} | ${tab.name || 'Untitled'} | ${tab.position ?? index} |\n`; + }); + } + + return output; +} + +/** + * Format card execution result + */ +export function formatCardResult(result: QueryResult, maxRows: number = 50): string { + // Card execution results have the same structure as query results + return formatQueryResult(result, maxRows); +} + +/** + * Format generic object response (fallback for CRUD operations) + */ +export function formatGenericResponse(operation: string, data: any, baseUrl?: string): string { + let output = `# ${operation} Result\n\n`; + + if (typeof data === 'string') { + output += data; + } else if (data && typeof data === 'object') { + output += "## Summary\n\n"; + + // Extract key fields + if (data.id) output += `- **ID**: ${data.id}\n`; + if (data.name) output += `- **Name**: ${data.name}\n`; + + if (baseUrl && data.id) { + const normalizedBaseUrl = baseUrl.replace(/\/+$/, ''); + const op = operation.toLowerCase(); + if (op.includes('dashboard')) { + output += `- **URL**: ${normalizedBaseUrl}/dashboard/${data.id}\n`; + } else if (op.includes('card') || op.includes('question')) { + output += `- **URL**: ${normalizedBaseUrl}/question/${data.id}\n`; + } + } + + if (data.created_at) output += `- **Created**: ${new Date(data.created_at).toISOString()}\n`; + if (data.updated_at) output += `- **Updated**: ${new Date(data.updated_at).toISOString()}\n`; + + output += "\n*Full JSON response saved to `.last-response.json` in the server's install directory (if response logging is enabled)*\n"; + } else { + output += "Operation completed successfully.\n"; + } + + return output; +} diff --git a/src/index.ts b/src/index.ts index f78d782..679cdc5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,6 +27,16 @@ import { } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import axios, { AxiosInstance } from "axios"; +import { + formatDatabases, + formatDashboards, + formatCards, + formatQueryResult, + formatDashboardCards, + formatCardResult, + formatGenericResponse +} from "./formatters.js"; +import { logResponse } from "./response-logger.js"; // 自定义错误枚举 enum ErrorCode { @@ -39,7 +49,7 @@ enum ErrorCode { // 自定义错误类 class McpError extends Error { code: ErrorCode; - + constructor(code: ErrorCode, message: string) { super(message); this.code = code; @@ -51,9 +61,12 @@ class McpError extends Error { const METABASE_URL = process.env.METABASE_URL; const METABASE_USERNAME = process.env.METABASE_USERNAME; const METABASE_PASSWORD = process.env.METABASE_PASSWORD; +const METABASE_API_KEY = process.env.METABASE_API_KEY; -if (!METABASE_URL || !METABASE_USERNAME || !METABASE_PASSWORD) { - throw new Error("METABASE_URL, METABASE_USERNAME, and METABASE_PASSWORD environment variables are required"); +if (!METABASE_URL || (!METABASE_API_KEY && (!METABASE_USERNAME || !METABASE_PASSWORD))) { + throw new Error( + "Either (METABASE_URL and METABASE_API_KEY) or (METABASE_URL, METABASE_USERNAME, and METABASE_PASSWORD) environment variables are required" + ); } // 创建自定义 Schema 对象,使用 z.object @@ -89,11 +102,26 @@ class MetabaseServer { headers: { "Content-Type": "application/json", }, + timeout: 30000, // 30 second timeout to prevent hanging }); + if (METABASE_API_KEY) { + this.logInfo('Using Metabase API Key for authentication.'); + this.axiosInstance.defaults.headers.common['X-API-Key'] = METABASE_API_KEY; + this.sessionToken = "api_key_used"; // Indicate API key is in use + } else if (METABASE_USERNAME && METABASE_PASSWORD) { + this.logInfo('Using Metabase username/password for authentication.'); + // Existing session token logic will apply + } else { + // This case should ideally be caught by the initial environment variable check + // but as a safeguard: + this.logError('Metabase authentication credentials not configured properly.', {}); + throw new Error("Metabase authentication credentials not provided or incomplete."); + } + this.setupResourceHandlers(); this.setupToolHandlers(); - + // Enhanced error handling with logging this.server.onerror = (error: Error) => { this.logError('Server Error', error); @@ -127,7 +155,7 @@ class MetabaseServer { private logError(message: string, error: unknown) { const errorObj = error as Error; const apiError = error as { response?: { data?: { message?: string } }, message?: string }; - + const logMessage = { timestamp: new Date().toISOString(), level: 'error', @@ -148,11 +176,12 @@ class MetabaseServer { * 获取 Metabase 会话令牌 */ private async getSessionToken(): Promise { - if (this.sessionToken) { + if (this.sessionToken) { // Handles both API key ("api_key_used") and actual session tokens return this.sessionToken; } - this.logInfo('Authenticating with Metabase...'); + // This part should only be reached if using username/password and sessionToken is null + this.logInfo('Authenticating with Metabase using username/password...'); try { const response = await this.axiosInstance.post('/api/session', { username: METABASE_USERNAME, @@ -160,10 +189,10 @@ class MetabaseServer { }); this.sessionToken = response.data.id; - + // 设置默认请求头 this.axiosInstance.defaults.headers.common['X-Metabase-Session'] = this.sessionToken; - + this.logInfo('Successfully authenticated with Metabase'); return this.sessionToken as string; } catch (error) { @@ -181,12 +210,14 @@ class MetabaseServer { private setupResourceHandlers() { this.server.setRequestHandler(ListResourcesRequestSchema, async (request) => { this.logInfo('Listing resources...', { requestStructure: JSON.stringify(request) }); - await this.getSessionToken(); + if (!METABASE_API_KEY) { + await this.getSessionToken(); + } try { // 获取仪表板列表 const dashboardsResponse = await this.axiosInstance.get('/api/dashboard'); - + this.logInfo('Successfully listed resources', { count: dashboardsResponse.data.length }); // 将仪表板作为资源返回 return { @@ -235,7 +266,9 @@ class MetabaseServer { // 读取资源 this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { this.logInfo('Reading resource...', { requestStructure: JSON.stringify(request) }); - await this.getSessionToken(); + if (!METABASE_API_KEY) { + await this.getSessionToken(); + } const uri = request.params?.uri; let match; @@ -245,7 +278,7 @@ class MetabaseServer { if ((match = uri.match(/^metabase:\/\/dashboard\/(\d+)$/))) { const dashboardId = match[1]; const response = await this.axiosInstance.get(`/api/dashboard/${dashboardId}`); - + return { contents: [{ uri: request.params?.uri, @@ -254,12 +287,12 @@ class MetabaseServer { }] }; } - + // 处理问题/卡片资源 else if ((match = uri.match(/^metabase:\/\/card\/(\d+)$/))) { const cardId = match[1]; const response = await this.axiosInstance.get(`/api/card/${cardId}`); - + return { contents: [{ uri: request.params?.uri, @@ -268,12 +301,12 @@ class MetabaseServer { }] }; } - + // 处理数据库资源 else if ((match = uri.match(/^metabase:\/\/database\/(\d+)$/))) { const databaseId = match[1]; const response = await this.axiosInstance.get(`/api/database/${databaseId}`); - + return { contents: [{ uri: request.params?.uri, @@ -282,7 +315,7 @@ class MetabaseServer { }] }; } - + else { throw new McpError( ErrorCode.InvalidRequest, @@ -305,6 +338,7 @@ class MetabaseServer { * 设置工具处理程序 */ private setupToolHandlers() { + // No session token needed for listing tools, as it's static data this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ @@ -321,7 +355,12 @@ class MetabaseServer { description: "List all questions/cards in Metabase", inputSchema: { type: "object", - properties: {} + properties: { + f: { + type: "string", + description: "Optional filter function, possible values: archived, table, database, using_model, bookmarked, using_segment, all, mine" + } + } } }, { @@ -332,6 +371,20 @@ class MetabaseServer { properties: {} } }, + { + name: "get_card", + description: "Get a single Metabase question/card by ID with full details including dataset_query with template-tags configuration for variables/filters. Use this to inspect a card before updating it.", + inputSchema: { + type: "object", + properties: { + card_id: { + type: "number", + description: "ID of the card/question to retrieve" + } + }, + required: ["card_id"] + } + }, { name: "execute_card", description: "Execute a Metabase question/card and get results", @@ -343,8 +396,15 @@ class MetabaseServer { description: "ID of the card/question to execute" }, parameters: { - type: "object", - description: "Optional parameters for the query" + description: "Optional parameters for the query. Metabase expects an array; a single object will be wrapped.", + oneOf: [ + { type: "array", items: { type: "object" } }, + { type: "object" } + ] + }, + max_rows: { + type: "number", + description: "Maximum number of rows to display in output (default: 50, use -1 for all rows)" } }, required: ["card_id"] @@ -384,10 +444,414 @@ class MetabaseServer { items: { type: "object" } + }, + max_rows: { + type: "number", + description: "Maximum number of rows to display in output (default: 50, use -1 for all rows)" } }, required: ["database_id", "query"] } + }, + { + name: "create_card", + description: "Create a new Metabase question (card).", + inputSchema: { + type: "object", + properties: { + name: { type: "string", description: "Name of the card" }, + dataset_query: { type: "object", description: "The query for the card (e.g., MBQL or native query)" }, + display: { type: "string", description: "Display type (e.g., 'table', 'line', 'bar')" }, + visualization_settings: { type: "object", description: "Settings for the visualization" }, + collection_id: { type: "number", description: "Optional ID of the collection to save the card in" }, + description: { type: "string", description: "Optional description for the card" } + }, + required: ["name", "dataset_query", "display", "visualization_settings"] + } + }, + { + name: "update_card", + description: "Update an existing Metabase question (card). For native SQL queries with template variables (like dropdown filters), use dataset_query.native.template-tags to configure each variable. Each template-tag can have: name, display-name, type (text/number/dimension), dimension (for field filters), widget-type (category, string/=, number/=, etc.), and default value.", + inputSchema: { + type: "object", + properties: { + card_id: { type: "number", description: "ID of the card to update" }, + name: { type: "string", description: "New name for the card" }, + dataset_query: { + type: "object", + description: "Query configuration. For native SQL: {type: 'native', database: , native: {query: 'SELECT...', template-tags: {...}}}. Template-tags example: {'semester': {id: 'uuid', name: 'semester', display-name: 'Semester', type: 'dimension', dimension: ['field', , null], widget-type: 'category'}}", + properties: { + type: { type: "string", description: "'native' for SQL queries, 'query' for MBQL" }, + database: { type: "number", description: "Database ID" }, + native: { + type: "object", + description: "Native SQL query configuration", + properties: { + query: { type: "string", description: "SQL query with {{variable}} placeholders" }, + "template-tags": { + type: "object", + description: "Variable configurations keyed by variable name. Each has: id, name, display-name, type (text/number/dimension), dimension (for field filters as ['field', field_id, null]), widget-type (category/string/=/number/=)" + } + } + } + } + }, + display: { type: "string", description: "New display type" }, + visualization_settings: { type: "object", description: "New visualization settings" }, + collection_id: { type: "number", description: "New collection ID" }, + description: { type: "string", description: "New description" }, + archived: { type: "boolean", description: "Set to true to archive the card" }, + type: { type: "string", description: "Card type: 'question' or 'model'" } + }, + required: ["card_id"] + } + }, + { + name: "delete_card", + description: "Delete a Metabase question (card).", + inputSchema: { + type: "object", + properties: { + card_id: { type: "number", description: "ID of the card to delete" }, + hard_delete: { type: "boolean", description: "Set to true for hard delete, false (default) for archive", default: false } + }, + required: ["card_id"] + } + }, + { + name: "create_dashboard", + description: "Create a new Metabase dashboard.", + inputSchema: { + type: "object", + properties: { + name: { type: "string", description: "Name of the dashboard" }, + description: { type: "string", description: "Optional description for the dashboard" }, + parameters: { type: "array", description: "Optional parameters for the dashboard", items: { type: "object" } }, + collection_id: { type: "number", description: "Optional ID of the collection to save the dashboard in" } + }, + required: ["name"] + } + }, + { + name: "update_dashboard", + description: "Update an existing Metabase dashboard.", + inputSchema: { + type: "object", + properties: { + dashboard_id: { type: "number", description: "ID of the dashboard to update" }, + name: { type: "string", description: "New name for the dashboard" }, + description: { type: "string", description: "New description for the dashboard" }, + parameters: { type: "array", description: "New parameters for the dashboard", items: { type: "object" } }, + collection_id: { type: "number", description: "New collection ID" }, + archived: { type: "boolean", description: "Set to true to archive the dashboard" } + }, + required: ["dashboard_id"] + } + }, + { + name: "delete_dashboard", + description: "Delete a Metabase dashboard.", + inputSchema: { + type: "object", + properties: { + dashboard_id: { type: "number", description: "ID of the dashboard to delete" }, + hard_delete: { type: "boolean", description: "Set to true for hard delete, false (default) for archive", default: false } + }, + required: ["dashboard_id"] + } + }, + { + name: "add_card_to_dashboard", + description: "Add a card/question to a dashboard.", + inputSchema: { + type: "object", + properties: { + dashboard_id: { type: "number", description: "ID of the dashboard" }, + card_id: { type: "number", description: "ID of the card to add" }, + size_x: { type: "number", description: "Width of the card (default: 4)", default: 4 }, + size_y: { type: "number", description: "Height of the card (default: 3)", default: 3 }, + row: { type: "number", description: "Row position (default: 0)", default: 0 }, + col: { type: "number", description: "Column position (default: 0)", default: 0 }, + dashboard_tab_id: { type: "number", description: "ID of the dashboard tab. Required for tabbed dashboards. Use get_dashboard_cards to find tab IDs." } + }, + required: ["dashboard_id", "card_id"] + } + }, + { + name: "list_collections", + description: "List all collections in Metabase.", + inputSchema: { + type: "object", + properties: { + namespace: { type: "string", description: "Optional namespace filter" } + } + } + }, + { + name: "create_collection", + description: "Create a new collection in Metabase.", + inputSchema: { + type: "object", + properties: { + name: { type: "string", description: "Name of the collection" }, + description: { type: "string", description: "Optional description" }, + color: { type: "string", description: "Optional color (hex code like #509EE3)" }, + parent_id: { type: "number", description: "Optional parent collection ID for nesting" } + }, + required: ["name"] + } + }, + { + name: "update_collection", + description: "Update a collection in Metabase.", + inputSchema: { + type: "object", + properties: { + collection_id: { type: "number", description: "ID of the collection to update" }, + name: { type: "string", description: "New name for the collection" }, + description: { type: "string", description: "New description" }, + color: { type: "string", description: "New color (hex code)" }, + archived: { type: "boolean", description: "Set to true to archive" } + }, + required: ["collection_id"] + } + }, + { + name: "list_permission_groups", + description: "List all permission groups in Metabase.", + inputSchema: { + type: "object", + properties: {} + } + }, + { + name: "create_permission_group", + description: "Create a new permission group in Metabase.", + inputSchema: { + type: "object", + properties: { + name: { type: "string", description: "Name of the permission group" } + }, + required: ["name"] + } + }, + { + name: "delete_permission_group", + description: "Delete a permission group in Metabase.", + inputSchema: { + type: "object", + properties: { + group_id: { type: "number", description: "ID of the group to delete" } + }, + required: ["group_id"] + } + }, + { + name: "get_collection_permissions", + description: "Get the collection permissions graph showing which groups have access to which collections.", + inputSchema: { + type: "object", + properties: {} + } + }, + { + name: "update_collection_permissions", + description: "Update collection permissions for a group. Sets the permission level for a group on a collection.", + inputSchema: { + type: "object", + properties: { + group_id: { type: "number", description: "ID of the permission group" }, + collection_id: { type: "number", description: "ID of the collection (use 'root' for root collection)" }, + permission: { type: "string", description: "Permission level: 'read', 'write', or 'none'" } + }, + required: ["group_id", "collection_id", "permission"] + } + }, + { + name: "add_user_to_group", + description: "Add a user to a permission group.", + inputSchema: { + type: "object", + properties: { + group_id: { type: "number", description: "ID of the permission group" }, + user_id: { type: "number", description: "ID of the user to add" } + }, + required: ["group_id", "user_id"] + } + }, + { + name: "list_users", + description: "List all users in Metabase.", + inputSchema: { + type: "object", + properties: {} + } + }, + { + name: "create_user", + description: "Create a new user in Metabase.", + inputSchema: { + type: "object", + properties: { + first_name: { type: "string", description: "User's first name" }, + last_name: { type: "string", description: "User's last name" }, + email: { type: "string", description: "User's email address (used as login)" }, + password: { type: "string", description: "User's password (optional - if not provided, user will need to reset)" }, + group_ids: { type: "array", items: { type: "number" }, description: "Optional array of permission group IDs to add the user to" } + }, + required: ["first_name", "last_name", "email"] + } + }, + { + name: "update_user", + description: "Update an existing user in Metabase.", + inputSchema: { + type: "object", + properties: { + user_id: { type: "number", description: "ID of the user to update" }, + first_name: { type: "string", description: "New first name" }, + last_name: { type: "string", description: "New last name" }, + email: { type: "string", description: "New email address" }, + is_superuser: { type: "boolean", description: "Whether the user should be an admin" }, + login_attributes: { type: "object", description: "Custom login attributes for the user" } + }, + required: ["user_id"] + } + }, + { + name: "disable_user", + description: "Disable (deactivate) a user in Metabase. This prevents them from logging in but preserves their data.", + inputSchema: { + type: "object", + properties: { + user_id: { type: "number", description: "ID of the user to disable" } + }, + required: ["user_id"] + } + }, + { + name: "remove_user_from_group", + description: "Remove a user from a permission group.", + inputSchema: { + type: "object", + properties: { + membership_id: { type: "number", description: "ID of the membership to remove (get this from the user's group_ids or list_permission_groups)" } + }, + required: ["membership_id"] + } + }, + { + name: "get_user", + description: "Get details about a specific user including their group memberships.", + inputSchema: { + type: "object", + properties: { + user_id: { type: "number", description: "ID of the user to retrieve" } + }, + required: ["user_id"] + } + }, + { + name: "get_dashboard", + description: "Get full dashboard details including cards and parameters.", + inputSchema: { + type: "object", + properties: { + dashboard_id: { type: "number", description: "ID of the dashboard" } + }, + required: ["dashboard_id"] + } + }, + { + name: "list_dashboard_tabs", + description: "List all tabs of a dashboard. Returns tab IDs, names, and positions.", + inputSchema: { + type: "object", + properties: { + dashboard_id: { type: "number", description: "ID of the dashboard" } + }, + required: ["dashboard_id"] + } + }, + { + name: "update_dashboard_cards", + description: "Update dashboard cards including their parameter mappings. Use this to connect dashboard filters to card variables.", + inputSchema: { + type: "object", + properties: { + dashboard_id: { type: "number", description: "ID of the dashboard" }, + cards: { + type: "array", + description: "Array of card configurations with parameter_mappings", + items: { + type: "object", + properties: { + id: { type: "number", description: "Dashcard ID (not card_id)" }, + card_id: { type: "number", description: "Card/Question ID" }, + row: { type: "number", description: "Row position" }, + col: { type: "number", description: "Column position" }, + size_x: { type: "number", description: "Width" }, + size_y: { type: "number", description: "Height" }, + dashboard_tab_id: { type: "number", description: "ID of the dashboard tab this card belongs to. Required for tabbed dashboards." }, + parameter_mappings: { + type: "array", + description: "Parameter mappings connecting dashboard filters to card variables", + items: { + type: "object", + properties: { + parameter_id: { type: "string", description: "Dashboard parameter ID" }, + card_id: { type: "number", description: "Card ID" }, + target: { type: "array", description: "Target specification, e.g. ['variable', ['template-tag', 'semester']]", items: {} } + } + } + } + } + } + } + }, + required: ["dashboard_id", "cards"] + } + }, + { + name: "remove_card_from_dashboard", + description: "Remove a card from a dashboard.", + inputSchema: { + type: "object", + properties: { + dashboard_id: { type: "number", description: "ID of the dashboard" }, + dashcard_id: { type: "number", description: "ID of the dashcard (not the card_id)" } + }, + required: ["dashboard_id", "dashcard_id"] + } + }, + { + name: "add_dashboard_filter", + description: "Add or update a filter parameter on a dashboard.", + inputSchema: { + type: "object", + properties: { + dashboard_id: { type: "number", description: "ID of the dashboard" }, + parameters: { + type: "array", + description: "Array of dashboard parameters/filters", + items: { + type: "object", + properties: { + id: { type: "string", description: "Unique parameter ID" }, + name: { type: "string", description: "Display name for the filter" }, + slug: { type: "string", description: "URL slug for the parameter" }, + type: { type: "string", description: "Parameter type, e.g. 'number/=', 'string/=', 'category'" }, + values_source_type: { type: "string", description: "Source for dropdown values: 'static-list', 'card', or null" }, + values_source_config: { + type: "object", + description: "Configuration for value source. For 'card': {card_id, value_field, label_field}. For 'static-list': {values: [[value, label], ...]}" + } + } + } + } + }, + required: ["dashboard_id", "parameters"] + } } ] }; @@ -395,41 +859,54 @@ class MetabaseServer { this.server.setRequestHandler(CallToolRequestSchema, async (request) => { this.logInfo('Calling tool...', { requestStructure: JSON.stringify(request) }); - await this.getSessionToken(); + if (!METABASE_API_KEY) { + await this.getSessionToken(); + } try { switch (request.params?.name) { case "list_dashboards": { const response = await this.axiosInstance.get('/api/dashboard'); + await logResponse('list_dashboards', request.params?.arguments, response.data); return { content: [{ type: "text", - text: JSON.stringify(response.data, null, 2) + text: formatDashboards(response.data) }] }; } case "list_cards": { - const response = await this.axiosInstance.get('/api/card'); + const VALID_CARD_FILTERS = ["archived", "table", "database", "using_model", "bookmarked", "using_segment", "all", "mine"]; + const f = String(request.params?.arguments?.f || "all"); + if (!VALID_CARD_FILTERS.includes(f)) { + throw new McpError( + ErrorCode.InvalidParams, + `Invalid filter "${f}". Must be one of: ${VALID_CARD_FILTERS.join(", ")}` + ); + } + const response = await this.axiosInstance.get(`/api/card?f=${encodeURIComponent(f)}`); + await logResponse('list_cards', request.params?.arguments, response.data); return { content: [{ type: "text", - text: JSON.stringify(response.data, null, 2) + text: formatCards(response.data) }] }; } case "list_databases": { const response = await this.axiosInstance.get('/api/database'); + await logResponse('list_databases', request.params?.arguments, response.data); return { content: [{ type: "text", - text: JSON.stringify(response.data, null, 2) + text: formatDatabases(response.data) }] }; } - case "execute_card": { + case "get_card": { const cardId = request.params?.arguments?.card_id; if (!cardId) { throw new McpError( @@ -437,10 +914,7 @@ class MetabaseServer { "Card ID is required" ); } - - const parameters = request.params?.arguments?.parameters || {}; - const response = await this.axiosInstance.post(`/api/card/${cardId}/query`, { parameters }); - + const response = await this.axiosInstance.get(`/api/card/${cardId}`); return { content: [{ type: "text", @@ -449,44 +923,91 @@ class MetabaseServer { }; } - case "get_dashboard_cards": { - const dashboardId = request.params?.arguments?.dashboard_id; - if (!dashboardId) { - throw new McpError( - ErrorCode.InvalidParams, + case "execute_card": { + const cardId = request.params?.arguments?.card_id; + if (!cardId) { + throw new McpError( + ErrorCode.InvalidParams, + "Card ID is required" + ); + } + + const rawParameters = request.params?.arguments?.parameters; + let parameters: any[]; + if (rawParameters === undefined || rawParameters === null) { + parameters = []; + } else if (Array.isArray(rawParameters)) { + parameters = rawParameters; + } else if (typeof rawParameters === "object" && Object.keys(rawParameters).length === 0) { + parameters = []; + } else { + throw new McpError( + ErrorCode.InvalidParams, + "parameters must be an array of parameter objects" + ); + } + const maxRows = typeof request.params?.arguments?.max_rows === 'number' + ? request.params.arguments.max_rows + : 50; + const response = await this.axiosInstance.post(`/api/card/${cardId}/query`, { parameters }); + await logResponse('execute_card', request.params?.arguments, response.data); + + return { + content: [{ + type: "text", + text: formatCardResult(response.data, maxRows === -1 ? Infinity : maxRows) + }] + }; + } + + case "get_dashboard_cards": { + const dashboardId = request.params?.arguments?.dashboard_id; + if (!dashboardId) { + throw new McpError( + ErrorCode.InvalidParams, "Dashboard ID is required" ); } const response = await this.axiosInstance.get(`/api/dashboard/${dashboardId}`); - + const dashcards = + response.data?.ordered_cards ?? + response.data?.dashcards ?? + response.data?.cards ?? + []; + const tabs = response.data?.tabs ?? []; + await logResponse('get_dashboard_cards', request.params?.arguments, dashcards); + return { content: [{ type: "text", - text: JSON.stringify(response.data.cards, null, 2) + text: formatDashboardCards(dashcards, tabs) }] }; } - + case "execute_query": { const databaseId = request.params?.arguments?.database_id; const query = request.params?.arguments?.query; const nativeParameters = request.params?.arguments?.native_parameters || []; - + const maxRows = typeof request.params?.arguments?.max_rows === 'number' + ? request.params.arguments.max_rows + : 50; + if (!databaseId) { throw new McpError( ErrorCode.InvalidParams, "Database ID is required" ); } - + if (!query) { throw new McpError( ErrorCode.InvalidParams, "SQL query is required" ); } - + // 构建查询请求体 const queryData = { type: "native", @@ -497,9 +1018,494 @@ class MetabaseServer { parameters: nativeParameters, database: databaseId }; - + const response = await this.axiosInstance.post('/api/dataset', queryData); - + await logResponse('execute_query', request.params?.arguments, response.data); + + return { + content: [{ + type: "text", + text: formatQueryResult(response.data, maxRows === -1 ? Infinity : maxRows) + }] + }; + } + + case "create_card": { + const { name, dataset_query, display, visualization_settings, collection_id, description } = request.params?.arguments || {}; + if (!name || !dataset_query || !display || !visualization_settings) { + throw new McpError( + ErrorCode.InvalidParams, + "Missing required fields for create_card: name, dataset_query, display, visualization_settings" + ); + } + const createCardBody: any = { + name, + dataset_query, + display, + visualization_settings, + }; + if (collection_id !== undefined) createCardBody.collection_id = collection_id; + if (description !== undefined) createCardBody.description = description; + + const response = await this.axiosInstance.post('/api/card', createCardBody); + await logResponse('create_card', request.params?.arguments, response.data); + + return { + content: [{ + type: "text", + text: formatGenericResponse('Create Card', response.data, METABASE_URL) + }] + }; + } + + case "update_card": { + const { card_id, ...updateFields } = request.params?.arguments || {}; + if (!card_id) { + throw new McpError( + ErrorCode.InvalidParams, + "Card ID is required for update_card" + ); + } + if (Object.keys(updateFields).length === 0) { + throw new McpError( + ErrorCode.InvalidParams, + "No fields provided for update_card" + ); + } + const response = await this.axiosInstance.put(`/api/card/${card_id}`, updateFields); + await logResponse('update_card', request.params?.arguments, response.data); + + return { + content: [{ + type: "text", + text: formatGenericResponse('Update Card', response.data, METABASE_URL) + }] + }; + } + + case "delete_card": { + const { card_id, hard_delete = false } = request.params?.arguments || {}; + if (!card_id) { + throw new McpError( + ErrorCode.InvalidParams, + "Card ID is required for delete_card" + ); + } + + if (hard_delete) { + await this.axiosInstance.delete(`/api/card/${card_id}`); + await logResponse('delete_card', request.params?.arguments, { card_id, deleted: true }); + return { + content: [{ + type: "text", + text: `Card ${card_id} permanently deleted.` + }] + }; + } else { + // Soft delete (archive) + const response = await this.axiosInstance.put(`/api/card/${card_id}`, { archived: true }); + await logResponse('delete_card', request.params?.arguments, response.data); + return { + content: [{ + type: "text", + text: formatGenericResponse('Archive Card', response.data, METABASE_URL) + }] + }; + } + } + + case "create_dashboard": { + const { name, description, parameters, collection_id } = request.params?.arguments || {}; + if (!name) { + throw new McpError( + ErrorCode.InvalidParams, + "Missing required field for create_dashboard: name" + ); + } + const createDashboardBody: any = { name }; + if (description !== undefined) createDashboardBody.description = description; + if (parameters !== undefined) createDashboardBody.parameters = parameters; + if (collection_id !== undefined) createDashboardBody.collection_id = collection_id; + + const response = await this.axiosInstance.post('/api/dashboard', createDashboardBody); + await logResponse('create_dashboard', request.params?.arguments, response.data); + + return { + content: [{ + type: "text", + text: formatGenericResponse('Create Dashboard', response.data, METABASE_URL) + }] + }; + } + + case "update_dashboard": { + const { dashboard_id, ...updateFields } = request.params?.arguments || {}; + if (!dashboard_id) { + throw new McpError( + ErrorCode.InvalidParams, + "Dashboard ID is required for update_dashboard" + ); + } + if (Object.keys(updateFields).length === 0) { + throw new McpError( + ErrorCode.InvalidParams, + "No fields provided for update_dashboard" + ); + } + const response = await this.axiosInstance.put(`/api/dashboard/${dashboard_id}`, updateFields); + await logResponse('update_dashboard', request.params?.arguments, response.data); + + return { + content: [{ + type: "text", + text: formatGenericResponse('Update Dashboard', response.data, METABASE_URL) + }] + }; + } + + case "delete_dashboard": { + const { dashboard_id, hard_delete = false } = request.params?.arguments || {}; + if (!dashboard_id) { + throw new McpError( + ErrorCode.InvalidParams, + "Dashboard ID is required for delete_dashboard" + ); + } + + if (hard_delete) { + await this.axiosInstance.delete(`/api/dashboard/${dashboard_id}`); + await logResponse('delete_dashboard', request.params?.arguments, { dashboard_id, deleted: true }); + return { + content: [{ + type: "text", + text: `Dashboard ${dashboard_id} permanently deleted.` + }] + }; + } else { + // Soft delete (archive) + const response = await this.axiosInstance.put(`/api/dashboard/${dashboard_id}`, { archived: true }); + await logResponse('delete_dashboard', request.params?.arguments, response.data); + return { + content: [{ + type: "text", + text: formatGenericResponse('Archive Dashboard', response.data, METABASE_URL) + }] + }; + } + } + + case "add_card_to_dashboard": { + const { dashboard_id, card_id, size_x = 4, size_y = 3, row = 0, col = 0, dashboard_tab_id } = request.params?.arguments || {}; + if (!dashboard_id || !card_id) { + throw new McpError( + ErrorCode.InvalidParams, + "Both dashboard_id and card_id are required" + ); + } + // Since Metabase 0.47+, POST /dashboard/:id/cards was removed. + // Must use PUT /dashboard/:id with dashcards array. Negative ID = new card. + // First get existing dashboard to preserve existing cards + const dashboardResponse = await this.axiosInstance.get(`/api/dashboard/${dashboard_id}`); + const existingDashcards = + dashboardResponse.data?.ordered_cards ?? + dashboardResponse.data?.dashcards ?? + dashboardResponse.data?.cards ?? + []; + const dashTabs = dashboardResponse.data.tabs || []; + + // Resolve tab ID: use provided value, or auto-select first tab for tabbed dashboards + let resolvedTabId = dashboard_tab_id ?? null; + let tabNote = ''; + if (dashTabs.length > 0 && resolvedTabId === null) { + resolvedTabId = dashTabs[0].id; + tabNote = `\n\n**Note**: No dashboard_tab_id was provided. Card was automatically added to the first tab "${dashTabs[0].name || 'Tab ' + dashTabs[0].id}" (ID: ${resolvedTabId}).`; + } + + // Add new card with negative ID (signals creation) + const newDashcard: any = { + id: -1, + card_id: card_id, + size_x, + size_y, + row, + col, + parameter_mappings: [] + }; + if (resolvedTabId !== null) { + newDashcard.dashboard_tab_id = resolvedTabId; + } + + const putBody: any = { + dashcards: [...existingDashcards, newDashcard] + }; + if (dashTabs.length > 0) { + putBody.tabs = dashTabs; + } + + const response = await this.axiosInstance.put(`/api/dashboard/${dashboard_id}`, putBody); + return { + content: [{ + type: "text", + text: JSON.stringify(response.data, null, 2) + tabNote + }] + }; + } + + case "list_collections": { + const namespace = request.params?.arguments?.namespace; + const url = namespace ? `/api/collection?namespace=${namespace}` : '/api/collection'; + const response = await this.axiosInstance.get(url); + return { + content: [{ + type: "text", + text: JSON.stringify(response.data, null, 2) + }] + }; + } + + case "create_collection": { + const { name, description, color, parent_id } = request.params?.arguments || {}; + if (!name) { + throw new McpError( + ErrorCode.InvalidParams, + "Collection name is required" + ); + } + const collectionData: any = { name }; + if (description) collectionData.description = description; + if (color) collectionData.color = color; + if (parent_id) collectionData.parent_id = parent_id; + + const response = await this.axiosInstance.post('/api/collection', collectionData); + return { + content: [{ + type: "text", + text: JSON.stringify(response.data, null, 2) + }] + }; + } + + case "update_collection": { + const { collection_id, ...updateFields } = request.params?.arguments || {}; + if (!collection_id) { + throw new McpError( + ErrorCode.InvalidParams, + "Collection ID is required" + ); + } + const response = await this.axiosInstance.put(`/api/collection/${collection_id}`, updateFields); + return { + content: [{ + type: "text", + text: JSON.stringify(response.data, null, 2) + }] + }; + } + + case "list_permission_groups": { + this.logInfo('Fetching permission groups...'); + const response = await this.axiosInstance.get('/api/permissions/group'); + this.logInfo('Permission groups response', { status: response.status, data: response.data }); + return { + content: [{ + type: "text", + text: JSON.stringify(response.data || [], null, 2) + }] + }; + } + + case "create_permission_group": { + const { name } = request.params?.arguments || {}; + if (!name) { + throw new McpError( + ErrorCode.InvalidParams, + "Group name is required" + ); + } + const response = await this.axiosInstance.post('/api/permissions/group', { name }); + return { + content: [{ + type: "text", + text: JSON.stringify(response.data, null, 2) + }] + }; + } + + case "delete_permission_group": { + const { group_id } = request.params?.arguments || {}; + if (!group_id) { + throw new McpError( + ErrorCode.InvalidParams, + "Group ID is required" + ); + } + await this.axiosInstance.delete(`/api/permissions/group/${group_id}`); + return { + content: [{ + type: "text", + text: `Permission group ${group_id} deleted successfully.` + }] + }; + } + + case "get_collection_permissions": { + const response = await this.axiosInstance.get('/api/collection/graph'); + return { + content: [{ + type: "text", + text: JSON.stringify(response.data, null, 2) + }] + }; + } + + case "update_collection_permissions": { + const { group_id, collection_id, permission } = request.params?.arguments || {}; + if (!group_id || collection_id === undefined || !permission) { + throw new McpError( + ErrorCode.InvalidParams, + "group_id, collection_id, and permission are all required" + ); + } + // First get current graph + const graphResponse = await this.axiosInstance.get('/api/collection/graph'); + const graph = graphResponse.data as { groups: Record>, revision: number }; + + // Update the specific permission + const collKey = collection_id === 0 ? 'root' : String(collection_id); + const groupKey = String(group_id); + if (!graph.groups[groupKey]) { + graph.groups[groupKey] = {}; + } + graph.groups[groupKey][collKey] = permission as string; + + // PUT the updated graph + const response = await this.axiosInstance.put('/api/collection/graph', graph); + return { + content: [{ + type: "text", + text: JSON.stringify(response.data, null, 2) + }] + }; + } + + case "add_user_to_group": { + const { group_id, user_id } = request.params?.arguments || {}; + if (!group_id || !user_id) { + throw new McpError( + ErrorCode.InvalidParams, + "Both group_id and user_id are required" + ); + } + const response = await this.axiosInstance.post('/api/permissions/membership', { + group_id, + user_id + }); + return { + content: [{ + type: "text", + text: JSON.stringify(response.data, null, 2) + }] + }; + } + + case "list_users": { + const response = await this.axiosInstance.get('/api/user'); + return { + content: [{ + type: "text", + text: JSON.stringify(response.data, null, 2) + }] + }; + } + + case "create_user": { + const { first_name, last_name, email, password, group_ids } = request.params?.arguments || {}; + if (!first_name || !last_name || !email) { + throw new McpError( + ErrorCode.InvalidParams, + "first_name, last_name, and email are required" + ); + } + const userData: any = { first_name, last_name, email }; + if (password) userData.password = password; + if (group_ids) userData.group_ids = group_ids; + + const response = await this.axiosInstance.post('/api/user', userData); + return { + content: [{ + type: "text", + text: JSON.stringify(response.data, null, 2) + }] + }; + } + + case "update_user": { + const { user_id, ...updateFields } = request.params?.arguments || {}; + if (!user_id) { + throw new McpError( + ErrorCode.InvalidParams, + "user_id is required" + ); + } + if (Object.keys(updateFields).length === 0) { + throw new McpError( + ErrorCode.InvalidParams, + "No fields provided for update" + ); + } + const response = await this.axiosInstance.put(`/api/user/${user_id}`, updateFields); + return { + content: [{ + type: "text", + text: JSON.stringify(response.data, null, 2) + }] + }; + } + + case "disable_user": { + const { user_id } = request.params?.arguments || {}; + if (!user_id) { + throw new McpError( + ErrorCode.InvalidParams, + "user_id is required" + ); + } + // Metabase uses DELETE on /api/user/:id to deactivate (not permanently delete) + await this.axiosInstance.delete(`/api/user/${user_id}`); + return { + content: [{ + type: "text", + text: `User ${user_id} has been disabled/deactivated.` + }] + }; + } + + case "remove_user_from_group": { + const { membership_id } = request.params?.arguments || {}; + if (!membership_id) { + throw new McpError( + ErrorCode.InvalidParams, + "membership_id is required" + ); + } + await this.axiosInstance.delete(`/api/permissions/membership/${membership_id}`); + return { + content: [{ + type: "text", + text: `Membership ${membership_id} removed successfully.` + }] + }; + } + + case "get_user": { + const { user_id } = request.params?.arguments || {}; + if (!user_id) { + throw new McpError( + ErrorCode.InvalidParams, + "user_id is required" + ); + } + const response = await this.axiosInstance.get(`/api/user/${user_id}`); return { content: [{ type: "text", @@ -507,7 +1513,164 @@ class MetabaseServer { }] }; } - + + case "get_dashboard": { + const { dashboard_id } = request.params?.arguments || {}; + if (!dashboard_id) { + throw new McpError( + ErrorCode.InvalidParams, + "Dashboard ID is required" + ); + } + const response = await this.axiosInstance.get(`/api/dashboard/${dashboard_id}`); + return { + content: [{ + type: "text", + text: JSON.stringify(response.data, null, 2) + }] + }; + } + + case "list_dashboard_tabs": { + const { dashboard_id } = request.params?.arguments || {}; + if (!dashboard_id) { + throw new McpError( + ErrorCode.InvalidParams, + "Dashboard ID is required" + ); + } + const response = await this.axiosInstance.get(`/api/dashboard/${dashboard_id}`); + const tabs = response.data?.tabs ?? []; + await logResponse('list_dashboard_tabs', request.params?.arguments, tabs); + + if (!Array.isArray(tabs) || tabs.length === 0) { + return { + content: [{ + type: "text", + text: "This dashboard has no tabs." + }] + }; + } + + let output = `# Dashboard Tabs (${tabs.length} total)\n\n`; + output += "| Tab ID | Name | Position |\n"; + output += "|---|---|---|\n"; + tabs.forEach((tab: any, index: number) => { + output += `| ${tab.id} | ${tab.name || 'Untitled'} | ${tab.position ?? index} |\n`; + }); + + return { + content: [{ + type: "text", + text: output + }] + }; + } + + case "update_dashboard_cards": { + const { dashboard_id, cards } = request.params?.arguments || {}; + if (!dashboard_id) { + throw new McpError( + ErrorCode.InvalidParams, + "Dashboard ID is required" + ); + } + if (!cards || !Array.isArray(cards)) { + throw new McpError( + ErrorCode.InvalidParams, + "Cards array is required" + ); + } + // Fetch existing dashboard to get tabs + const dashboardResponse = await this.axiosInstance.get(`/api/dashboard/${dashboard_id}`); + const dashTabs = dashboardResponse.data?.tabs ?? []; + + const putBody: any = { + dashcards: cards + }; + if (dashTabs.length > 0) { + putBody.tabs = dashTabs; + } + + const response = await this.axiosInstance.put(`/api/dashboard/${dashboard_id}`, putBody); + return { + content: [{ + type: "text", + text: JSON.stringify(response.data, null, 2) + }] + }; + } + + case "remove_card_from_dashboard": { + const { dashboard_id, dashcard_id } = request.params?.arguments || {}; + if (!dashboard_id || !dashcard_id) { + throw new McpError( + ErrorCode.InvalidParams, + "Both dashboard_id and dashcard_id are required" + ); + } + // Since Metabase 0.47+, DELETE endpoint was removed. + // Must use PUT with dashcards array, omitting the card to delete. + const dashboardResponse = await this.axiosInstance.get(`/api/dashboard/${dashboard_id}`); + const existingDashcards = + dashboardResponse.data?.ordered_cards ?? + dashboardResponse.data?.dashcards ?? + dashboardResponse.data?.cards ?? + []; + const dashTabs = dashboardResponse.data.tabs || []; + const filteredDashcards = existingDashcards.filter((dc: any) => dc.id !== dashcard_id); + + if (filteredDashcards.length === existingDashcards.length) { + return { + content: [{ + type: "text", + text: `Dashcard ${dashcard_id} not found on dashboard ${dashboard_id}` + }], + isError: true + }; + } + + const putBody: any = { + dashcards: filteredDashcards + }; + if (dashTabs.length > 0) { + putBody.tabs = dashTabs; + } + + await this.axiosInstance.put(`/api/dashboard/${dashboard_id}`, putBody); + return { + content: [{ + type: "text", + text: `Dashcard ${dashcard_id} removed from dashboard ${dashboard_id}` + }] + }; + } + + case "add_dashboard_filter": { + const { dashboard_id, parameters } = request.params?.arguments || {}; + if (!dashboard_id) { + throw new McpError( + ErrorCode.InvalidParams, + "Dashboard ID is required" + ); + } + if (!parameters || !Array.isArray(parameters)) { + throw new McpError( + ErrorCode.InvalidParams, + "Parameters array is required" + ); + } + const response = await this.axiosInstance.put(`/api/dashboard/${dashboard_id}`, { + parameters + }); + return { + content: [{ + type: "text", + text: JSON.stringify(response.data, null, 2) + }] + }; + } + default: return { content: [ diff --git a/src/response-logger.ts b/src/response-logger.ts new file mode 100644 index 0000000..117f6c2 --- /dev/null +++ b/src/response-logger.ts @@ -0,0 +1,56 @@ +/** + * Response logger for Metabase MCP server + * Saves full JSON responses for inspection and debugging + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const RESPONSE_LOG_PATH = path.join(__dirname, '..', '.last-response.json'); + +interface LoggedResponse { + timestamp: string; + tool: string; + request: any; + response: any; +} + +/** + * Log full response to .last-response.json, for local inspection/debugging. + * Off by default since requests/responses can contain sensitive data (e.g. + * raw SQL text, query results) — set METABASE_MCP_LOG_RESPONSES=true to enable. + */ +export async function logResponse( + toolName: string, + requestParams: any, + responseData: any +): Promise { + if (process.env.METABASE_MCP_LOG_RESPONSES !== 'true') { + return; + } + + try { + const logEntry: LoggedResponse = { + timestamp: new Date().toISOString(), + tool: toolName, + request: requestParams || {}, + response: responseData + }; + + const jsonData = JSON.stringify(logEntry, null, 2); + + // Write asynchronously but don't await to avoid blocking the response + fs.writeFile(RESPONSE_LOG_PATH, jsonData, 'utf8', (err) => { + if (err) { + console.error('Failed to write response log:', err); + } + }); + } catch (error) { + // Log error but don't throw - response logging should never block the main operation + console.error('Error in logResponse:', error); + } +} diff --git a/test/formatters.test.js b/test/formatters.test.js new file mode 100644 index 0000000..30bf1e3 --- /dev/null +++ b/test/formatters.test.js @@ -0,0 +1,29 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { formatGenericResponse } from '../build/formatters.js'; + +test('adds question URL for card operations', () => { + const result = formatGenericResponse('Create Card', { id: 42, name: 'Revenue' }, 'https://mb.example.com'); + + assert.match(result, /\*\*URL\*\*: https:\/\/mb\.example\.com\/question\/42/); +}); + +test('adds dashboard URL for dashboard operations', () => { + const result = formatGenericResponse('Update Dashboard', { id: 7, name: 'Overview' }, 'https://mb.example.com'); + + assert.match(result, /\*\*URL\*\*: https:\/\/mb\.example\.com\/dashboard\/7/); +}); + +test('normalizes trailing slash in base URL', () => { + const result = formatGenericResponse('Create Card', { id: 99, name: 'Users' }, 'https://mb.example.com/'); + + assert.doesNotMatch(result, /\/\/question\/99/); + assert.match(result, /\*\*URL\*\*: https:\/\/mb\.example\.com\/question\/99/); +}); + +test('does not add URL when id is missing', () => { + const result = formatGenericResponse('Create Card', { name: 'No ID' }, 'https://mb.example.com'); + + assert.doesNotMatch(result, /\*\*URL\*\*:/); +});