├── .env.example ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── example-files ├── 2a6a0807-d323-4424-a48a-e40a82b883bb.mp4 └── 55b9f28b-61a6-423e-bb86-f3791c639177.mp4 ├── package-lock.json ├── package.json ├── smithery.yaml ├── src ├── config.ts ├── index.ts ├── resources │ ├── images.ts │ └── videos.ts ├── server.ts ├── services │ └── veoClient.ts ├── tools │ └── generateVideo.ts └── utils │ └── logger.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Google API Key for Gemini/Veo2 2 | GOOGLE_API_KEY=your_api_key_here 3 | 4 | # Server configuration 5 | PORT=3000 6 | 7 | # Storage directory for generated videos 8 | STORAGE_DIR=./generated-videos 9 | 10 | # Logging level (verbose, debug, info, warn, error, fatal, none) 11 | # Default: fatal - Only logs critical errors 12 | # For development: Set to info or debug for more detailed logs 13 | LOG_LEVEL=fatal 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # vitepress build output 108 | **/.vitepress/dist 109 | 110 | # vitepress cache directory 111 | **/.vitepress/cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | src/standalone_python.py 138 | src/example_videofromimage.ts 139 | src/standalone_typescript.ts 140 | /generated-videos 141 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | FROM node:lts-alpine 3 | WORKDIR /app 4 | 5 | # Install dependencies 6 | COPY package*.json ./ 7 | RUN npm install --production 8 | 9 | # Copy project files 10 | COPY . . 11 | 12 | # Build the project 13 | RUN npm run build 14 | 15 | # Expose the port (default 3000) 16 | EXPOSE 3000 17 | 18 | # Start the server 19 | CMD [ "npm", "start" ] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 mario-andreschak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MCP Video Generation with Veo2 2 | 3 | [![smithery badge](https://smithery.ai/badge/@mario-andreschak/mcp-video-generation-veo2)](https://smithery.ai/server/@mario-andreschak/mcp-video-generation-veo2) 4 | 5 | This project implements a Model Context Protocol (MCP) server that exposes Google's Veo2 video generation capabilities. It allows clients to generate videos from text prompts or images, and access the generated videos through MCP resources. 6 | 7 | 8 | Video Generation with Veo2 MCP server 9 | 10 | 11 | ## Features 12 | 13 | - Generate **videos from text** prompts 14 | - Generate **videos from images** 15 | - Access generated videos through MCP resources 16 | - Example video generation templates 17 | - Support for both stdio and SSE transports 18 | 19 | ## Example Images 20 | ![1dec9c71-07dc-4a6e-9e17-8da355d72ba1](https://github.com/user-attachments/assets/ba987d14-dd46-49ac-9b31-1ce398e86c6f) 21 | 22 | 23 | ## Example Image to Video 24 | [Image to Video - from Grok generated puppy](https://github.com/mario-andreschak/mcp-veo2/raw/refs/heads/main/example-files/2a6a0807-d323-4424-a48a-e40a82b883bb.mp4) 25 | 26 | [Image to Video - from real cat](https://github.com/mario-andreschak/mcp-veo2/raw/refs/heads/main/example-files/55b9f28b-61a6-423e-bb86-f3791c639177.mp4) 27 | 28 | 29 | ## Prerequisites 30 | 31 | - Node.js 18 or higher 32 | - Google API key with access to Gemini API and Veo2 model (= You need to set up a credit card with your API key! -> Go to aistudio.google.com ) 33 | 34 | ## Installation 35 | 36 | ### Installing in [FLUJO](https://github.com/mario-andreschak/FLUJO/) 37 | 1. Click Add Server 38 | 2. Copy & Paste Github URL into FLUJO 39 | 3. Click Parse, Clone, Install, Build and Save. 40 | 41 | ### Installing via Smithery 42 | 43 | To install mcp-video-generation-veo2 for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@mario-andreschak/mcp-veo2): 44 | 45 | ```bash 46 | npx -y @smithery/cli install @mario-andreschak/mcp-veo2 --client claude 47 | ``` 48 | 49 | ### Manual Installation 50 | 1. Clone the repository: 51 | ```bash 52 | git clone https://github.com/yourusername/mcp-video-generation-veo2.git 53 | cd mcp-video-generation-veo2 54 | ``` 55 | 56 | 2. Install dependencies: 57 | ```bash 58 | npm install 59 | ``` 60 | 61 | 3. Create a `.env` file with your Google API key: 62 | ```bash 63 | cp .env.example .env 64 | # Edit .env and add your Google API key 65 | ``` 66 | 67 | The `.env` file supports the following variables: 68 | - `GOOGLE_API_KEY`: Your Google API key (required) 69 | - `PORT`: Server port (default: 3000) 70 | - `STORAGE_DIR`: Directory for storing generated videos (default: ./generated-videos) 71 | - `LOG_LEVEL`: Logging level (default: fatal) 72 | - Available levels: verbose, debug, info, warn, error, fatal, none 73 | - For development, set to `debug` or `info` for more detailed logs 74 | - For production, keep as `fatal` to minimize console output 75 | 76 | 4. Build the project: 77 | ```bash 78 | npm run build 79 | ``` 80 | 81 | ## Usage 82 | 83 | ### Starting the Server 84 | 85 | You can start the server with either stdio or SSE transport: 86 | 87 | #### stdio Transport (Default) 88 | 89 | ```bash 90 | npm start 91 | # or 92 | npm start stdio 93 | ``` 94 | 95 | #### SSE Transport 96 | 97 | ```bash 98 | npm start sse 99 | ``` 100 | 101 | This will start the server on port 3000 (or the port specified in your `.env` file). 102 | 103 | ### MCP Tools 104 | 105 | The server exposes the following MCP tools: 106 | 107 | #### generateVideoFromText 108 | 109 | Generates a video from a text prompt. 110 | 111 | Parameters: 112 | - `prompt` (string): The text prompt for video generation 113 | - `config` (object, optional): Configuration options 114 | - `aspectRatio` (string, optional): "16:9" or "9:16" 115 | - `personGeneration` (string, optional): "dont_allow" or "allow_adult" 116 | - `numberOfVideos` (number, optional): 1 or 2 117 | - `durationSeconds` (number, optional): Between 5 and 8 118 | - `enhancePrompt` (boolean, optional): Whether to enhance the prompt 119 | - `negativePrompt` (string, optional): Text describing what not to generate 120 | 121 | Example: 122 | ```json 123 | { 124 | "prompt": "Panning wide shot of a serene forest with sunlight filtering through the trees, cinematic quality", 125 | "config": { 126 | "aspectRatio": "16:9", 127 | "personGeneration": "dont_allow", 128 | "durationSeconds": 8 129 | } 130 | } 131 | ``` 132 | 133 | #### generateVideoFromImage 134 | 135 | Generates a video from an image. 136 | 137 | Parameters: 138 | - `image` (string): Base64-encoded image data 139 | - `prompt` (string, optional): Text prompt to guide the video generation 140 | - `config` (object, optional): Configuration options (same as above, but personGeneration only supports "dont_allow") 141 | 142 | #### listGeneratedVideos 143 | 144 | Lists all generated videos. 145 | 146 | ### MCP Resources 147 | 148 | The server exposes the following MCP resources: 149 | 150 | #### videos://{id} 151 | 152 | Access a generated video by its ID. 153 | 154 | #### videos://templates 155 | 156 | Access example video generation templates. 157 | 158 | ## Development 159 | 160 | ### Project Structure 161 | 162 | - `src/`: Source code 163 | - `index.ts`: Main entry point 164 | - `server.ts`: MCP server configuration 165 | - `config.ts`: Configuration handling 166 | - `tools/`: MCP tool implementations 167 | - `resources/`: MCP resource implementations 168 | - `services/`: External service integrations 169 | - `utils/`: Utility functions 170 | 171 | ### Building 172 | 173 | ```bash 174 | npm run build 175 | ``` 176 | 177 | ### Development Mode 178 | 179 | ```bash 180 | npm run dev 181 | ``` 182 | 183 | ## License 184 | 185 | MIT -------------------------------------------------------------------------------- /example-files/2a6a0807-d323-4424-a48a-e40a82b883bb.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mario-andreschak/mcp-veo2/1f16a7797857769c99b308e254ce30d4f98599b0/example-files/2a6a0807-d323-4424-a48a-e40a82b883bb.mp4 -------------------------------------------------------------------------------- /example-files/55b9f28b-61a6-423e-bb86-f3791c639177.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mario-andreschak/mcp-veo2/1f16a7797857769c99b308e254ce30d4f98599b0/example-files/55b9f28b-61a6-423e-bb86-f3791c639177.mp4 -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-video-generation-veo2", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "mcp-video-generation-veo2", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@google/genai": "^0.9.0", 13 | "@modelcontextprotocol/sdk": "latest", 14 | "@types/express": "^4.17.21", 15 | "@types/node": "^20.10.5", 16 | "@types/uuid": "^10.0.0", 17 | "dotenv": "^16.3.1", 18 | "express": "^4.18.2", 19 | "typescript": "^5.3.3", 20 | "uuid": "^11.1.0", 21 | "zod": "^3.22.4" 22 | } 23 | }, 24 | "node_modules/@google/genai": { 25 | "version": "0.9.0", 26 | "resolved": "https://registry.npmjs.org/@google/genai/-/genai-0.9.0.tgz", 27 | "integrity": "sha512-FD2RizYGInsvfjeaN6O+wQGpRnGVglS1XWrGQr8K7D04AfMmvPodDSw94U9KyFtsVLzWH9kmlPyFM+G4jbmkqg==", 28 | "license": "Apache-2.0", 29 | "dependencies": { 30 | "google-auth-library": "^9.14.2", 31 | "ws": "^8.18.0", 32 | "zod": "^3.22.4", 33 | "zod-to-json-schema": "^3.22.4" 34 | }, 35 | "engines": { 36 | "node": ">=18.0.0" 37 | } 38 | }, 39 | "node_modules/@modelcontextprotocol/sdk": { 40 | "version": "1.9.0", 41 | "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.9.0.tgz", 42 | "integrity": "sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==", 43 | "license": "MIT", 44 | "dependencies": { 45 | "content-type": "^1.0.5", 46 | "cors": "^2.8.5", 47 | "cross-spawn": "^7.0.3", 48 | "eventsource": "^3.0.2", 49 | "express": "^5.0.1", 50 | "express-rate-limit": "^7.5.0", 51 | "pkce-challenge": "^5.0.0", 52 | "raw-body": "^3.0.0", 53 | "zod": "^3.23.8", 54 | "zod-to-json-schema": "^3.24.1" 55 | }, 56 | "engines": { 57 | "node": ">=18" 58 | } 59 | }, 60 | "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { 61 | "version": "2.0.0", 62 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", 63 | "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", 64 | "license": "MIT", 65 | "dependencies": { 66 | "mime-types": "^3.0.0", 67 | "negotiator": "^1.0.0" 68 | }, 69 | "engines": { 70 | "node": ">= 0.6" 71 | } 72 | }, 73 | "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { 74 | "version": "2.2.0", 75 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", 76 | "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", 77 | "license": "MIT", 78 | "dependencies": { 79 | "bytes": "^3.1.2", 80 | "content-type": "^1.0.5", 81 | "debug": "^4.4.0", 82 | "http-errors": "^2.0.0", 83 | "iconv-lite": "^0.6.3", 84 | "on-finished": "^2.4.1", 85 | "qs": "^6.14.0", 86 | "raw-body": "^3.0.0", 87 | "type-is": "^2.0.0" 88 | }, 89 | "engines": { 90 | "node": ">=18" 91 | } 92 | }, 93 | "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { 94 | "version": "1.0.0", 95 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", 96 | "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", 97 | "license": "MIT", 98 | "dependencies": { 99 | "safe-buffer": "5.2.1" 100 | }, 101 | "engines": { 102 | "node": ">= 0.6" 103 | } 104 | }, 105 | "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { 106 | "version": "1.2.2", 107 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", 108 | "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", 109 | "license": "MIT", 110 | "engines": { 111 | "node": ">=6.6.0" 112 | } 113 | }, 114 | "node_modules/@modelcontextprotocol/sdk/node_modules/debug": { 115 | "version": "4.4.0", 116 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 117 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 118 | "license": "MIT", 119 | "dependencies": { 120 | "ms": "^2.1.3" 121 | }, 122 | "engines": { 123 | "node": ">=6.0" 124 | }, 125 | "peerDependenciesMeta": { 126 | "supports-color": { 127 | "optional": true 128 | } 129 | } 130 | }, 131 | "node_modules/@modelcontextprotocol/sdk/node_modules/express": { 132 | "version": "5.1.0", 133 | "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", 134 | "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", 135 | "license": "MIT", 136 | "dependencies": { 137 | "accepts": "^2.0.0", 138 | "body-parser": "^2.2.0", 139 | "content-disposition": "^1.0.0", 140 | "content-type": "^1.0.5", 141 | "cookie": "^0.7.1", 142 | "cookie-signature": "^1.2.1", 143 | "debug": "^4.4.0", 144 | "encodeurl": "^2.0.0", 145 | "escape-html": "^1.0.3", 146 | "etag": "^1.8.1", 147 | "finalhandler": "^2.1.0", 148 | "fresh": "^2.0.0", 149 | "http-errors": "^2.0.0", 150 | "merge-descriptors": "^2.0.0", 151 | "mime-types": "^3.0.0", 152 | "on-finished": "^2.4.1", 153 | "once": "^1.4.0", 154 | "parseurl": "^1.3.3", 155 | "proxy-addr": "^2.0.7", 156 | "qs": "^6.14.0", 157 | "range-parser": "^1.2.1", 158 | "router": "^2.2.0", 159 | "send": "^1.1.0", 160 | "serve-static": "^2.2.0", 161 | "statuses": "^2.0.1", 162 | "type-is": "^2.0.1", 163 | "vary": "^1.1.2" 164 | }, 165 | "engines": { 166 | "node": ">= 18" 167 | }, 168 | "funding": { 169 | "type": "opencollective", 170 | "url": "https://opencollective.com/express" 171 | } 172 | }, 173 | "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { 174 | "version": "2.1.0", 175 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", 176 | "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", 177 | "license": "MIT", 178 | "dependencies": { 179 | "debug": "^4.4.0", 180 | "encodeurl": "^2.0.0", 181 | "escape-html": "^1.0.3", 182 | "on-finished": "^2.4.1", 183 | "parseurl": "^1.3.3", 184 | "statuses": "^2.0.1" 185 | }, 186 | "engines": { 187 | "node": ">= 0.8" 188 | } 189 | }, 190 | "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { 191 | "version": "2.0.0", 192 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", 193 | "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", 194 | "license": "MIT", 195 | "engines": { 196 | "node": ">= 0.8" 197 | } 198 | }, 199 | "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { 200 | "version": "0.6.3", 201 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 202 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 203 | "license": "MIT", 204 | "dependencies": { 205 | "safer-buffer": ">= 2.1.2 < 3.0.0" 206 | }, 207 | "engines": { 208 | "node": ">=0.10.0" 209 | } 210 | }, 211 | "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { 212 | "version": "1.1.0", 213 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", 214 | "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", 215 | "license": "MIT", 216 | "engines": { 217 | "node": ">= 0.8" 218 | } 219 | }, 220 | "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { 221 | "version": "2.0.0", 222 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", 223 | "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", 224 | "license": "MIT", 225 | "engines": { 226 | "node": ">=18" 227 | }, 228 | "funding": { 229 | "url": "https://github.com/sponsors/sindresorhus" 230 | } 231 | }, 232 | "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { 233 | "version": "1.54.0", 234 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", 235 | "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", 236 | "license": "MIT", 237 | "engines": { 238 | "node": ">= 0.6" 239 | } 240 | }, 241 | "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { 242 | "version": "3.0.1", 243 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", 244 | "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", 245 | "license": "MIT", 246 | "dependencies": { 247 | "mime-db": "^1.54.0" 248 | }, 249 | "engines": { 250 | "node": ">= 0.6" 251 | } 252 | }, 253 | "node_modules/@modelcontextprotocol/sdk/node_modules/ms": { 254 | "version": "2.1.3", 255 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 256 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 257 | "license": "MIT" 258 | }, 259 | "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { 260 | "version": "1.0.0", 261 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", 262 | "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", 263 | "license": "MIT", 264 | "engines": { 265 | "node": ">= 0.6" 266 | } 267 | }, 268 | "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { 269 | "version": "6.14.0", 270 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", 271 | "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", 272 | "license": "BSD-3-Clause", 273 | "dependencies": { 274 | "side-channel": "^1.1.0" 275 | }, 276 | "engines": { 277 | "node": ">=0.6" 278 | }, 279 | "funding": { 280 | "url": "https://github.com/sponsors/ljharb" 281 | } 282 | }, 283 | "node_modules/@modelcontextprotocol/sdk/node_modules/send": { 284 | "version": "1.2.0", 285 | "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", 286 | "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", 287 | "license": "MIT", 288 | "dependencies": { 289 | "debug": "^4.3.5", 290 | "encodeurl": "^2.0.0", 291 | "escape-html": "^1.0.3", 292 | "etag": "^1.8.1", 293 | "fresh": "^2.0.0", 294 | "http-errors": "^2.0.0", 295 | "mime-types": "^3.0.1", 296 | "ms": "^2.1.3", 297 | "on-finished": "^2.4.1", 298 | "range-parser": "^1.2.1", 299 | "statuses": "^2.0.1" 300 | }, 301 | "engines": { 302 | "node": ">= 18" 303 | } 304 | }, 305 | "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { 306 | "version": "2.2.0", 307 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", 308 | "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", 309 | "license": "MIT", 310 | "dependencies": { 311 | "encodeurl": "^2.0.0", 312 | "escape-html": "^1.0.3", 313 | "parseurl": "^1.3.3", 314 | "send": "^1.2.0" 315 | }, 316 | "engines": { 317 | "node": ">= 18" 318 | } 319 | }, 320 | "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { 321 | "version": "2.0.1", 322 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", 323 | "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", 324 | "license": "MIT", 325 | "dependencies": { 326 | "content-type": "^1.0.5", 327 | "media-typer": "^1.1.0", 328 | "mime-types": "^3.0.0" 329 | }, 330 | "engines": { 331 | "node": ">= 0.6" 332 | } 333 | }, 334 | "node_modules/@types/body-parser": { 335 | "version": "1.19.5", 336 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", 337 | "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", 338 | "license": "MIT", 339 | "dependencies": { 340 | "@types/connect": "*", 341 | "@types/node": "*" 342 | } 343 | }, 344 | "node_modules/@types/connect": { 345 | "version": "3.4.38", 346 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", 347 | "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", 348 | "license": "MIT", 349 | "dependencies": { 350 | "@types/node": "*" 351 | } 352 | }, 353 | "node_modules/@types/express": { 354 | "version": "4.17.21", 355 | "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", 356 | "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", 357 | "license": "MIT", 358 | "dependencies": { 359 | "@types/body-parser": "*", 360 | "@types/express-serve-static-core": "^4.17.33", 361 | "@types/qs": "*", 362 | "@types/serve-static": "*" 363 | } 364 | }, 365 | "node_modules/@types/express-serve-static-core": { 366 | "version": "4.19.6", 367 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", 368 | "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", 369 | "license": "MIT", 370 | "dependencies": { 371 | "@types/node": "*", 372 | "@types/qs": "*", 373 | "@types/range-parser": "*", 374 | "@types/send": "*" 375 | } 376 | }, 377 | "node_modules/@types/http-errors": { 378 | "version": "2.0.4", 379 | "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", 380 | "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", 381 | "license": "MIT" 382 | }, 383 | "node_modules/@types/mime": { 384 | "version": "1.3.5", 385 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", 386 | "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", 387 | "license": "MIT" 388 | }, 389 | "node_modules/@types/node": { 390 | "version": "20.17.30", 391 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz", 392 | "integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==", 393 | "license": "MIT", 394 | "dependencies": { 395 | "undici-types": "~6.19.2" 396 | } 397 | }, 398 | "node_modules/@types/qs": { 399 | "version": "6.9.18", 400 | "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", 401 | "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", 402 | "license": "MIT" 403 | }, 404 | "node_modules/@types/range-parser": { 405 | "version": "1.2.7", 406 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", 407 | "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", 408 | "license": "MIT" 409 | }, 410 | "node_modules/@types/send": { 411 | "version": "0.17.4", 412 | "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", 413 | "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", 414 | "license": "MIT", 415 | "dependencies": { 416 | "@types/mime": "^1", 417 | "@types/node": "*" 418 | } 419 | }, 420 | "node_modules/@types/serve-static": { 421 | "version": "1.15.7", 422 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", 423 | "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", 424 | "license": "MIT", 425 | "dependencies": { 426 | "@types/http-errors": "*", 427 | "@types/node": "*", 428 | "@types/send": "*" 429 | } 430 | }, 431 | "node_modules/@types/uuid": { 432 | "version": "10.0.0", 433 | "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", 434 | "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", 435 | "license": "MIT" 436 | }, 437 | "node_modules/accepts": { 438 | "version": "1.3.8", 439 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 440 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 441 | "license": "MIT", 442 | "dependencies": { 443 | "mime-types": "~2.1.34", 444 | "negotiator": "0.6.3" 445 | }, 446 | "engines": { 447 | "node": ">= 0.6" 448 | } 449 | }, 450 | "node_modules/agent-base": { 451 | "version": "7.1.3", 452 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", 453 | "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", 454 | "license": "MIT", 455 | "engines": { 456 | "node": ">= 14" 457 | } 458 | }, 459 | "node_modules/array-flatten": { 460 | "version": "1.1.1", 461 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 462 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", 463 | "license": "MIT" 464 | }, 465 | "node_modules/base64-js": { 466 | "version": "1.5.1", 467 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 468 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 469 | "funding": [ 470 | { 471 | "type": "github", 472 | "url": "https://github.com/sponsors/feross" 473 | }, 474 | { 475 | "type": "patreon", 476 | "url": "https://www.patreon.com/feross" 477 | }, 478 | { 479 | "type": "consulting", 480 | "url": "https://feross.org/support" 481 | } 482 | ], 483 | "license": "MIT" 484 | }, 485 | "node_modules/bignumber.js": { 486 | "version": "9.2.1", 487 | "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.2.1.tgz", 488 | "integrity": "sha512-+NzaKgOUvInq9TIUZ1+DRspzf/HApkCwD4btfuasFTdrfnOxqx853TgDpMolp+uv4RpRp7bPcEU2zKr9+fRmyw==", 489 | "license": "MIT", 490 | "engines": { 491 | "node": "*" 492 | } 493 | }, 494 | "node_modules/body-parser": { 495 | "version": "1.20.3", 496 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", 497 | "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", 498 | "license": "MIT", 499 | "dependencies": { 500 | "bytes": "3.1.2", 501 | "content-type": "~1.0.5", 502 | "debug": "2.6.9", 503 | "depd": "2.0.0", 504 | "destroy": "1.2.0", 505 | "http-errors": "2.0.0", 506 | "iconv-lite": "0.4.24", 507 | "on-finished": "2.4.1", 508 | "qs": "6.13.0", 509 | "raw-body": "2.5.2", 510 | "type-is": "~1.6.18", 511 | "unpipe": "1.0.0" 512 | }, 513 | "engines": { 514 | "node": ">= 0.8", 515 | "npm": "1.2.8000 || >= 1.4.16" 516 | } 517 | }, 518 | "node_modules/body-parser/node_modules/raw-body": { 519 | "version": "2.5.2", 520 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 521 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 522 | "license": "MIT", 523 | "dependencies": { 524 | "bytes": "3.1.2", 525 | "http-errors": "2.0.0", 526 | "iconv-lite": "0.4.24", 527 | "unpipe": "1.0.0" 528 | }, 529 | "engines": { 530 | "node": ">= 0.8" 531 | } 532 | }, 533 | "node_modules/buffer-equal-constant-time": { 534 | "version": "1.0.1", 535 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 536 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", 537 | "license": "BSD-3-Clause" 538 | }, 539 | "node_modules/bytes": { 540 | "version": "3.1.2", 541 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 542 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 543 | "license": "MIT", 544 | "engines": { 545 | "node": ">= 0.8" 546 | } 547 | }, 548 | "node_modules/call-bind-apply-helpers": { 549 | "version": "1.0.2", 550 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 551 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 552 | "license": "MIT", 553 | "dependencies": { 554 | "es-errors": "^1.3.0", 555 | "function-bind": "^1.1.2" 556 | }, 557 | "engines": { 558 | "node": ">= 0.4" 559 | } 560 | }, 561 | "node_modules/call-bound": { 562 | "version": "1.0.4", 563 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 564 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 565 | "license": "MIT", 566 | "dependencies": { 567 | "call-bind-apply-helpers": "^1.0.2", 568 | "get-intrinsic": "^1.3.0" 569 | }, 570 | "engines": { 571 | "node": ">= 0.4" 572 | }, 573 | "funding": { 574 | "url": "https://github.com/sponsors/ljharb" 575 | } 576 | }, 577 | "node_modules/content-disposition": { 578 | "version": "0.5.4", 579 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 580 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 581 | "license": "MIT", 582 | "dependencies": { 583 | "safe-buffer": "5.2.1" 584 | }, 585 | "engines": { 586 | "node": ">= 0.6" 587 | } 588 | }, 589 | "node_modules/content-type": { 590 | "version": "1.0.5", 591 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 592 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 593 | "license": "MIT", 594 | "engines": { 595 | "node": ">= 0.6" 596 | } 597 | }, 598 | "node_modules/cookie": { 599 | "version": "0.7.1", 600 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", 601 | "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", 602 | "license": "MIT", 603 | "engines": { 604 | "node": ">= 0.6" 605 | } 606 | }, 607 | "node_modules/cookie-signature": { 608 | "version": "1.0.6", 609 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 610 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", 611 | "license": "MIT" 612 | }, 613 | "node_modules/cors": { 614 | "version": "2.8.5", 615 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 616 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 617 | "license": "MIT", 618 | "dependencies": { 619 | "object-assign": "^4", 620 | "vary": "^1" 621 | }, 622 | "engines": { 623 | "node": ">= 0.10" 624 | } 625 | }, 626 | "node_modules/cross-spawn": { 627 | "version": "7.0.6", 628 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 629 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 630 | "license": "MIT", 631 | "dependencies": { 632 | "path-key": "^3.1.0", 633 | "shebang-command": "^2.0.0", 634 | "which": "^2.0.1" 635 | }, 636 | "engines": { 637 | "node": ">= 8" 638 | } 639 | }, 640 | "node_modules/debug": { 641 | "version": "2.6.9", 642 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 643 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 644 | "license": "MIT", 645 | "dependencies": { 646 | "ms": "2.0.0" 647 | } 648 | }, 649 | "node_modules/depd": { 650 | "version": "2.0.0", 651 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 652 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 653 | "license": "MIT", 654 | "engines": { 655 | "node": ">= 0.8" 656 | } 657 | }, 658 | "node_modules/destroy": { 659 | "version": "1.2.0", 660 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 661 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 662 | "license": "MIT", 663 | "engines": { 664 | "node": ">= 0.8", 665 | "npm": "1.2.8000 || >= 1.4.16" 666 | } 667 | }, 668 | "node_modules/dotenv": { 669 | "version": "16.5.0", 670 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", 671 | "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", 672 | "license": "BSD-2-Clause", 673 | "engines": { 674 | "node": ">=12" 675 | }, 676 | "funding": { 677 | "url": "https://dotenvx.com" 678 | } 679 | }, 680 | "node_modules/dunder-proto": { 681 | "version": "1.0.1", 682 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 683 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 684 | "license": "MIT", 685 | "dependencies": { 686 | "call-bind-apply-helpers": "^1.0.1", 687 | "es-errors": "^1.3.0", 688 | "gopd": "^1.2.0" 689 | }, 690 | "engines": { 691 | "node": ">= 0.4" 692 | } 693 | }, 694 | "node_modules/ecdsa-sig-formatter": { 695 | "version": "1.0.11", 696 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 697 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 698 | "license": "Apache-2.0", 699 | "dependencies": { 700 | "safe-buffer": "^5.0.1" 701 | } 702 | }, 703 | "node_modules/ee-first": { 704 | "version": "1.1.1", 705 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 706 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 707 | "license": "MIT" 708 | }, 709 | "node_modules/encodeurl": { 710 | "version": "2.0.0", 711 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 712 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 713 | "license": "MIT", 714 | "engines": { 715 | "node": ">= 0.8" 716 | } 717 | }, 718 | "node_modules/es-define-property": { 719 | "version": "1.0.1", 720 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 721 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 722 | "license": "MIT", 723 | "engines": { 724 | "node": ">= 0.4" 725 | } 726 | }, 727 | "node_modules/es-errors": { 728 | "version": "1.3.0", 729 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 730 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 731 | "license": "MIT", 732 | "engines": { 733 | "node": ">= 0.4" 734 | } 735 | }, 736 | "node_modules/es-object-atoms": { 737 | "version": "1.1.1", 738 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 739 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 740 | "license": "MIT", 741 | "dependencies": { 742 | "es-errors": "^1.3.0" 743 | }, 744 | "engines": { 745 | "node": ">= 0.4" 746 | } 747 | }, 748 | "node_modules/escape-html": { 749 | "version": "1.0.3", 750 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 751 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 752 | "license": "MIT" 753 | }, 754 | "node_modules/etag": { 755 | "version": "1.8.1", 756 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 757 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 758 | "license": "MIT", 759 | "engines": { 760 | "node": ">= 0.6" 761 | } 762 | }, 763 | "node_modules/eventsource": { 764 | "version": "3.0.6", 765 | "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", 766 | "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", 767 | "license": "MIT", 768 | "dependencies": { 769 | "eventsource-parser": "^3.0.1" 770 | }, 771 | "engines": { 772 | "node": ">=18.0.0" 773 | } 774 | }, 775 | "node_modules/eventsource-parser": { 776 | "version": "3.0.1", 777 | "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", 778 | "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", 779 | "license": "MIT", 780 | "engines": { 781 | "node": ">=18.0.0" 782 | } 783 | }, 784 | "node_modules/express": { 785 | "version": "4.21.2", 786 | "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", 787 | "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", 788 | "license": "MIT", 789 | "dependencies": { 790 | "accepts": "~1.3.8", 791 | "array-flatten": "1.1.1", 792 | "body-parser": "1.20.3", 793 | "content-disposition": "0.5.4", 794 | "content-type": "~1.0.4", 795 | "cookie": "0.7.1", 796 | "cookie-signature": "1.0.6", 797 | "debug": "2.6.9", 798 | "depd": "2.0.0", 799 | "encodeurl": "~2.0.0", 800 | "escape-html": "~1.0.3", 801 | "etag": "~1.8.1", 802 | "finalhandler": "1.3.1", 803 | "fresh": "0.5.2", 804 | "http-errors": "2.0.0", 805 | "merge-descriptors": "1.0.3", 806 | "methods": "~1.1.2", 807 | "on-finished": "2.4.1", 808 | "parseurl": "~1.3.3", 809 | "path-to-regexp": "0.1.12", 810 | "proxy-addr": "~2.0.7", 811 | "qs": "6.13.0", 812 | "range-parser": "~1.2.1", 813 | "safe-buffer": "5.2.1", 814 | "send": "0.19.0", 815 | "serve-static": "1.16.2", 816 | "setprototypeof": "1.2.0", 817 | "statuses": "2.0.1", 818 | "type-is": "~1.6.18", 819 | "utils-merge": "1.0.1", 820 | "vary": "~1.1.2" 821 | }, 822 | "engines": { 823 | "node": ">= 0.10.0" 824 | }, 825 | "funding": { 826 | "type": "opencollective", 827 | "url": "https://opencollective.com/express" 828 | } 829 | }, 830 | "node_modules/express-rate-limit": { 831 | "version": "7.5.0", 832 | "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", 833 | "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", 834 | "license": "MIT", 835 | "engines": { 836 | "node": ">= 16" 837 | }, 838 | "funding": { 839 | "url": "https://github.com/sponsors/express-rate-limit" 840 | }, 841 | "peerDependencies": { 842 | "express": "^4.11 || 5 || ^5.0.0-beta.1" 843 | } 844 | }, 845 | "node_modules/extend": { 846 | "version": "3.0.2", 847 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 848 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", 849 | "license": "MIT" 850 | }, 851 | "node_modules/finalhandler": { 852 | "version": "1.3.1", 853 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", 854 | "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", 855 | "license": "MIT", 856 | "dependencies": { 857 | "debug": "2.6.9", 858 | "encodeurl": "~2.0.0", 859 | "escape-html": "~1.0.3", 860 | "on-finished": "2.4.1", 861 | "parseurl": "~1.3.3", 862 | "statuses": "2.0.1", 863 | "unpipe": "~1.0.0" 864 | }, 865 | "engines": { 866 | "node": ">= 0.8" 867 | } 868 | }, 869 | "node_modules/forwarded": { 870 | "version": "0.2.0", 871 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 872 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 873 | "license": "MIT", 874 | "engines": { 875 | "node": ">= 0.6" 876 | } 877 | }, 878 | "node_modules/fresh": { 879 | "version": "0.5.2", 880 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 881 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 882 | "license": "MIT", 883 | "engines": { 884 | "node": ">= 0.6" 885 | } 886 | }, 887 | "node_modules/function-bind": { 888 | "version": "1.1.2", 889 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 890 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 891 | "license": "MIT", 892 | "funding": { 893 | "url": "https://github.com/sponsors/ljharb" 894 | } 895 | }, 896 | "node_modules/gaxios": { 897 | "version": "6.7.1", 898 | "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", 899 | "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", 900 | "license": "Apache-2.0", 901 | "dependencies": { 902 | "extend": "^3.0.2", 903 | "https-proxy-agent": "^7.0.1", 904 | "is-stream": "^2.0.0", 905 | "node-fetch": "^2.6.9", 906 | "uuid": "^9.0.1" 907 | }, 908 | "engines": { 909 | "node": ">=14" 910 | } 911 | }, 912 | "node_modules/gaxios/node_modules/uuid": { 913 | "version": "9.0.1", 914 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", 915 | "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", 916 | "funding": [ 917 | "https://github.com/sponsors/broofa", 918 | "https://github.com/sponsors/ctavan" 919 | ], 920 | "license": "MIT", 921 | "bin": { 922 | "uuid": "dist/bin/uuid" 923 | } 924 | }, 925 | "node_modules/gcp-metadata": { 926 | "version": "6.1.1", 927 | "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", 928 | "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", 929 | "license": "Apache-2.0", 930 | "dependencies": { 931 | "gaxios": "^6.1.1", 932 | "google-logging-utils": "^0.0.2", 933 | "json-bigint": "^1.0.0" 934 | }, 935 | "engines": { 936 | "node": ">=14" 937 | } 938 | }, 939 | "node_modules/get-intrinsic": { 940 | "version": "1.3.0", 941 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 942 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 943 | "license": "MIT", 944 | "dependencies": { 945 | "call-bind-apply-helpers": "^1.0.2", 946 | "es-define-property": "^1.0.1", 947 | "es-errors": "^1.3.0", 948 | "es-object-atoms": "^1.1.1", 949 | "function-bind": "^1.1.2", 950 | "get-proto": "^1.0.1", 951 | "gopd": "^1.2.0", 952 | "has-symbols": "^1.1.0", 953 | "hasown": "^2.0.2", 954 | "math-intrinsics": "^1.1.0" 955 | }, 956 | "engines": { 957 | "node": ">= 0.4" 958 | }, 959 | "funding": { 960 | "url": "https://github.com/sponsors/ljharb" 961 | } 962 | }, 963 | "node_modules/get-proto": { 964 | "version": "1.0.1", 965 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 966 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 967 | "license": "MIT", 968 | "dependencies": { 969 | "dunder-proto": "^1.0.1", 970 | "es-object-atoms": "^1.0.0" 971 | }, 972 | "engines": { 973 | "node": ">= 0.4" 974 | } 975 | }, 976 | "node_modules/google-auth-library": { 977 | "version": "9.15.1", 978 | "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", 979 | "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", 980 | "license": "Apache-2.0", 981 | "dependencies": { 982 | "base64-js": "^1.3.0", 983 | "ecdsa-sig-formatter": "^1.0.11", 984 | "gaxios": "^6.1.1", 985 | "gcp-metadata": "^6.1.0", 986 | "gtoken": "^7.0.0", 987 | "jws": "^4.0.0" 988 | }, 989 | "engines": { 990 | "node": ">=14" 991 | } 992 | }, 993 | "node_modules/google-logging-utils": { 994 | "version": "0.0.2", 995 | "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", 996 | "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", 997 | "license": "Apache-2.0", 998 | "engines": { 999 | "node": ">=14" 1000 | } 1001 | }, 1002 | "node_modules/gopd": { 1003 | "version": "1.2.0", 1004 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 1005 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 1006 | "license": "MIT", 1007 | "engines": { 1008 | "node": ">= 0.4" 1009 | }, 1010 | "funding": { 1011 | "url": "https://github.com/sponsors/ljharb" 1012 | } 1013 | }, 1014 | "node_modules/gtoken": { 1015 | "version": "7.1.0", 1016 | "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", 1017 | "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", 1018 | "license": "MIT", 1019 | "dependencies": { 1020 | "gaxios": "^6.0.0", 1021 | "jws": "^4.0.0" 1022 | }, 1023 | "engines": { 1024 | "node": ">=14.0.0" 1025 | } 1026 | }, 1027 | "node_modules/has-symbols": { 1028 | "version": "1.1.0", 1029 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 1030 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 1031 | "license": "MIT", 1032 | "engines": { 1033 | "node": ">= 0.4" 1034 | }, 1035 | "funding": { 1036 | "url": "https://github.com/sponsors/ljharb" 1037 | } 1038 | }, 1039 | "node_modules/hasown": { 1040 | "version": "2.0.2", 1041 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 1042 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 1043 | "license": "MIT", 1044 | "dependencies": { 1045 | "function-bind": "^1.1.2" 1046 | }, 1047 | "engines": { 1048 | "node": ">= 0.4" 1049 | } 1050 | }, 1051 | "node_modules/http-errors": { 1052 | "version": "2.0.0", 1053 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 1054 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 1055 | "license": "MIT", 1056 | "dependencies": { 1057 | "depd": "2.0.0", 1058 | "inherits": "2.0.4", 1059 | "setprototypeof": "1.2.0", 1060 | "statuses": "2.0.1", 1061 | "toidentifier": "1.0.1" 1062 | }, 1063 | "engines": { 1064 | "node": ">= 0.8" 1065 | } 1066 | }, 1067 | "node_modules/https-proxy-agent": { 1068 | "version": "7.0.6", 1069 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", 1070 | "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", 1071 | "license": "MIT", 1072 | "dependencies": { 1073 | "agent-base": "^7.1.2", 1074 | "debug": "4" 1075 | }, 1076 | "engines": { 1077 | "node": ">= 14" 1078 | } 1079 | }, 1080 | "node_modules/https-proxy-agent/node_modules/debug": { 1081 | "version": "4.4.0", 1082 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 1083 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 1084 | "license": "MIT", 1085 | "dependencies": { 1086 | "ms": "^2.1.3" 1087 | }, 1088 | "engines": { 1089 | "node": ">=6.0" 1090 | }, 1091 | "peerDependenciesMeta": { 1092 | "supports-color": { 1093 | "optional": true 1094 | } 1095 | } 1096 | }, 1097 | "node_modules/https-proxy-agent/node_modules/ms": { 1098 | "version": "2.1.3", 1099 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1100 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1101 | "license": "MIT" 1102 | }, 1103 | "node_modules/iconv-lite": { 1104 | "version": "0.4.24", 1105 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 1106 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 1107 | "license": "MIT", 1108 | "dependencies": { 1109 | "safer-buffer": ">= 2.1.2 < 3" 1110 | }, 1111 | "engines": { 1112 | "node": ">=0.10.0" 1113 | } 1114 | }, 1115 | "node_modules/inherits": { 1116 | "version": "2.0.4", 1117 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1118 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 1119 | "license": "ISC" 1120 | }, 1121 | "node_modules/ipaddr.js": { 1122 | "version": "1.9.1", 1123 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 1124 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 1125 | "license": "MIT", 1126 | "engines": { 1127 | "node": ">= 0.10" 1128 | } 1129 | }, 1130 | "node_modules/is-promise": { 1131 | "version": "4.0.0", 1132 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", 1133 | "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", 1134 | "license": "MIT" 1135 | }, 1136 | "node_modules/is-stream": { 1137 | "version": "2.0.1", 1138 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", 1139 | "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", 1140 | "license": "MIT", 1141 | "engines": { 1142 | "node": ">=8" 1143 | }, 1144 | "funding": { 1145 | "url": "https://github.com/sponsors/sindresorhus" 1146 | } 1147 | }, 1148 | "node_modules/isexe": { 1149 | "version": "2.0.0", 1150 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 1151 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 1152 | "license": "ISC" 1153 | }, 1154 | "node_modules/json-bigint": { 1155 | "version": "1.0.0", 1156 | "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", 1157 | "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", 1158 | "license": "MIT", 1159 | "dependencies": { 1160 | "bignumber.js": "^9.0.0" 1161 | } 1162 | }, 1163 | "node_modules/jwa": { 1164 | "version": "2.0.0", 1165 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", 1166 | "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", 1167 | "license": "MIT", 1168 | "dependencies": { 1169 | "buffer-equal-constant-time": "1.0.1", 1170 | "ecdsa-sig-formatter": "1.0.11", 1171 | "safe-buffer": "^5.0.1" 1172 | } 1173 | }, 1174 | "node_modules/jws": { 1175 | "version": "4.0.0", 1176 | "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", 1177 | "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", 1178 | "license": "MIT", 1179 | "dependencies": { 1180 | "jwa": "^2.0.0", 1181 | "safe-buffer": "^5.0.1" 1182 | } 1183 | }, 1184 | "node_modules/math-intrinsics": { 1185 | "version": "1.1.0", 1186 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 1187 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 1188 | "license": "MIT", 1189 | "engines": { 1190 | "node": ">= 0.4" 1191 | } 1192 | }, 1193 | "node_modules/media-typer": { 1194 | "version": "0.3.0", 1195 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 1196 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 1197 | "license": "MIT", 1198 | "engines": { 1199 | "node": ">= 0.6" 1200 | } 1201 | }, 1202 | "node_modules/merge-descriptors": { 1203 | "version": "1.0.3", 1204 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", 1205 | "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", 1206 | "license": "MIT", 1207 | "funding": { 1208 | "url": "https://github.com/sponsors/sindresorhus" 1209 | } 1210 | }, 1211 | "node_modules/methods": { 1212 | "version": "1.1.2", 1213 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 1214 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 1215 | "license": "MIT", 1216 | "engines": { 1217 | "node": ">= 0.6" 1218 | } 1219 | }, 1220 | "node_modules/mime": { 1221 | "version": "1.6.0", 1222 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 1223 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 1224 | "license": "MIT", 1225 | "bin": { 1226 | "mime": "cli.js" 1227 | }, 1228 | "engines": { 1229 | "node": ">=4" 1230 | } 1231 | }, 1232 | "node_modules/mime-db": { 1233 | "version": "1.52.0", 1234 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 1235 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 1236 | "license": "MIT", 1237 | "engines": { 1238 | "node": ">= 0.6" 1239 | } 1240 | }, 1241 | "node_modules/mime-types": { 1242 | "version": "2.1.35", 1243 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 1244 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 1245 | "license": "MIT", 1246 | "dependencies": { 1247 | "mime-db": "1.52.0" 1248 | }, 1249 | "engines": { 1250 | "node": ">= 0.6" 1251 | } 1252 | }, 1253 | "node_modules/ms": { 1254 | "version": "2.0.0", 1255 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1256 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 1257 | "license": "MIT" 1258 | }, 1259 | "node_modules/negotiator": { 1260 | "version": "0.6.3", 1261 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 1262 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 1263 | "license": "MIT", 1264 | "engines": { 1265 | "node": ">= 0.6" 1266 | } 1267 | }, 1268 | "node_modules/node-fetch": { 1269 | "version": "2.7.0", 1270 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", 1271 | "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", 1272 | "license": "MIT", 1273 | "dependencies": { 1274 | "whatwg-url": "^5.0.0" 1275 | }, 1276 | "engines": { 1277 | "node": "4.x || >=6.0.0" 1278 | }, 1279 | "peerDependencies": { 1280 | "encoding": "^0.1.0" 1281 | }, 1282 | "peerDependenciesMeta": { 1283 | "encoding": { 1284 | "optional": true 1285 | } 1286 | } 1287 | }, 1288 | "node_modules/object-assign": { 1289 | "version": "4.1.1", 1290 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 1291 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 1292 | "license": "MIT", 1293 | "engines": { 1294 | "node": ">=0.10.0" 1295 | } 1296 | }, 1297 | "node_modules/object-inspect": { 1298 | "version": "1.13.4", 1299 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 1300 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 1301 | "license": "MIT", 1302 | "engines": { 1303 | "node": ">= 0.4" 1304 | }, 1305 | "funding": { 1306 | "url": "https://github.com/sponsors/ljharb" 1307 | } 1308 | }, 1309 | "node_modules/on-finished": { 1310 | "version": "2.4.1", 1311 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 1312 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 1313 | "license": "MIT", 1314 | "dependencies": { 1315 | "ee-first": "1.1.1" 1316 | }, 1317 | "engines": { 1318 | "node": ">= 0.8" 1319 | } 1320 | }, 1321 | "node_modules/once": { 1322 | "version": "1.4.0", 1323 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1324 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 1325 | "license": "ISC", 1326 | "dependencies": { 1327 | "wrappy": "1" 1328 | } 1329 | }, 1330 | "node_modules/parseurl": { 1331 | "version": "1.3.3", 1332 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 1333 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 1334 | "license": "MIT", 1335 | "engines": { 1336 | "node": ">= 0.8" 1337 | } 1338 | }, 1339 | "node_modules/path-key": { 1340 | "version": "3.1.1", 1341 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 1342 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 1343 | "license": "MIT", 1344 | "engines": { 1345 | "node": ">=8" 1346 | } 1347 | }, 1348 | "node_modules/path-to-regexp": { 1349 | "version": "0.1.12", 1350 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", 1351 | "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", 1352 | "license": "MIT" 1353 | }, 1354 | "node_modules/pkce-challenge": { 1355 | "version": "5.0.0", 1356 | "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", 1357 | "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", 1358 | "license": "MIT", 1359 | "engines": { 1360 | "node": ">=16.20.0" 1361 | } 1362 | }, 1363 | "node_modules/proxy-addr": { 1364 | "version": "2.0.7", 1365 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 1366 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 1367 | "license": "MIT", 1368 | "dependencies": { 1369 | "forwarded": "0.2.0", 1370 | "ipaddr.js": "1.9.1" 1371 | }, 1372 | "engines": { 1373 | "node": ">= 0.10" 1374 | } 1375 | }, 1376 | "node_modules/qs": { 1377 | "version": "6.13.0", 1378 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", 1379 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", 1380 | "license": "BSD-3-Clause", 1381 | "dependencies": { 1382 | "side-channel": "^1.0.6" 1383 | }, 1384 | "engines": { 1385 | "node": ">=0.6" 1386 | }, 1387 | "funding": { 1388 | "url": "https://github.com/sponsors/ljharb" 1389 | } 1390 | }, 1391 | "node_modules/range-parser": { 1392 | "version": "1.2.1", 1393 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 1394 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 1395 | "license": "MIT", 1396 | "engines": { 1397 | "node": ">= 0.6" 1398 | } 1399 | }, 1400 | "node_modules/raw-body": { 1401 | "version": "3.0.0", 1402 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", 1403 | "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", 1404 | "license": "MIT", 1405 | "dependencies": { 1406 | "bytes": "3.1.2", 1407 | "http-errors": "2.0.0", 1408 | "iconv-lite": "0.6.3", 1409 | "unpipe": "1.0.0" 1410 | }, 1411 | "engines": { 1412 | "node": ">= 0.8" 1413 | } 1414 | }, 1415 | "node_modules/raw-body/node_modules/iconv-lite": { 1416 | "version": "0.6.3", 1417 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 1418 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 1419 | "license": "MIT", 1420 | "dependencies": { 1421 | "safer-buffer": ">= 2.1.2 < 3.0.0" 1422 | }, 1423 | "engines": { 1424 | "node": ">=0.10.0" 1425 | } 1426 | }, 1427 | "node_modules/router": { 1428 | "version": "2.2.0", 1429 | "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", 1430 | "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", 1431 | "license": "MIT", 1432 | "dependencies": { 1433 | "debug": "^4.4.0", 1434 | "depd": "^2.0.0", 1435 | "is-promise": "^4.0.0", 1436 | "parseurl": "^1.3.3", 1437 | "path-to-regexp": "^8.0.0" 1438 | }, 1439 | "engines": { 1440 | "node": ">= 18" 1441 | } 1442 | }, 1443 | "node_modules/router/node_modules/debug": { 1444 | "version": "4.4.0", 1445 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 1446 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 1447 | "license": "MIT", 1448 | "dependencies": { 1449 | "ms": "^2.1.3" 1450 | }, 1451 | "engines": { 1452 | "node": ">=6.0" 1453 | }, 1454 | "peerDependenciesMeta": { 1455 | "supports-color": { 1456 | "optional": true 1457 | } 1458 | } 1459 | }, 1460 | "node_modules/router/node_modules/ms": { 1461 | "version": "2.1.3", 1462 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1463 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1464 | "license": "MIT" 1465 | }, 1466 | "node_modules/router/node_modules/path-to-regexp": { 1467 | "version": "8.2.0", 1468 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", 1469 | "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", 1470 | "license": "MIT", 1471 | "engines": { 1472 | "node": ">=16" 1473 | } 1474 | }, 1475 | "node_modules/safe-buffer": { 1476 | "version": "5.2.1", 1477 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1478 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1479 | "funding": [ 1480 | { 1481 | "type": "github", 1482 | "url": "https://github.com/sponsors/feross" 1483 | }, 1484 | { 1485 | "type": "patreon", 1486 | "url": "https://www.patreon.com/feross" 1487 | }, 1488 | { 1489 | "type": "consulting", 1490 | "url": "https://feross.org/support" 1491 | } 1492 | ], 1493 | "license": "MIT" 1494 | }, 1495 | "node_modules/safer-buffer": { 1496 | "version": "2.1.2", 1497 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1498 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 1499 | "license": "MIT" 1500 | }, 1501 | "node_modules/send": { 1502 | "version": "0.19.0", 1503 | "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", 1504 | "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", 1505 | "license": "MIT", 1506 | "dependencies": { 1507 | "debug": "2.6.9", 1508 | "depd": "2.0.0", 1509 | "destroy": "1.2.0", 1510 | "encodeurl": "~1.0.2", 1511 | "escape-html": "~1.0.3", 1512 | "etag": "~1.8.1", 1513 | "fresh": "0.5.2", 1514 | "http-errors": "2.0.0", 1515 | "mime": "1.6.0", 1516 | "ms": "2.1.3", 1517 | "on-finished": "2.4.1", 1518 | "range-parser": "~1.2.1", 1519 | "statuses": "2.0.1" 1520 | }, 1521 | "engines": { 1522 | "node": ">= 0.8.0" 1523 | } 1524 | }, 1525 | "node_modules/send/node_modules/encodeurl": { 1526 | "version": "1.0.2", 1527 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 1528 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 1529 | "license": "MIT", 1530 | "engines": { 1531 | "node": ">= 0.8" 1532 | } 1533 | }, 1534 | "node_modules/send/node_modules/ms": { 1535 | "version": "2.1.3", 1536 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1537 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1538 | "license": "MIT" 1539 | }, 1540 | "node_modules/serve-static": { 1541 | "version": "1.16.2", 1542 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", 1543 | "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", 1544 | "license": "MIT", 1545 | "dependencies": { 1546 | "encodeurl": "~2.0.0", 1547 | "escape-html": "~1.0.3", 1548 | "parseurl": "~1.3.3", 1549 | "send": "0.19.0" 1550 | }, 1551 | "engines": { 1552 | "node": ">= 0.8.0" 1553 | } 1554 | }, 1555 | "node_modules/setprototypeof": { 1556 | "version": "1.2.0", 1557 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1558 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 1559 | "license": "ISC" 1560 | }, 1561 | "node_modules/shebang-command": { 1562 | "version": "2.0.0", 1563 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 1564 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1565 | "license": "MIT", 1566 | "dependencies": { 1567 | "shebang-regex": "^3.0.0" 1568 | }, 1569 | "engines": { 1570 | "node": ">=8" 1571 | } 1572 | }, 1573 | "node_modules/shebang-regex": { 1574 | "version": "3.0.0", 1575 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1576 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 1577 | "license": "MIT", 1578 | "engines": { 1579 | "node": ">=8" 1580 | } 1581 | }, 1582 | "node_modules/side-channel": { 1583 | "version": "1.1.0", 1584 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 1585 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 1586 | "license": "MIT", 1587 | "dependencies": { 1588 | "es-errors": "^1.3.0", 1589 | "object-inspect": "^1.13.3", 1590 | "side-channel-list": "^1.0.0", 1591 | "side-channel-map": "^1.0.1", 1592 | "side-channel-weakmap": "^1.0.2" 1593 | }, 1594 | "engines": { 1595 | "node": ">= 0.4" 1596 | }, 1597 | "funding": { 1598 | "url": "https://github.com/sponsors/ljharb" 1599 | } 1600 | }, 1601 | "node_modules/side-channel-list": { 1602 | "version": "1.0.0", 1603 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 1604 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 1605 | "license": "MIT", 1606 | "dependencies": { 1607 | "es-errors": "^1.3.0", 1608 | "object-inspect": "^1.13.3" 1609 | }, 1610 | "engines": { 1611 | "node": ">= 0.4" 1612 | }, 1613 | "funding": { 1614 | "url": "https://github.com/sponsors/ljharb" 1615 | } 1616 | }, 1617 | "node_modules/side-channel-map": { 1618 | "version": "1.0.1", 1619 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 1620 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 1621 | "license": "MIT", 1622 | "dependencies": { 1623 | "call-bound": "^1.0.2", 1624 | "es-errors": "^1.3.0", 1625 | "get-intrinsic": "^1.2.5", 1626 | "object-inspect": "^1.13.3" 1627 | }, 1628 | "engines": { 1629 | "node": ">= 0.4" 1630 | }, 1631 | "funding": { 1632 | "url": "https://github.com/sponsors/ljharb" 1633 | } 1634 | }, 1635 | "node_modules/side-channel-weakmap": { 1636 | "version": "1.0.2", 1637 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 1638 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 1639 | "license": "MIT", 1640 | "dependencies": { 1641 | "call-bound": "^1.0.2", 1642 | "es-errors": "^1.3.0", 1643 | "get-intrinsic": "^1.2.5", 1644 | "object-inspect": "^1.13.3", 1645 | "side-channel-map": "^1.0.1" 1646 | }, 1647 | "engines": { 1648 | "node": ">= 0.4" 1649 | }, 1650 | "funding": { 1651 | "url": "https://github.com/sponsors/ljharb" 1652 | } 1653 | }, 1654 | "node_modules/statuses": { 1655 | "version": "2.0.1", 1656 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1657 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 1658 | "license": "MIT", 1659 | "engines": { 1660 | "node": ">= 0.8" 1661 | } 1662 | }, 1663 | "node_modules/toidentifier": { 1664 | "version": "1.0.1", 1665 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1666 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 1667 | "license": "MIT", 1668 | "engines": { 1669 | "node": ">=0.6" 1670 | } 1671 | }, 1672 | "node_modules/tr46": { 1673 | "version": "0.0.3", 1674 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 1675 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", 1676 | "license": "MIT" 1677 | }, 1678 | "node_modules/type-is": { 1679 | "version": "1.6.18", 1680 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1681 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1682 | "license": "MIT", 1683 | "dependencies": { 1684 | "media-typer": "0.3.0", 1685 | "mime-types": "~2.1.24" 1686 | }, 1687 | "engines": { 1688 | "node": ">= 0.6" 1689 | } 1690 | }, 1691 | "node_modules/typescript": { 1692 | "version": "5.8.3", 1693 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", 1694 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 1695 | "license": "Apache-2.0", 1696 | "bin": { 1697 | "tsc": "bin/tsc", 1698 | "tsserver": "bin/tsserver" 1699 | }, 1700 | "engines": { 1701 | "node": ">=14.17" 1702 | } 1703 | }, 1704 | "node_modules/undici-types": { 1705 | "version": "6.19.8", 1706 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", 1707 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", 1708 | "license": "MIT" 1709 | }, 1710 | "node_modules/unpipe": { 1711 | "version": "1.0.0", 1712 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1713 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 1714 | "license": "MIT", 1715 | "engines": { 1716 | "node": ">= 0.8" 1717 | } 1718 | }, 1719 | "node_modules/utils-merge": { 1720 | "version": "1.0.1", 1721 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1722 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 1723 | "license": "MIT", 1724 | "engines": { 1725 | "node": ">= 0.4.0" 1726 | } 1727 | }, 1728 | "node_modules/uuid": { 1729 | "version": "11.1.0", 1730 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", 1731 | "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", 1732 | "funding": [ 1733 | "https://github.com/sponsors/broofa", 1734 | "https://github.com/sponsors/ctavan" 1735 | ], 1736 | "license": "MIT", 1737 | "bin": { 1738 | "uuid": "dist/esm/bin/uuid" 1739 | } 1740 | }, 1741 | "node_modules/vary": { 1742 | "version": "1.1.2", 1743 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1744 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 1745 | "license": "MIT", 1746 | "engines": { 1747 | "node": ">= 0.8" 1748 | } 1749 | }, 1750 | "node_modules/webidl-conversions": { 1751 | "version": "3.0.1", 1752 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 1753 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", 1754 | "license": "BSD-2-Clause" 1755 | }, 1756 | "node_modules/whatwg-url": { 1757 | "version": "5.0.0", 1758 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 1759 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 1760 | "license": "MIT", 1761 | "dependencies": { 1762 | "tr46": "~0.0.3", 1763 | "webidl-conversions": "^3.0.0" 1764 | } 1765 | }, 1766 | "node_modules/which": { 1767 | "version": "2.0.2", 1768 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1769 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1770 | "license": "ISC", 1771 | "dependencies": { 1772 | "isexe": "^2.0.0" 1773 | }, 1774 | "bin": { 1775 | "node-which": "bin/node-which" 1776 | }, 1777 | "engines": { 1778 | "node": ">= 8" 1779 | } 1780 | }, 1781 | "node_modules/wrappy": { 1782 | "version": "1.0.2", 1783 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1784 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1785 | "license": "ISC" 1786 | }, 1787 | "node_modules/ws": { 1788 | "version": "8.18.1", 1789 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", 1790 | "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", 1791 | "license": "MIT", 1792 | "engines": { 1793 | "node": ">=10.0.0" 1794 | }, 1795 | "peerDependencies": { 1796 | "bufferutil": "^4.0.1", 1797 | "utf-8-validate": ">=5.0.2" 1798 | }, 1799 | "peerDependenciesMeta": { 1800 | "bufferutil": { 1801 | "optional": true 1802 | }, 1803 | "utf-8-validate": { 1804 | "optional": true 1805 | } 1806 | } 1807 | }, 1808 | "node_modules/zod": { 1809 | "version": "3.24.2", 1810 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", 1811 | "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", 1812 | "license": "MIT", 1813 | "funding": { 1814 | "url": "https://github.com/sponsors/colinhacks" 1815 | } 1816 | }, 1817 | "node_modules/zod-to-json-schema": { 1818 | "version": "3.24.5", 1819 | "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", 1820 | "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", 1821 | "license": "ISC", 1822 | "peerDependencies": { 1823 | "zod": "^3.24.1" 1824 | } 1825 | } 1826 | } 1827 | } 1828 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-video-generation-veo2", 3 | "version": "1.0.0", 4 | "description": "MCP server for generating videos with Google Veo2", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "build": "tsc", 9 | "start": "node dist/index.js", 10 | "dev": "tsc -w & node --watch dist/index.js", 11 | "debug": "npx @modelcontextprotocol/inspector node --no-deprecation dist/index.js", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "keywords": [ 15 | "mcp", 16 | "video", 17 | "generation", 18 | "veo2", 19 | "gemini" 20 | ], 21 | "author": "", 22 | "license": "MIT", 23 | "dependencies": { 24 | "@google/genai": "^0.9.0", 25 | "@modelcontextprotocol/sdk": "latest", 26 | "@types/express": "^4.17.21", 27 | "@types/node": "^20.10.5", 28 | "@types/uuid": "^10.0.0", 29 | "dotenv": "^16.3.1", 30 | "express": "^4.18.2", 31 | "typescript": "^5.3.3", 32 | "uuid": "^11.1.0", 33 | "zod": "^3.22.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - googleApiKey 10 | properties: 11 | googleApiKey: 12 | type: string 13 | description: Your Google API key for accessing the Gemini and Veo2 services. 14 | port: 15 | type: number 16 | default: 3000 17 | description: Port for the server to listen on. 18 | storageDir: 19 | type: string 20 | default: ./generated-videos 21 | description: Directory path to store generated videos. 22 | logLevel: 23 | type: string 24 | default: fatal 25 | description: Logging level, e.g., debug, info, warn, error, fatal. 26 | commandFunction: 27 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio. 28 | |- 29 | (config) => ({ 30 | command: 'npm', 31 | args: ['start'], 32 | env: { 33 | GOOGLE_API_KEY: config.googleApiKey, 34 | PORT: config.port ? String(config.port) : '3000', 35 | STORAGE_DIR: config.storageDir || './generated-videos', 36 | LOG_LEVEL: config.logLevel || 'fatal' 37 | } 38 | }) 39 | exampleConfig: 40 | googleApiKey: YOUR_GOOGLE_API_KEY 41 | port: 3000 42 | storageDir: ./generated-videos 43 | logLevel: info 44 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { z } from 'zod'; 3 | import { log } from './utils/logger.js'; 4 | 5 | // Load environment variables from .env file 6 | dotenv.config(); 7 | 8 | // Define log levels 9 | export type LogLevel = 'verbose' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'none'; 10 | 11 | // Define schema for environment variables 12 | const ConfigSchema = z.object({ 13 | // Google API Key for Gemini/Veo2 14 | GOOGLE_API_KEY: z.string().min(1), 15 | 16 | // Server configuration (optional with default) 17 | PORT: z.string().transform(Number).default('3000'), 18 | 19 | // Storage directory for generated videos (optional with default) 20 | STORAGE_DIR: z.string().default('./generated-videos'), 21 | 22 | // Logging level (optional with default to 'fatal') 23 | LOG_LEVEL: z.enum(['verbose', 'debug', 'info', 'warn', 'error', 'fatal', 'none']).default('fatal'), 24 | }); 25 | 26 | // Parse and validate environment variables 27 | const parseConfig = () => { 28 | try { 29 | return ConfigSchema.parse(process.env); 30 | } catch (error) { 31 | if (error instanceof z.ZodError) { 32 | const missingVars = error.errors 33 | .filter(e => e.code === 'invalid_type' && e.received === 'undefined') 34 | .map(e => e.path.join('.')); 35 | 36 | if (missingVars.length > 0) { 37 | log.fatal(`❌ Missing required environment variables: ${missingVars.join(', ')}`); 38 | log.fatal('Please check your .env file or environment configuration.'); 39 | } else { 40 | log.fatal('❌ Invalid environment variables:', error.errors); 41 | } 42 | } else { 43 | log.fatal('❌ Error parsing configuration:', error); 44 | } 45 | process.exit(1); 46 | } 47 | }; 48 | 49 | // Parse and export the validated config 50 | const config = parseConfig(); 51 | 52 | // Initialize the logger with the configured log level 53 | log.initialize(config.LOG_LEVEL); 54 | 55 | export default config; 56 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 3 | import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; 4 | import { createServer } from './server.js'; 5 | import config from './config.js'; 6 | import { log } from './utils/logger.js'; 7 | import fs from 'fs/promises'; 8 | import path from 'path'; 9 | 10 | /** 11 | * Main entry point for the MCP server 12 | */ 13 | async function main() { 14 | try { 15 | // Create the MCP server 16 | const server = createServer(); 17 | 18 | // Determine the transport type from command line arguments 19 | const transportType = process.argv[2] || 'stdio'; 20 | 21 | // Ensure the storage directory exists 22 | await fs.mkdir(config.STORAGE_DIR, { recursive: true }); 23 | 24 | if (transportType === 'stdio') { 25 | // Use stdio transport 26 | log.info('Starting server with stdio transport'); 27 | 28 | const transport = new StdioServerTransport(); 29 | await server.connect(transport); 30 | 31 | log.info('Server started with stdio transport'); 32 | } else if (transportType === 'sse') { 33 | // Use SSE transport 34 | log.info(`Starting server with SSE transport on port ${config.PORT}`); 35 | 36 | const app = express(); 37 | const port = config.PORT; 38 | 39 | // Store active SSE transports 40 | const transports: Record = {}; 41 | 42 | // Serve static files from the generated-videos directory 43 | app.use('/videos', express.static(config.STORAGE_DIR)); 44 | 45 | // SSE endpoint 46 | app.get('/sse', (req, res) => { 47 | log.info('New SSE connection'); 48 | 49 | const transport = new SSEServerTransport('/messages', res); 50 | transports[transport.sessionId] = transport; 51 | 52 | res.on('close', () => { 53 | log.info(`SSE connection closed: ${transport.sessionId}`); 54 | delete transports[transport.sessionId]; 55 | }); 56 | 57 | server.connect(transport).catch(err => { 58 | log.error('Error connecting transport:', err); 59 | }); 60 | }); 61 | 62 | // Message endpoint 63 | app.post('/messages', express.json(), (req, res) => { 64 | const sessionId = req.query.sessionId as string; 65 | const transport = transports[sessionId]; 66 | 67 | if (transport) { 68 | transport.handlePostMessage(req, res).catch(err => { 69 | log.error(`Error handling message for session ${sessionId}:`, err); 70 | }); 71 | } else { 72 | res.status(404).send('Session not found'); 73 | } 74 | }); 75 | 76 | // Start the server 77 | app.listen(port, () => { 78 | log.info(`Server started with SSE transport on port ${port}`); 79 | log.info(`Connect to http://localhost:${port}/sse`); 80 | }); 81 | } else { 82 | log.fatal(`Unknown transport type: ${transportType}`); 83 | log.info('Usage: npm start [stdio|sse]'); 84 | process.exit(1); 85 | } 86 | } catch (error) { 87 | log.fatal('Error starting server:', error); 88 | process.exit(1); 89 | } 90 | } 91 | 92 | // Start the server 93 | main().catch(err => { 94 | log.fatal('Unhandled error:', err); 95 | process.exit(1); 96 | }); 97 | -------------------------------------------------------------------------------- /src/resources/images.ts: -------------------------------------------------------------------------------- 1 | import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { ReadResourceResult } from '@modelcontextprotocol/sdk/types.js'; 3 | import { log } from '../utils/logger.js'; 4 | import fs from 'fs/promises'; 5 | import path from 'path'; 6 | import appConfig from '../config.js'; 7 | 8 | // Define the storage directory for generated images 9 | const IMAGE_STORAGE_DIR = path.join(appConfig.STORAGE_DIR, 'images'); 10 | 11 | /** 12 | * Resource template for accessing generated images 13 | */ 14 | export const imageResourceTemplate = new ResourceTemplate( 15 | 'images://{id}', 16 | { 17 | list: async () => { 18 | try { 19 | // Get all files in the image storage directory 20 | const files = await fs.readdir(IMAGE_STORAGE_DIR); 21 | 22 | // Filter for JSON metadata files 23 | const metadataFiles = files.filter(file => file.endsWith('.json')); 24 | 25 | // Read and parse each metadata file 26 | const imagesPromises = metadataFiles.map(async file => { 27 | const filePath = path.resolve(IMAGE_STORAGE_DIR, file); 28 | try { 29 | const metadataJson = await fs.readFile(filePath, 'utf-8'); 30 | return JSON.parse(metadataJson); 31 | } catch (error) { 32 | log.error(`Error reading image metadata file ${filePath}:`, error); 33 | return null; 34 | } 35 | }); 36 | 37 | // Wait for all metadata to be read and filter out any null values 38 | const images = (await Promise.all(imagesPromises)).filter(image => image !== null); 39 | 40 | // Map to MCP resources 41 | return { 42 | resources: images.map(image => ({ 43 | uri: `images://${image.id}`, 44 | name: `Image: ${image.prompt || 'Untitled'}`, 45 | description: `Generated on ${new Date(image.createdAt).toLocaleString()}`, 46 | mimeType: image.mimeType, 47 | filepath: image.filepath 48 | })) 49 | }; 50 | } catch (error) { 51 | log.error('Error listing image resources:', error); 52 | return { resources: [] }; 53 | } 54 | } 55 | } 56 | ); 57 | 58 | /** 59 | * Gets image metadata by ID 60 | * 61 | * @param id The image ID 62 | * @returns The image metadata 63 | */ 64 | async function getImageMetadata(id: string): Promise { 65 | try { 66 | const metadataPath = path.resolve(IMAGE_STORAGE_DIR, `${id}.json`); 67 | const metadataJson = await fs.readFile(metadataPath, 'utf-8'); 68 | return JSON.parse(metadataJson); 69 | } catch (error) { 70 | log.error(`Error getting metadata for image ${id}:`, error); 71 | throw new Error(`Image metadata not found: ${id}`); 72 | } 73 | } 74 | 75 | /** 76 | * Resource handler for accessing a specific image 77 | * 78 | * @param uri The resource URI 79 | * @param variables The URI template variables 80 | * @returns The image resource contents 81 | */ 82 | export async function readImageResource( 83 | uri: URL, 84 | variables: Record 85 | ): Promise { 86 | // The variables object should contain the 'id' from the URI template 87 | const { id } = variables; 88 | 89 | if (!id || typeof id !== 'string') { 90 | throw new Error('Missing or invalid image ID in resource URI'); 91 | } 92 | 93 | // Default to false since we can't access query parameters 94 | const includeFullData = false; 95 | 96 | try { 97 | // Get the image metadata 98 | const metadata = await getImageMetadata(id); 99 | 100 | // If includeFullData is true and we have a filepath, return the image data 101 | if (includeFullData && metadata.filepath) { 102 | try { 103 | const imageData = await fs.readFile(metadata.filepath); 104 | return { 105 | contents: [ 106 | { 107 | uri: uri.href, 108 | mimeType: metadata.mimeType || 'image/png', 109 | blob: imageData.toString('base64'), 110 | filepath: metadata.filepath 111 | } 112 | ] 113 | }; 114 | } catch (error) { 115 | log.error(`Error reading image file ${metadata.filepath}:`, error); 116 | // Fall back to returning just the metadata 117 | } 118 | } 119 | 120 | // Otherwise, just return the metadata 121 | return { 122 | contents: [ 123 | { 124 | uri: uri.href, 125 | mimeType: 'application/json', 126 | text: JSON.stringify({ 127 | id: metadata.id, 128 | createdAt: metadata.createdAt, 129 | prompt: metadata.prompt, 130 | mimeType: metadata.mimeType, 131 | size: metadata.size, 132 | filepath: metadata.filepath 133 | }, null, 2) 134 | } 135 | ] 136 | }; 137 | } catch (error) { 138 | log.error(`Error reading image resource ${id}:`, error); 139 | throw error; 140 | } 141 | } 142 | 143 | /** 144 | * Resource for accessing example image prompts 145 | */ 146 | export const imagePromptsResource = { 147 | uri: 'images://templates', 148 | name: 'Image Generation Templates', 149 | description: 'Example prompts for generating images with Google Imagen', 150 | async read(): Promise { 151 | // Example prompts for image generation 152 | const templates = [ 153 | { 154 | title: 'Nature Landscape', 155 | prompt: 'A breathtaking mountain landscape with snow-capped peaks, a crystal clear lake in the foreground, and a colorful sunset, photorealistic style', 156 | config: { 157 | numberOfImages: 1 158 | } 159 | }, 160 | { 161 | title: 'Futuristic City', 162 | prompt: 'A futuristic cityscape with flying vehicles, holographic billboards, and towering skyscrapers, digital art style', 163 | config: { 164 | numberOfImages: 1 165 | } 166 | }, 167 | { 168 | title: 'Fantasy Character', 169 | prompt: 'A mystical wizard with flowing robes, glowing staff, and magical energy swirling around them, fantasy art style', 170 | config: { 171 | numberOfImages: 1 172 | } 173 | }, 174 | { 175 | title: 'Food Photography', 176 | prompt: 'A gourmet burger with melted cheese, fresh vegetables, and a brioche bun, on a wooden plate, professional food photography', 177 | config: { 178 | numberOfImages: 1 179 | } 180 | }, 181 | { 182 | title: 'Abstract Art', 183 | prompt: 'Abstract fluid art with vibrant colors flowing and blending together, high resolution', 184 | config: { 185 | numberOfImages: 1 186 | } 187 | } 188 | ]; 189 | 190 | // Format as a text resource 191 | return { 192 | contents: [ 193 | { 194 | uri: 'images://templates', 195 | mimeType: 'application/json', 196 | text: JSON.stringify(templates, null, 2) 197 | } 198 | ] 199 | }; 200 | } 201 | }; 202 | -------------------------------------------------------------------------------- /src/resources/videos.ts: -------------------------------------------------------------------------------- 1 | import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { veoClient } from '../services/veoClient.js'; 3 | import { ReadResourceResult } from '@modelcontextprotocol/sdk/types.js'; 4 | import { log } from '../utils/logger.js'; 5 | 6 | /** 7 | * Resource template for accessing generated videos 8 | */ 9 | export const videoResourceTemplate = new ResourceTemplate( 10 | 'videos://{id}', 11 | { 12 | list: async () => { 13 | // Get all videos 14 | const videos = await veoClient.listVideos(); 15 | 16 | // Map to MCP resources 17 | return { 18 | resources: videos.map(video => ({ 19 | uri: `videos://${video.id}`, 20 | name: `Video: ${video.prompt || 'Untitled'}`, 21 | description: `Generated on ${new Date(video.createdAt).toLocaleString()}`, 22 | mimeType: video.mimeType, 23 | filepath: video.filepath 24 | })) 25 | }; 26 | } 27 | } 28 | ); 29 | 30 | /** 31 | * Resource handler for accessing a specific video 32 | * 33 | * @param uri The resource URI 34 | * @param variables The URI template variables 35 | * @param query Optional query parameters 36 | * @returns The video resource contents 37 | */ 38 | export async function readVideoResource( 39 | uri: URL, 40 | variables: Record, 41 | query?: URLSearchParams 42 | ): Promise { 43 | // The variables object should contain the 'id' from the URI template 44 | const { id } = variables; 45 | 46 | if (!id || typeof id !== 'string') { 47 | throw new Error('Missing or invalid video ID in resource URI'); 48 | } 49 | 50 | // Default to false if query is not provided 51 | const includeFullData = false; 52 | 53 | try { 54 | // Get the video data and metadata with the includeFullData option 55 | const result = await veoClient.getVideo(id, { includeFullData }); 56 | 57 | // If includeFullData is true and we have video data, return it 58 | if (includeFullData && result.videoData) { 59 | return { 60 | contents: [ 61 | { 62 | uri: uri.href, 63 | mimeType: result.metadata.mimeType, 64 | blob: result.videoData, 65 | filepath: result.metadata.filepath 66 | } 67 | ] 68 | }; 69 | } 70 | 71 | // Otherwise, just return the metadata 72 | return { 73 | contents: [ 74 | { 75 | uri: uri.href, 76 | mimeType: 'application/json', 77 | text: JSON.stringify({ 78 | id: result.metadata.id, 79 | createdAt: result.metadata.createdAt, 80 | prompt: result.metadata.prompt, 81 | config: result.metadata.config, 82 | mimeType: result.metadata.mimeType, 83 | size: result.metadata.size, 84 | filepath: result.metadata.filepath, 85 | videoUrl: result.metadata.videoUrl 86 | }, null, 2) 87 | } 88 | ] 89 | }; 90 | } catch (error) { 91 | log.error(`Error reading video resource ${id}:`, error); 92 | throw error; 93 | } 94 | } 95 | 96 | /** 97 | * Resource for accessing example video prompts 98 | */ 99 | export const videoPromptsResource = { 100 | uri: 'videos://templates', 101 | name: 'Video Generation Templates', 102 | description: 'Example prompts for generating videos with Veo2', 103 | async read(): Promise { 104 | // Example prompts for video generation 105 | const templates = [ 106 | { 107 | title: 'Nature Scene', 108 | prompt: 'Panning wide shot of a serene forest with sunlight filtering through the trees, cinematic quality', 109 | config: { 110 | aspectRatio: '16:9', 111 | personGeneration: 'dont_allow' 112 | } 113 | }, 114 | { 115 | title: 'Urban Timelapse', 116 | prompt: 'Timelapse of a busy city intersection at night with cars leaving light trails, cinematic quality', 117 | config: { 118 | aspectRatio: '16:9', 119 | personGeneration: 'dont_allow' 120 | } 121 | }, 122 | { 123 | title: 'Abstract Animation', 124 | prompt: 'Abstract fluid animation with vibrant colors morphing and flowing, digital art style', 125 | config: { 126 | aspectRatio: '16:9', 127 | personGeneration: 'dont_allow' 128 | } 129 | }, 130 | { 131 | title: 'Product Showcase', 132 | prompt: 'Elegant product showcase of a modern smartphone rotating on a pedestal with soft lighting', 133 | config: { 134 | aspectRatio: '16:9', 135 | personGeneration: 'dont_allow' 136 | } 137 | }, 138 | { 139 | title: 'Food Close-up', 140 | prompt: 'Close-up of a delicious chocolate cake with melting chocolate dripping down the sides', 141 | config: { 142 | aspectRatio: '16:9', 143 | personGeneration: 'dont_allow' 144 | } 145 | } 146 | ]; 147 | 148 | // Format as a text resource 149 | return { 150 | contents: [ 151 | { 152 | uri: 'videos://templates', 153 | mimeType: 'application/json', 154 | text: JSON.stringify(templates, null, 2) 155 | } 156 | ] 157 | }; 158 | } 159 | }; 160 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { z } from 'zod'; 3 | import { videoResourceTemplate, readVideoResource, videoPromptsResource } from './resources/videos.js'; 4 | import { imageResourceTemplate, readImageResource, imagePromptsResource } from './resources/images.js'; 5 | import { 6 | generateVideoFromText, 7 | generateVideoFromImage, 8 | generateImage, 9 | generateVideoFromGeneratedImage, 10 | listGeneratedVideos, 11 | listGeneratedImages, 12 | getImage 13 | } from './tools/generateVideo.js'; 14 | import { ImageContent } from '@modelcontextprotocol/sdk/types.js'; 15 | import { log } from './utils/logger.js'; 16 | 17 | /** 18 | * Creates and configures the MCP server for Veo2 video generation 19 | * 20 | * @returns The configured MCP server 21 | */ 22 | export function createServer(): McpServer { 23 | // Create the MCP server 24 | const server = new McpServer({ 25 | name: 'veo2-video-generation', 26 | version: '1.0.0' 27 | }, { 28 | capabilities: { 29 | resources: { 30 | listChanged: true, 31 | subscribe: true 32 | }, 33 | tools: { 34 | listChanged: true 35 | } 36 | } 37 | }); 38 | 39 | log.info('Initializing MCP server for Veo2 video generation'); 40 | 41 | // Register resources 42 | log.info('Registering video and image resources'); 43 | 44 | // Register the video resource template 45 | server.resource( 46 | 'videos', 47 | videoResourceTemplate, 48 | { 49 | description: 'Access generated videos' 50 | }, 51 | async (uri, variables) => { 52 | // Since we can't access query parameters directly, we'll just pass an empty URLSearchParams 53 | return readVideoResource(uri, variables); 54 | } 55 | ); 56 | 57 | // Register the video templates resource 58 | server.resource( 59 | 'video-templates', 60 | videoPromptsResource.uri, 61 | { 62 | description: videoPromptsResource.description 63 | }, 64 | async () => videoPromptsResource.read() 65 | ); 66 | 67 | // Register the image resource template 68 | server.resource( 69 | 'images', 70 | imageResourceTemplate, 71 | { 72 | description: 'Access generated images' 73 | }, 74 | async (uri, variables) => { 75 | // Since we can't access query parameters directly, we'll just pass an empty URLSearchParams 76 | return readImageResource(uri, variables); 77 | } 78 | ); 79 | 80 | // Register the image templates resource 81 | server.resource( 82 | 'image-templates', 83 | imagePromptsResource.uri, 84 | { 85 | description: imagePromptsResource.description 86 | }, 87 | async () => imagePromptsResource.read() 88 | ); 89 | 90 | // Register tools 91 | log.info('Registering video generation tools'); 92 | 93 | // Define schemas for tool inputs 94 | const TextToVideoConfigSchema = z.object({ 95 | aspectRatio: z.enum(['16:9', '9:16']).default('16:9'), 96 | personGeneration: z.enum(['dont_allow', 'allow_adult']).default('dont_allow'), 97 | numberOfVideos: z.union([z.literal(1), z.literal(2)]).default(1), 98 | durationSeconds: z.number().min(5).max(8).default(5), 99 | enhancePrompt: z.boolean().default(false), 100 | negativePrompt: z.string().default(''), 101 | }); 102 | 103 | // Register the text-to-video generation tool 104 | server.tool( 105 | 'generateVideoFromText', 106 | 'Generate a video from a text prompt', 107 | { 108 | prompt: z.string().min(1).max(1000), 109 | aspectRatio: z.enum(['16:9', '9:16']).default('16:9'), 110 | personGeneration: z.enum(['dont_allow', 'allow_adult']).default('dont_allow'), 111 | numberOfVideos: z.union([z.literal(1), z.literal(2)]).default(1), 112 | durationSeconds: z.number().min(5).max(8).default(5), 113 | enhancePrompt: z.union([z.boolean(), z.string()]).default(false), 114 | negativePrompt: z.string().default(''), 115 | includeFullData: z.union([z.boolean(), z.string()]).default(false), 116 | autoDownload: z.union([z.boolean(), z.string()]).default(true), 117 | }, 118 | generateVideoFromText 119 | ); 120 | 121 | // Register the image-to-video generation tool 122 | server.tool( 123 | 'generateVideoFromImage', 124 | 'Generate a video from an image', 125 | { 126 | prompt: z.string().min(1).max(1000).optional().default('Generate a video from this image'), 127 | image: z.union([ 128 | // ImageContent object 129 | z.object({ 130 | type: z.literal('image'), 131 | mimeType: z.string(), 132 | data: z.string().min(1) // base64 encoded image data 133 | }), 134 | // URL string 135 | z.string().url(), 136 | // File path string 137 | z.string().min(1) 138 | ]), 139 | aspectRatio: z.enum(['16:9', '9:16']).default('16:9'), 140 | personGeneration: z.enum(['dont_allow', 'allow_adult']).default('dont_allow'), 141 | numberOfVideos: z.union([z.literal(1), z.literal(2)]).default(1), 142 | durationSeconds: z.number().min(5).max(8).default(5), 143 | enhancePrompt: z.union([z.boolean(), z.string()]).default(false), 144 | negativePrompt: z.string().default(''), 145 | includeFullData: z.union([z.boolean(), z.string()]).default(false), 146 | autoDownload: z.union([z.boolean(), z.string()]).default(true), 147 | }, 148 | generateVideoFromImage 149 | ); 150 | 151 | // Schema for image generation configuration 152 | const ImageGenerationConfigSchema = z.object({ 153 | numberOfImages: z.number().min(1).max(4).default(1), 154 | // Add other Imagen parameters as needed 155 | }); 156 | 157 | // Register the image generation tool 158 | server.tool( 159 | 'generateImage', 160 | 'Generate an image from a text prompt using Google Imagen', 161 | { 162 | prompt: z.string().min(1).max(1000), 163 | numberOfImages: z.number().min(1).max(4).default(1), 164 | includeFullData: z.union([z.boolean(), z.string()]).default(false), 165 | }, 166 | generateImage 167 | ); 168 | 169 | // Register the image-to-video generation with generated image tool 170 | server.tool( 171 | 'generateVideoFromGeneratedImage', 172 | 'Generate a video from a generated image (one-step process)', 173 | { 174 | prompt: z.string().min(1).max(1000), 175 | videoPrompt: z.string().min(1).max(1000).optional(), 176 | // Image generation parameters 177 | numberOfImages: z.number().min(1).max(4).default(1), 178 | // Video generation parameters 179 | aspectRatio: z.enum(['16:9', '9:16']).default('16:9'), 180 | personGeneration: z.enum(['dont_allow', 'allow_adult']).default('dont_allow'), 181 | numberOfVideos: z.union([z.literal(1), z.literal(2)]).default(1), 182 | durationSeconds: z.number().min(5).max(8).default(5), 183 | enhancePrompt: z.union([z.boolean(), z.string()]).default(false), 184 | negativePrompt: z.string().default(''), 185 | includeFullData: z.union([z.boolean(), z.string()]).default(false), 186 | autoDownload: z.union([z.boolean(), z.string()]).default(true), 187 | }, 188 | generateVideoFromGeneratedImage 189 | ); 190 | 191 | // Register the list videos tool 192 | server.tool( 193 | 'listGeneratedVideos', 194 | 'List all generated videos', 195 | listGeneratedVideos 196 | ); 197 | 198 | // Register the get image tool 199 | server.tool( 200 | 'getImage', 201 | 'Get a specific image by ID', 202 | { 203 | id: z.string().min(1), 204 | includeFullData: z.union([z.boolean(), z.string()]).default(true), 205 | }, 206 | getImage 207 | ); 208 | 209 | // Register the list images tool 210 | server.tool( 211 | 'listGeneratedImages', 212 | 'List all generated images', 213 | listGeneratedImages 214 | ); 215 | 216 | log.info('MCP server initialized successfully'); 217 | 218 | return server; 219 | } 220 | -------------------------------------------------------------------------------- /src/services/veoClient.ts: -------------------------------------------------------------------------------- 1 | import { GoogleGenAI, GenerateVideosParameters } from '@google/genai'; 2 | import fs from 'fs/promises'; 3 | import path from 'path'; 4 | import { v4 as uuidv4 } from 'uuid'; 5 | import { createWriteStream } from 'fs'; 6 | import { Readable } from 'stream'; 7 | import appConfig from '../config.js'; 8 | import { log } from '../utils/logger.js'; 9 | 10 | // Define types for video generation 11 | interface VideoConfig { 12 | aspectRatio?: '16:9' | '9:16'; 13 | personGeneration?: 'dont_allow' | 'allow_adult'; 14 | numberOfVideos?: 1 | 2; 15 | durationSeconds?: number; 16 | negativePrompt?: string; 17 | } 18 | 19 | // Options for video generation 20 | interface VideoGenerationOptions { 21 | autoDownload?: boolean; // Default: true 22 | includeFullData?: boolean; // Default: false 23 | } 24 | 25 | // Define types for video generation operation 26 | interface VideoOperation { 27 | done: boolean; 28 | response?: { 29 | generatedVideos?: Array<{ 30 | video?: { 31 | uri?: string; 32 | }; 33 | }>; 34 | }; 35 | } 36 | 37 | // Metadata for stored videos 38 | interface StoredVideoMetadata { 39 | id: string; 40 | createdAt: string; 41 | prompt?: string; 42 | config: { 43 | aspectRatio: '16:9' | '9:16'; 44 | personGeneration: 'dont_allow' | 'allow_adult'; 45 | durationSeconds: number; 46 | }; 47 | mimeType: string; 48 | size: number; 49 | filepath: string; // Path to the video file on disk 50 | videoUrl?: string; // URL to the video (when autoDownload is false) 51 | } 52 | 53 | /** 54 | * Client for interacting with Google's Veo2 video generation API 55 | */ 56 | export class VeoClient { 57 | private client: GoogleGenAI; 58 | private model: string = 'veo-2.0-generate-001'; 59 | private storageDir: string; 60 | 61 | /** 62 | * Creates a new VeoClient instance 63 | */ 64 | constructor() { 65 | // Initialize the Google Gen AI client 66 | this.client = new GoogleGenAI({ apiKey: appConfig.GOOGLE_API_KEY }); 67 | 68 | // Set the storage directory 69 | this.storageDir = appConfig.STORAGE_DIR; 70 | 71 | // Ensure the storage directory exists 72 | this.ensureStorageDir().catch(err => { 73 | log.fatal('Failed to create storage directory:', err); 74 | process.exit(1); 75 | }); 76 | } 77 | 78 | /** 79 | * Ensures the storage directory exists 80 | */ 81 | private async ensureStorageDir(): Promise { 82 | try { 83 | await fs.mkdir(this.storageDir, { recursive: true }); 84 | } catch (error) { 85 | throw new Error(`Failed to create storage directory: ${error}`); 86 | } 87 | } 88 | 89 | /** 90 | * Processes an image input which can be base64 data, a file path, or a URL 91 | * 92 | * @param image The image input (base64 data, file path, or URL) 93 | * @param mimeType The MIME type of the image (optional, detected for files and URLs) 94 | * @returns The image bytes and MIME type 95 | */ 96 | private async processImageInput( 97 | image: string, 98 | mimeType?: string 99 | ): Promise<{ imageBytes: string; mimeType: string }> { 100 | // Check if the image is a URL 101 | if (image.startsWith('http://') || image.startsWith('https://')) { 102 | log.debug('Processing image from URL'); 103 | const response = await fetch(image); 104 | if (!response.ok) { 105 | throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`); 106 | } 107 | 108 | const arrayBuffer = await response.arrayBuffer(); 109 | const buffer = Buffer.from(arrayBuffer); 110 | 111 | // Get the MIME type from the response or use a default 112 | const responseMimeType = response.headers.get('content-type') || mimeType || 'image/jpeg'; 113 | 114 | return { 115 | imageBytes: buffer.toString('base64'), 116 | mimeType: responseMimeType 117 | }; 118 | } 119 | 120 | // Check if the image is a file path 121 | if (image.startsWith('/') || image.includes(':\\') || image.includes(':/')) { 122 | log.debug('Processing image from file path'); 123 | const buffer = await fs.readFile(image); 124 | 125 | // Determine MIME type from file extension if not provided 126 | let detectedMimeType = mimeType; 127 | if (!detectedMimeType) { 128 | const extension = path.extname(image).toLowerCase(); 129 | switch (extension) { 130 | case '.png': 131 | detectedMimeType = 'image/png'; 132 | break; 133 | case '.jpg': 134 | case '.jpeg': 135 | detectedMimeType = 'image/jpeg'; 136 | break; 137 | case '.gif': 138 | detectedMimeType = 'image/gif'; 139 | break; 140 | case '.webp': 141 | detectedMimeType = 'image/webp'; 142 | break; 143 | default: 144 | detectedMimeType = 'image/jpeg'; // Default 145 | } 146 | } 147 | 148 | return { 149 | imageBytes: buffer.toString('base64'), 150 | mimeType: detectedMimeType 151 | }; 152 | } 153 | 154 | // Assume it's already base64 data 155 | return { 156 | imageBytes: image, 157 | mimeType: mimeType || 'image/png' 158 | }; 159 | } 160 | 161 | /** 162 | * Generates a video from a text prompt 163 | * 164 | * @param prompt The text prompt for video generation 165 | * @param config Optional configuration for video generation 166 | * @param options Optional generation options 167 | * @returns Metadata for the generated video and optionally the video data 168 | */ 169 | async generateFromText( 170 | prompt: string, 171 | config?: VideoConfig, 172 | options?: VideoGenerationOptions 173 | ): Promise { 174 | try { 175 | log.info('Generating video from text prompt'); 176 | log.verbose('Text prompt parameters:', JSON.stringify({ prompt, config, options })); 177 | 178 | // Default options 179 | const autoDownload = options?.autoDownload !== false; // Default to true if not specified 180 | const includeFullData = options?.includeFullData === true; // Default to false if not specified 181 | 182 | // Create generation config 183 | const generateConfig: Record = {}; 184 | 185 | // Add optional parameters if provided 186 | if (config?.aspectRatio) { 187 | generateConfig.aspectRatio = config.aspectRatio; 188 | } 189 | 190 | if (config?.personGeneration) { 191 | generateConfig.personGeneration = config.personGeneration; 192 | } 193 | 194 | if (config?.numberOfVideos) { 195 | generateConfig.numberOfVideos = config.numberOfVideos; 196 | } 197 | 198 | if (config?.durationSeconds) { 199 | generateConfig.durationSeconds = config.durationSeconds; 200 | } 201 | 202 | if (config?.negativePrompt) { 203 | generateConfig.negativePrompt = config.negativePrompt; 204 | } 205 | 206 | // Initialize request parameters 207 | const requestParams = { 208 | model: this.model, 209 | prompt: prompt, 210 | config: generateConfig 211 | }; 212 | 213 | // Call the generateVideos method 214 | log.debug('Calling generateVideos API'); 215 | let operation = await this.client.models.generateVideos(requestParams); 216 | 217 | // Poll until the operation is complete 218 | log.debug('Polling operation status'); 219 | while (!operation.done) { 220 | log.verbose('Operation not complete, waiting...', JSON.stringify(operation)); 221 | // Wait for 5 seconds before checking again 222 | await new Promise(resolve => setTimeout(resolve, 5000)); 223 | operation = await this.client.operations.getVideosOperation({ 224 | operation: operation 225 | }); 226 | } 227 | 228 | log.debug('Video generation operation complete'); 229 | log.verbose('Operation result:', JSON.stringify(operation)); 230 | 231 | // Check if we have generated videos 232 | if (!operation.response?.generatedVideos || operation.response.generatedVideos.length === 0) { 233 | throw new Error('No videos generated in the response'); 234 | } 235 | 236 | // Process each video 237 | const videoPromises = operation.response.generatedVideos.map(async (generatedVideo, index) => { 238 | if (!generatedVideo.video?.uri) { 239 | log.warn('Generated video missing URI'); 240 | return null; 241 | } 242 | 243 | // Append API key to the URI - use the imported config module 244 | const videoUri = `${generatedVideo.video.uri}&key=${appConfig.GOOGLE_API_KEY}`; 245 | log.debug(`Processing video ${index + 1} from URI`); 246 | 247 | // Generate a unique ID for the video 248 | const id = index === 0 ? uuidv4() : `${uuidv4()}_${index}`; 249 | 250 | if (autoDownload) { 251 | // Fetch the video 252 | const response = await fetch(videoUri); 253 | if (!response.ok) { 254 | throw new Error(`Failed to fetch video: ${response.status} ${response.statusText}`); 255 | } 256 | 257 | // Convert the response to a buffer 258 | const arrayBuffer = await response.arrayBuffer(); 259 | const buffer = Buffer.from(arrayBuffer); 260 | 261 | // Save the video to disk 262 | return this.saveVideoBuffer(buffer, prompt, config, id); 263 | } else { 264 | // Just return metadata with the URL 265 | const metadata: StoredVideoMetadata = { 266 | id, 267 | createdAt: new Date().toISOString(), 268 | prompt, 269 | config: { 270 | aspectRatio: config?.aspectRatio || '16:9', 271 | personGeneration: config?.personGeneration || 'dont_allow', 272 | durationSeconds: config?.durationSeconds || 5 273 | }, 274 | mimeType: 'video/mp4', 275 | size: 0, // Size unknown without downloading 276 | filepath: '', // No filepath without downloading 277 | videoUrl: videoUri // Include the video URL 278 | }; 279 | 280 | // Save the metadata 281 | await this.saveMetadata(id, metadata); 282 | 283 | return metadata; 284 | } 285 | }); 286 | 287 | // Wait for all videos to be processed 288 | const metadataArray = await Promise.all(videoPromises); 289 | 290 | // Filter out any null values (from videos with missing URIs) 291 | const validMetadata = metadataArray.filter(metadata => metadata !== null); 292 | 293 | if (validMetadata.length === 0) { 294 | throw new Error('Failed to process any videos'); 295 | } 296 | 297 | // Return the first video's metadata 298 | const result = validMetadata[0] as StoredVideoMetadata & { videoUrl?: string }; 299 | 300 | // If we didn't download but have a URL, include it in the result 301 | if (!autoDownload && result.videoUrl) { 302 | return result; 303 | } 304 | 305 | // If includeFullData is true and we downloaded the video, include the video data 306 | if (includeFullData && autoDownload && result.filepath) { 307 | const videoData = await fs.readFile(result.filepath); 308 | return { 309 | ...result, 310 | videoData: videoData.toString('base64') 311 | }; 312 | } 313 | 314 | return result; 315 | } catch (error) { 316 | log.error('Error generating video from text:', error); 317 | throw error; 318 | } 319 | } 320 | 321 | /** 322 | * Generates a video from an image 323 | * 324 | * @param image The image input (base64 data, file path, or URL) 325 | * @param prompt Optional text prompt for video generation 326 | * @param config Optional configuration for video generation 327 | * @param options Optional generation options 328 | * @param mimeType The MIME type of the image (optional, detected for files and URLs) 329 | * @returns Metadata for the generated video and optionally the video data 330 | */ 331 | async generateFromImage( 332 | image: string, 333 | prompt?: string, 334 | config?: VideoConfig, 335 | options?: VideoGenerationOptions, 336 | mimeType?: string 337 | ): Promise { 338 | try { 339 | log.info('Generating video from image'); 340 | log.verbose('Image prompt parameters:', JSON.stringify({ prompt, config, options, mimeType })); 341 | 342 | // Default options 343 | const autoDownload = options?.autoDownload !== false; // Default to true if not specified 344 | const includeFullData = options?.includeFullData === true; // Default to false if not specified 345 | 346 | // Default prompt 347 | prompt = prompt || 'Generate a video from this image'; 348 | 349 | // Create generation config 350 | const generateConfig: Record = {}; 351 | 352 | // Add optional parameters if provided 353 | if (config?.aspectRatio) { 354 | generateConfig.aspectRatio = config.aspectRatio; 355 | } 356 | 357 | // Note: personGeneration is not allowed for image-to-video generation 358 | 359 | if (config?.numberOfVideos) { 360 | generateConfig.numberOfVideos = config.numberOfVideos; 361 | } 362 | 363 | if (config?.durationSeconds) { 364 | generateConfig.durationSeconds = config.durationSeconds; 365 | } 366 | 367 | if (config?.negativePrompt) { 368 | generateConfig.negativePrompt = config.negativePrompt; 369 | } 370 | 371 | // Process the image input 372 | const { imageBytes, mimeType: detectedMimeType } = await this.processImageInput(image, mimeType); 373 | 374 | // Initialize request parameters with the image 375 | const requestParams = { 376 | model: this.model, 377 | prompt: prompt || 'Generate a video from this image', 378 | image: { 379 | imageBytes: imageBytes, 380 | mimeType: detectedMimeType 381 | }, 382 | config: generateConfig 383 | }; 384 | 385 | // Call the generateVideos method 386 | log.debug('Calling generateVideos API with image'); 387 | let operation = await this.client.models.generateVideos(requestParams); 388 | 389 | // Poll until the operation is complete 390 | log.debug('Polling operation status'); 391 | while (!operation.done) { 392 | log.verbose('Operation not complete, waiting...', JSON.stringify(operation)); 393 | // Wait for 5 seconds before checking again 394 | await new Promise(resolve => setTimeout(resolve, 5000)); 395 | operation = await this.client.operations.getVideosOperation({ 396 | operation: operation 397 | }); 398 | } 399 | 400 | log.debug('Video generation operation complete'); 401 | log.verbose('Operation result:', JSON.stringify(operation)); 402 | 403 | // Check if we have generated videos 404 | if (!operation.response?.generatedVideos || operation.response.generatedVideos.length === 0) { 405 | throw new Error('No videos generated in the response'); 406 | } 407 | 408 | // Process each video 409 | const videoPromises = operation.response.generatedVideos.map(async (generatedVideo, index) => { 410 | if (!generatedVideo.video?.uri) { 411 | log.warn('Generated video missing URI'); 412 | return null; 413 | } 414 | 415 | // Append API key to the URI - use the imported config module 416 | const videoUri = `${generatedVideo.video.uri}&key=${appConfig.GOOGLE_API_KEY}`; 417 | log.debug(`Processing video ${index + 1} from URI`); 418 | 419 | // Generate a unique ID for the video 420 | const id = index === 0 ? uuidv4() : `${uuidv4()}_${index}`; 421 | 422 | if (autoDownload) { 423 | // Fetch the video 424 | const response = await fetch(videoUri); 425 | if (!response.ok) { 426 | throw new Error(`Failed to fetch video: ${response.status} ${response.statusText}`); 427 | } 428 | 429 | // Convert the response to a buffer 430 | const arrayBuffer = await response.arrayBuffer(); 431 | const buffer = Buffer.from(arrayBuffer); 432 | 433 | // Save the video to disk 434 | return this.saveVideoBuffer(buffer, prompt, config, id); 435 | } else { 436 | // Just return metadata with the URL 437 | const metadata: StoredVideoMetadata = { 438 | id, 439 | createdAt: new Date().toISOString(), 440 | prompt, 441 | config: { 442 | aspectRatio: config?.aspectRatio || '16:9', 443 | personGeneration: config?.personGeneration || 'dont_allow', 444 | durationSeconds: config?.durationSeconds || 5 445 | }, 446 | mimeType: 'video/mp4', 447 | size: 0, // Size unknown without downloading 448 | filepath: '', // No filepath without downloading 449 | videoUrl: videoUri // Include the video URL 450 | }; 451 | 452 | // Save the metadata 453 | await this.saveMetadata(id, metadata); 454 | 455 | return metadata; 456 | } 457 | }); 458 | 459 | // Wait for all videos to be processed 460 | const metadataArray = await Promise.all(videoPromises); 461 | 462 | // Filter out any null values (from videos with missing URIs) 463 | const validMetadata = metadataArray.filter(metadata => metadata !== null); 464 | 465 | if (validMetadata.length === 0) { 466 | throw new Error('Failed to process any videos'); 467 | } 468 | 469 | // Return the first video's metadata 470 | const result = validMetadata[0] as StoredVideoMetadata & { videoUrl?: string }; 471 | 472 | // If we didn't download but have a URL, include it in the result 473 | if (!autoDownload && result.videoUrl) { 474 | return result; 475 | } 476 | 477 | // If includeFullData is true and we downloaded the video, include the video data 478 | if (includeFullData && autoDownload && result.filepath) { 479 | const videoData = await fs.readFile(result.filepath); 480 | return { 481 | ...result, 482 | videoData: videoData.toString('base64') 483 | }; 484 | } 485 | 486 | return result; 487 | } catch (error) { 488 | log.error('Error generating video from image:', error); 489 | throw error; 490 | } 491 | } 492 | 493 | /** 494 | * Saves a video buffer to disk 495 | * 496 | * @param videoBuffer The video buffer to save 497 | * @param prompt The prompt used for generation 498 | * @param config The configuration used for generation 499 | * @param id The ID to use for the video 500 | * @returns Metadata for the saved video 501 | */ 502 | private async saveVideoBuffer( 503 | videoBuffer: Buffer, 504 | prompt?: string, 505 | config?: VideoConfig, 506 | id: string = uuidv4() 507 | ): Promise { 508 | try { 509 | log.debug(`Saving video with ID: ${id}`); 510 | 511 | // Determine the file extension based on MIME type 512 | const mimeType = 'video/mp4'; // Assuming Veo2 returns MP4 videos 513 | const extension = '.mp4'; 514 | 515 | // Create the file path (using absolute path) 516 | const filePath = path.resolve(this.storageDir, `${id}${extension}`); 517 | 518 | // Save the video to disk 519 | await fs.writeFile(filePath, videoBuffer); 520 | 521 | // Create and return the metadata 522 | const metadata: StoredVideoMetadata = { 523 | id, 524 | createdAt: new Date().toISOString(), 525 | prompt, 526 | config: { 527 | aspectRatio: config?.aspectRatio || '16:9', 528 | personGeneration: config?.personGeneration || 'dont_allow', 529 | durationSeconds: config?.durationSeconds || 5 530 | }, 531 | mimeType, 532 | size: videoBuffer.length, 533 | filepath: filePath 534 | }; 535 | 536 | // Save the metadata 537 | await this.saveMetadata(id, metadata); 538 | 539 | log.info(`Video saved successfully with ID: ${id}`); 540 | return metadata; 541 | } catch (error) { 542 | log.error(`Error saving video buffer: ${error}`); 543 | throw error; 544 | } 545 | } 546 | 547 | /** 548 | * Saves video metadata to disk 549 | * 550 | * @param id The video ID 551 | * @param metadata The video metadata 552 | */ 553 | private async saveMetadata(id: string, metadata: StoredVideoMetadata): Promise { 554 | const metadataPath = path.resolve(this.storageDir, `${id}.json`); 555 | await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2)); 556 | } 557 | 558 | /** 559 | * Gets a video by ID 560 | * 561 | * @param id The video ID 562 | * @param options Optional options for getting the video 563 | * @returns The video data and metadata 564 | */ 565 | async getVideo( 566 | id: string, 567 | options?: { includeFullData?: boolean } 568 | ): Promise<{ data?: Buffer; metadata: StoredVideoMetadata; videoData?: string }> { 569 | try { 570 | // Get the metadata 571 | const metadata = await this.getMetadata(id); 572 | 573 | // Default options 574 | const includeFullData = options?.includeFullData === true; // Default to false if not specified 575 | 576 | // If includeFullData is false, just return the metadata 577 | if (!includeFullData) { 578 | return { metadata }; 579 | } 580 | 581 | // Get the video data - use the filepath from metadata if available 582 | let filePath: string; 583 | if (metadata.filepath) { 584 | filePath = metadata.filepath; 585 | } else { 586 | // Fallback to constructing the path 587 | const extension = metadata.mimeType === 'video/mp4' ? '.mp4' : '.webm'; 588 | filePath = path.resolve(this.storageDir, `${id}${extension}`); 589 | 590 | // Update the metadata with the filepath 591 | metadata.filepath = filePath; 592 | await this.saveMetadata(id, metadata); 593 | } 594 | 595 | const data = await fs.readFile(filePath); 596 | 597 | // If includeFullData is true, include the base64 data 598 | if (includeFullData) { 599 | return { 600 | metadata, 601 | data, 602 | videoData: data.toString('base64') 603 | }; 604 | } 605 | 606 | return { data, metadata }; 607 | } catch (error) { 608 | log.error(`Error getting video ${id}:`, error); 609 | throw new Error(`Video not found: ${id}`); 610 | } 611 | } 612 | 613 | /** 614 | * Gets video metadata by ID 615 | * 616 | * @param id The video ID 617 | * @returns The video metadata 618 | */ 619 | async getMetadata(id: string): Promise { 620 | try { 621 | const metadataPath = path.resolve(this.storageDir, `${id}.json`); 622 | const metadataJson = await fs.readFile(metadataPath, 'utf-8'); 623 | return JSON.parse(metadataJson) as StoredVideoMetadata; 624 | } catch (error) { 625 | log.error(`Error getting metadata for video ${id}:`, error); 626 | throw new Error(`Video metadata not found: ${id}`); 627 | } 628 | } 629 | 630 | /** 631 | * Lists all generated videos 632 | * 633 | * @returns Array of video metadata 634 | */ 635 | async listVideos(): Promise { 636 | try { 637 | // Get all files in the storage directory 638 | const files = await fs.readdir(this.storageDir); 639 | 640 | // Filter for JSON metadata files 641 | const metadataFiles = files.filter(file => file.endsWith('.json')); 642 | 643 | // Read and parse each metadata file 644 | const metadataPromises = metadataFiles.map(async file => { 645 | const filePath = path.resolve(this.storageDir, file); 646 | const metadataJson = await fs.readFile(filePath, 'utf-8'); 647 | return JSON.parse(metadataJson) as StoredVideoMetadata; 648 | }); 649 | 650 | // Wait for all metadata to be read 651 | return Promise.all(metadataPromises); 652 | } catch (error) { 653 | log.error('Error listing videos:', error); 654 | return []; 655 | } 656 | } 657 | } 658 | 659 | // Export a singleton instance 660 | export const veoClient = new VeoClient(); 661 | -------------------------------------------------------------------------------- /src/tools/generateVideo.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { GoogleGenAI } from '@google/genai'; 3 | import { veoClient } from '../services/veoClient.js'; 4 | import { CallToolResult, ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js'; 5 | import { log } from '../utils/logger.js'; 6 | import appConfig from '../config.js'; 7 | import fs from 'fs/promises'; 8 | import path from 'path'; 9 | import { v4 as uuidv4 } from 'uuid'; 10 | 11 | // Initialize the Google Gen AI client for image generation 12 | const ai = new GoogleGenAI({ apiKey: appConfig.GOOGLE_API_KEY }); 13 | 14 | // Define the storage directory for generated images 15 | const IMAGE_STORAGE_DIR = path.join(appConfig.STORAGE_DIR, 'images'); 16 | 17 | // Ensure the image storage directory exists 18 | (async () => { 19 | try { 20 | await fs.mkdir(IMAGE_STORAGE_DIR, { recursive: true }); 21 | } catch (error) { 22 | log.fatal('Failed to create image storage directory:', error); 23 | process.exit(1); 24 | } 25 | })(); 26 | 27 | /** 28 | * Saves a generated image to disk 29 | * 30 | * @param imageBytes The base64 encoded image data 31 | * @param prompt The prompt used to generate the image 32 | * @param mimeType The MIME type of the image 33 | * @returns The filepath and ID of the saved image 34 | */ 35 | async function saveGeneratedImage( 36 | imageBytes: string, 37 | prompt: string, 38 | mimeType: string = 'image/png' 39 | ): Promise<{ id: string; filepath: string }> { 40 | try { 41 | // Generate a unique ID for the image 42 | const id = uuidv4(); 43 | 44 | // Determine the file extension based on MIME type 45 | let extension = '.png'; 46 | if (mimeType === 'image/jpeg' || mimeType === 'image/jpg') { 47 | extension = '.jpg'; 48 | } else if (mimeType === 'image/webp') { 49 | extension = '.webp'; 50 | } 51 | 52 | // Create the file path 53 | const filepath = path.resolve(IMAGE_STORAGE_DIR, `${id}${extension}`); 54 | 55 | // Convert base64 to buffer and save to disk 56 | const buffer = Buffer.from(imageBytes, 'base64'); 57 | await fs.writeFile(filepath, buffer); 58 | 59 | // Save metadata 60 | const metadata = { 61 | id, 62 | createdAt: new Date().toISOString(), 63 | prompt, 64 | mimeType, 65 | size: buffer.length, 66 | filepath 67 | }; 68 | 69 | const metadataPath = path.resolve(IMAGE_STORAGE_DIR, `${id}.json`); 70 | await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2)); 71 | 72 | log.info(`Image saved successfully with ID: ${id}`); 73 | return { id, filepath }; 74 | } catch (error) { 75 | log.error('Error saving generated image:', error); 76 | throw error; 77 | } 78 | } 79 | 80 | // Define schemas for tool inputs 81 | const AspectRatioSchema = z.enum(['16:9', '9:16']); 82 | const PersonGenerationSchema = z.enum(['dont_allow', 'allow_adult']); 83 | 84 | /** 85 | * Tool for generating a video from a text prompt 86 | * 87 | * @param args The tool arguments 88 | * @returns The tool result 89 | */ 90 | export async function generateVideoFromText(args: { 91 | prompt: string; 92 | aspectRatio?: '16:9' | '9:16'; 93 | personGeneration?: 'dont_allow' | 'allow_adult'; 94 | numberOfVideos?: 1 | 2; 95 | durationSeconds?: number; 96 | enhancePrompt?: boolean | string; 97 | negativePrompt?: string; 98 | includeFullData?: boolean | string; 99 | autoDownload?: boolean | string; 100 | }): Promise { 101 | try { 102 | log.info('Generating video from text prompt'); 103 | log.verbose('Text prompt parameters:', JSON.stringify(args)); 104 | 105 | // Convert string boolean parameters to actual booleans 106 | const enhancePrompt = typeof args.enhancePrompt === 'string' 107 | ? args.enhancePrompt.toLowerCase() === 'true' || args.enhancePrompt === '1' 108 | : args.enhancePrompt ?? false; 109 | 110 | const includeFullData = typeof args.includeFullData === 'string' 111 | ? args.includeFullData.toLowerCase() === 'true' || args.includeFullData === '1' 112 | : args.includeFullData ?? false; 113 | 114 | const autoDownload = typeof args.autoDownload === 'string' 115 | ? args.autoDownload.toLowerCase() === 'true' || args.autoDownload === '1' 116 | : args.autoDownload ?? true; 117 | 118 | // Create config object from individual parameters with defaults 119 | const config = { 120 | aspectRatio: args.aspectRatio || '16:9', 121 | personGeneration: args.personGeneration || 'dont_allow', 122 | numberOfVideos: args.numberOfVideos || 1, 123 | durationSeconds: args.durationSeconds || 5, 124 | enhancePrompt: enhancePrompt, 125 | negativePrompt: args.negativePrompt || '' 126 | }; 127 | 128 | // Options for video generation with defaults 129 | const options = { 130 | includeFullData: includeFullData, 131 | autoDownload: autoDownload 132 | }; 133 | 134 | // Generate the video 135 | const result = await veoClient.generateFromText(args.prompt, config, options); 136 | 137 | // Prepare response content 138 | const responseContent: Array = []; 139 | 140 | // If includeFullData is true and we have video data, include it in the response 141 | if (args.includeFullData && result.videoData) { 142 | responseContent.push({ 143 | type: 'image', // Use 'image' type for video content since MCP doesn't have a 'video' type 144 | mimeType: result.mimeType, 145 | data: result.videoData 146 | }); 147 | } 148 | 149 | // Add text content with metadata 150 | responseContent.push({ 151 | type: 'text', 152 | text: JSON.stringify({ 153 | success: true, 154 | message: 'Video generated successfully', 155 | videoId: result.id, 156 | resourceUri: `videos://${result.id}`, 157 | filepath: result.filepath, 158 | videoUrl: result.videoUrl, 159 | metadata: result 160 | }, null, 2) 161 | }); 162 | 163 | // Return the result 164 | return { 165 | content: responseContent 166 | }; 167 | } catch (error) { 168 | log.error('Error generating video from text:', error); 169 | 170 | // Return the error 171 | return { 172 | isError: true, 173 | content: [ 174 | { 175 | type: 'text', 176 | text: `Error generating video: ${error instanceof Error ? error.message : String(error)}` 177 | } 178 | ] 179 | }; 180 | } 181 | } 182 | 183 | /** 184 | * Tool for generating a video from an image 185 | * 186 | * @param args The tool arguments 187 | * @returns The tool result 188 | */ 189 | export async function generateVideoFromImage(args: { 190 | image: string | { type: 'image'; mimeType: string; data: string }; 191 | prompt?: string; 192 | aspectRatio?: '16:9' | '9:16'; 193 | numberOfVideos?: 1 | 2; 194 | durationSeconds?: number; 195 | enhancePrompt?: boolean | string; 196 | negativePrompt?: string; 197 | includeFullData?: boolean | string; 198 | autoDownload?: boolean | string; 199 | }): Promise { 200 | try { 201 | log.info('Generating video from image'); 202 | log.verbose('Image parameters:', JSON.stringify(args)); 203 | 204 | // Extract image data based on the type 205 | let imageData: string; 206 | let mimeType: string | undefined; 207 | 208 | if (typeof args.image === 'string') { 209 | // It's a URL or file path 210 | imageData = args.image; 211 | } else { 212 | // It's an ImageContent object 213 | imageData = args.image.data; 214 | mimeType = args.image.mimeType; 215 | } 216 | 217 | // Convert string boolean parameters to actual booleans 218 | const enhancePrompt = typeof args.enhancePrompt === 'string' 219 | ? args.enhancePrompt.toLowerCase() === 'true' || args.enhancePrompt === '1' 220 | : args.enhancePrompt ?? false; 221 | 222 | const includeFullData = typeof args.includeFullData === 'string' 223 | ? args.includeFullData.toLowerCase() === 'true' || args.includeFullData === '1' 224 | : args.includeFullData ?? false; 225 | 226 | const autoDownload = typeof args.autoDownload === 'string' 227 | ? args.autoDownload.toLowerCase() === 'true' || args.autoDownload === '1' 228 | : args.autoDownload ?? true; 229 | 230 | // Create config object from individual parameters with defaults 231 | const config = { 232 | aspectRatio: args.aspectRatio || '16:9', 233 | numberOfVideos: args.numberOfVideos || 1, 234 | durationSeconds: args.durationSeconds || 5, 235 | enhancePrompt: enhancePrompt, 236 | negativePrompt: args.negativePrompt || '' 237 | }; 238 | 239 | // Options for video generation with defaults 240 | const options = { 241 | includeFullData: includeFullData, 242 | autoDownload: autoDownload 243 | }; 244 | 245 | // Generate the video 246 | const result = await veoClient.generateFromImage( 247 | imageData, 248 | args.prompt, 249 | config, 250 | options, 251 | mimeType 252 | ); 253 | 254 | // Prepare response content 255 | const responseContent: Array = []; 256 | 257 | // If includeFullData is true and we have video data, include it in the response 258 | if (args.includeFullData && result.videoData) { 259 | responseContent.push({ 260 | type: 'image', // Use 'image' type for video content since MCP doesn't have a 'video' type 261 | mimeType: result.mimeType, 262 | data: result.videoData 263 | }); 264 | } 265 | 266 | // Add text content with metadata 267 | responseContent.push({ 268 | type: 'text', 269 | text: JSON.stringify({ 270 | success: true, 271 | message: 'Video generated successfully', 272 | videoId: result.id, 273 | resourceUri: `videos://${result.id}`, 274 | filepath: result.filepath, 275 | videoUrl: result.videoUrl, 276 | metadata: result 277 | }, null, 2) 278 | }); 279 | 280 | // Return the result 281 | return { 282 | content: responseContent 283 | }; 284 | } catch (error) { 285 | log.error('Error generating video from image:', error); 286 | 287 | // Return the error 288 | return { 289 | isError: true, 290 | content: [ 291 | { 292 | type: 'text', 293 | text: `Error generating video: ${error instanceof Error ? error.message : String(error)}` 294 | } 295 | ] 296 | }; 297 | } 298 | } 299 | 300 | /** 301 | * Tool for generating an image from a text prompt 302 | * 303 | * @param args The tool arguments 304 | * @returns The tool result with generated image 305 | */ 306 | export async function generateImage(args: { 307 | prompt: string; 308 | numberOfImages?: number; 309 | includeFullData?: boolean | string; 310 | }): Promise { 311 | try { 312 | log.info('Generating image from text prompt'); 313 | log.verbose('Image generation parameters:', JSON.stringify(args)); 314 | 315 | // Create config object 316 | const config = { 317 | numberOfImages: args.numberOfImages || 1 318 | }; 319 | 320 | // Generate the image using Imagen 321 | const response = await ai.models.generateImages({ 322 | model: "imagen-3.0-generate-002", 323 | prompt: args.prompt, 324 | config: config, 325 | }); 326 | 327 | if (!response.generatedImages || response.generatedImages.length === 0) { 328 | throw new Error('No images generated in the response'); 329 | } 330 | 331 | const generatedImage = response.generatedImages[0]; 332 | 333 | if (!generatedImage.image?.imageBytes) { 334 | throw new Error('Generated image missing image bytes'); 335 | } 336 | 337 | // Save the generated image to disk 338 | const { id, filepath } = await saveGeneratedImage( 339 | generatedImage.image.imageBytes, 340 | args.prompt, 341 | 'image/png' 342 | ); 343 | 344 | // Prepare response content 345 | const responseContent: Array = []; 346 | 347 | // Convert includeFullData to boolean if it's a string 348 | const includeFullData = typeof args.includeFullData === 'string' 349 | ? args.includeFullData.toLowerCase() === 'true' || args.includeFullData === '1' 350 | : args.includeFullData !== false; 351 | 352 | // If includeFullData is true (default) or not specified, include the image data 353 | if (includeFullData) { 354 | responseContent.push({ 355 | type: 'image', 356 | mimeType: 'image/png', 357 | data: generatedImage.image.imageBytes 358 | }); 359 | } 360 | 361 | // Add text content with metadata 362 | responseContent.push({ 363 | type: 'text', 364 | text: JSON.stringify({ 365 | success: true, 366 | message: 'Image generated successfully', 367 | imageId: id, 368 | resourceUri: `images://${id}`, 369 | filepath: filepath 370 | }, null, 2) 371 | }); 372 | 373 | // Return the result 374 | return { 375 | content: responseContent 376 | }; 377 | } catch (error) { 378 | log.error('Error generating image:', error); 379 | 380 | // Return the error 381 | return { 382 | isError: true, 383 | content: [ 384 | { 385 | type: 'text', 386 | text: `Error generating image: ${error instanceof Error ? error.message : String(error)}` 387 | } 388 | ] 389 | }; 390 | } 391 | } 392 | 393 | /** 394 | * Tool for generating a video from a generated image 395 | * 396 | * @param args The tool arguments 397 | * @returns The tool result 398 | */ 399 | export async function generateVideoFromGeneratedImage(args: { 400 | prompt: string; 401 | videoPrompt?: string; 402 | // Image generation parameters 403 | numberOfImages?: number; 404 | // Video generation parameters 405 | aspectRatio?: '16:9' | '9:16'; 406 | personGeneration?: 'dont_allow' | 'allow_adult'; 407 | numberOfVideos?: 1 | 2; 408 | durationSeconds?: number; 409 | enhancePrompt?: boolean | string; 410 | negativePrompt?: string; 411 | includeFullData?: boolean | string; 412 | autoDownload?: boolean | string; 413 | }): Promise { 414 | try { 415 | log.info('Generating video from generated image'); 416 | log.verbose('Image generation parameters:', JSON.stringify(args)); 417 | 418 | // Convert string boolean parameters to actual booleans 419 | const enhancePrompt = typeof args.enhancePrompt === 'string' 420 | ? args.enhancePrompt.toLowerCase() === 'true' || args.enhancePrompt === '1' 421 | : args.enhancePrompt ?? false; 422 | 423 | const includeFullData = typeof args.includeFullData === 'string' 424 | ? args.includeFullData.toLowerCase() === 'true' || args.includeFullData === '1' 425 | : args.includeFullData ?? false; 426 | 427 | const autoDownload = typeof args.autoDownload === 'string' 428 | ? args.autoDownload.toLowerCase() === 'true' || args.autoDownload === '1' 429 | : args.autoDownload ?? true; 430 | 431 | // Create image config with defaults 432 | const imageConfig = { 433 | numberOfImages: args.numberOfImages || 1 434 | }; 435 | 436 | // Create video config with defaults 437 | const videoConfig = { 438 | aspectRatio: args.aspectRatio || '16:9', 439 | personGeneration: args.personGeneration || 'dont_allow', 440 | numberOfVideos: args.numberOfVideos || 1, 441 | durationSeconds: args.durationSeconds || 5, 442 | enhancePrompt: enhancePrompt, 443 | negativePrompt: args.negativePrompt || '' 444 | }; 445 | 446 | // Options for video generation with defaults 447 | const options = { 448 | includeFullData: includeFullData, 449 | autoDownload: autoDownload 450 | }; 451 | 452 | // First generate the image 453 | const imageResponse = await ai.models.generateImages({ 454 | model: "imagen-3.0-generate-002", 455 | prompt: args.prompt, 456 | config: imageConfig, 457 | }); 458 | 459 | if (!imageResponse.generatedImages || imageResponse.generatedImages.length === 0) { 460 | throw new Error('No images generated in the response'); 461 | } 462 | 463 | const generatedImage = imageResponse.generatedImages[0]; 464 | 465 | if (!generatedImage.image?.imageBytes) { 466 | throw new Error('Generated image missing image bytes'); 467 | } 468 | 469 | // Save the generated image to disk 470 | const { id: imageId, filepath: imageFilepath } = await saveGeneratedImage( 471 | generatedImage.image.imageBytes, 472 | args.prompt, 473 | 'image/png' 474 | ); 475 | 476 | // Use the generated image to create a video 477 | const videoPrompt = args.videoPrompt || args.prompt; 478 | const result = await veoClient.generateFromImage( 479 | generatedImage.image.imageBytes, 480 | videoPrompt, 481 | videoConfig, 482 | options, 483 | 'image/png' 484 | ); 485 | 486 | // Prepare response content 487 | const responseContent: Array = []; 488 | 489 | // Always include the generated image 490 | responseContent.push({ 491 | type: 'image', 492 | mimeType: 'image/png', 493 | data: generatedImage.image.imageBytes 494 | }); 495 | 496 | // If includeFullData is true and we have video data, include it in the response 497 | if (args.includeFullData && result.videoData) { 498 | responseContent.push({ 499 | type: 'image', // Use 'image' type for video content since MCP doesn't have a 'video' type 500 | mimeType: result.mimeType, 501 | data: result.videoData 502 | }); 503 | } 504 | 505 | // Add text content with metadata 506 | responseContent.push({ 507 | type: 'text', 508 | text: JSON.stringify({ 509 | success: true, 510 | message: 'Video generated from image successfully', 511 | videoId: result.id, 512 | videoResourceUri: `videos://${result.id}`, 513 | videoFilepath: result.filepath, 514 | videoUrl: result.videoUrl, 515 | imageId: imageId, 516 | imageResourceUri: `images://${imageId}`, 517 | imageFilepath: imageFilepath, 518 | metadata: result 519 | }, null, 2) 520 | }); 521 | 522 | // Return the result 523 | return { 524 | content: responseContent 525 | }; 526 | } catch (error) { 527 | log.error('Error generating video from generated image:', error); 528 | 529 | // Return the error 530 | return { 531 | isError: true, 532 | content: [ 533 | { 534 | type: 'text', 535 | text: `Error generating video from generated image: ${error instanceof Error ? error.message : String(error)}` 536 | } 537 | ] 538 | }; 539 | } 540 | } 541 | 542 | /** 543 | * Gets image metadata by ID 544 | * 545 | * @param id The image ID 546 | * @returns The image metadata 547 | */ 548 | async function getImageMetadata(id: string): Promise { 549 | try { 550 | const metadataPath = path.resolve(IMAGE_STORAGE_DIR, `${id}.json`); 551 | const metadataJson = await fs.readFile(metadataPath, 'utf-8'); 552 | return JSON.parse(metadataJson); 553 | } catch (error) { 554 | log.error(`Error getting metadata for image ${id}:`, error); 555 | throw new Error(`Image metadata not found: ${id}`); 556 | } 557 | } 558 | 559 | /** 560 | * Tool for getting an image by ID 561 | * 562 | * @param args The tool arguments 563 | * @returns The tool result 564 | */ 565 | export async function getImage(args: { 566 | id: string; 567 | includeFullData?: boolean | string; 568 | }): Promise { 569 | try { 570 | log.info(`Getting image with ID: ${args.id}`); 571 | 572 | // Get the image metadata 573 | const metadata = await getImageMetadata(args.id); 574 | 575 | // Convert includeFullData to boolean if it's a string 576 | const includeFullData = typeof args.includeFullData === 'string' 577 | ? args.includeFullData.toLowerCase() === 'true' || args.includeFullData === '1' 578 | : args.includeFullData !== false; 579 | 580 | // Prepare response content 581 | const responseContent: Array = []; 582 | 583 | // If includeFullData is true (default) or not specified, include the image data 584 | if (includeFullData && metadata.filepath) { 585 | try { 586 | const imageData = await fs.readFile(metadata.filepath); 587 | responseContent.push({ 588 | type: 'image', 589 | mimeType: metadata.mimeType || 'image/png', 590 | data: imageData.toString('base64') 591 | }); 592 | } catch (error) { 593 | log.error(`Error reading image file ${metadata.filepath}:`, error); 594 | // Continue without the image data 595 | } 596 | } 597 | 598 | // Add text content with metadata 599 | responseContent.push({ 600 | type: 'text', 601 | text: JSON.stringify({ 602 | success: true, 603 | message: 'Image retrieved successfully', 604 | imageId: metadata.id, 605 | resourceUri: `images://${metadata.id}`, 606 | filepath: metadata.filepath, 607 | prompt: metadata.prompt, 608 | createdAt: metadata.createdAt, 609 | mimeType: metadata.mimeType, 610 | size: metadata.size 611 | }, null, 2) 612 | }); 613 | 614 | // Return the result 615 | return { 616 | content: responseContent 617 | }; 618 | } catch (error) { 619 | log.error(`Error getting image:`, error); 620 | 621 | // Return the error 622 | return { 623 | isError: true, 624 | content: [ 625 | { 626 | type: 'text', 627 | text: `Error getting image: ${error instanceof Error ? error.message : String(error)}` 628 | } 629 | ] 630 | }; 631 | } 632 | } 633 | 634 | /** 635 | * Tool for listing all generated images 636 | * 637 | * @returns The tool result 638 | */ 639 | export async function listGeneratedImages(): Promise { 640 | try { 641 | log.info('Listing all generated images'); 642 | 643 | // Get all files in the image storage directory 644 | const files = await fs.readdir(IMAGE_STORAGE_DIR); 645 | 646 | // Filter for JSON metadata files 647 | const metadataFiles = files.filter(file => file.endsWith('.json')); 648 | 649 | // Read and parse each metadata file 650 | const imagesPromises = metadataFiles.map(async file => { 651 | const filePath = path.resolve(IMAGE_STORAGE_DIR, file); 652 | try { 653 | const metadataJson = await fs.readFile(filePath, 'utf-8'); 654 | return JSON.parse(metadataJson); 655 | } catch (error) { 656 | log.error(`Error reading image metadata file ${filePath}:`, error); 657 | return null; 658 | } 659 | }); 660 | 661 | // Wait for all metadata to be read and filter out any null values 662 | const images = (await Promise.all(imagesPromises)).filter(image => image !== null); 663 | 664 | // Return the result 665 | return { 666 | content: [ 667 | { 668 | type: 'text', 669 | text: JSON.stringify({ 670 | success: true, 671 | count: images.length, 672 | images: images.map(image => ({ 673 | id: image.id, 674 | createdAt: image.createdAt, 675 | prompt: image.prompt, 676 | resourceUri: `images://${image.id}`, 677 | filepath: image.filepath, 678 | mimeType: image.mimeType, 679 | size: image.size 680 | })) 681 | }, null, 2) 682 | } 683 | ] 684 | }; 685 | } catch (error) { 686 | log.error('Error listing images:', error); 687 | 688 | // Return the error 689 | return { 690 | isError: true, 691 | content: [ 692 | { 693 | type: 'text', 694 | text: `Error listing images: ${error instanceof Error ? error.message : String(error)}` 695 | } 696 | ] 697 | }; 698 | } 699 | } 700 | 701 | /** 702 | * Tool for listing all generated videos 703 | * 704 | * @returns The tool result 705 | */ 706 | export async function listGeneratedVideos(): Promise { 707 | try { 708 | // Get all videos 709 | const videos = await veoClient.listVideos(); 710 | 711 | // Return the result 712 | return { 713 | content: [ 714 | { 715 | type: 'text', 716 | text: JSON.stringify({ 717 | success: true, 718 | count: videos.length, 719 | videos: videos.map(video => ({ 720 | id: video.id, 721 | createdAt: video.createdAt, 722 | prompt: video.prompt, 723 | resourceUri: `videos://${video.id}`, 724 | filepath: video.filepath, 725 | videoUrl: video.videoUrl 726 | })) 727 | }, null, 2) 728 | } 729 | ] 730 | }; 731 | } catch (error) { 732 | log.error('Error listing videos:', error); 733 | 734 | // Return the error 735 | return { 736 | isError: true, 737 | content: [ 738 | { 739 | type: 'text', 740 | text: `Error listing videos: ${error instanceof Error ? error.message : String(error)}` 741 | } 742 | ] 743 | }; 744 | } 745 | } 746 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import type { LogLevel } from '../config.js'; 2 | 3 | // Define log level priorities (higher number = higher priority) 4 | const LOG_LEVEL_PRIORITY: Record = { 5 | 'VERBOSE': 0, 6 | 'DEBUG': 1, 7 | 'INFO': 2, 8 | 'WARN': 3, 9 | 'ERROR': 4, 10 | 'FATAL': 5, 11 | 'NONE': 6 12 | }; 13 | 14 | /** 15 | * Simple logger utility for the MCP server 16 | * Respects the LOG_LEVEL environment variable 17 | */ 18 | class Logger { 19 | private currentLogLevel: string = 'FATAL'; // Default to FATAL 20 | 21 | /** 22 | * Initialize the logger with the configured log level 23 | * This should be called after config is loaded to avoid circular dependencies 24 | * 25 | * @param logLevel The log level from configuration 26 | */ 27 | initialize(logLevel: LogLevel): void { 28 | this.currentLogLevel = logLevel.toUpperCase(); 29 | } 30 | 31 | /** 32 | * Log a verbose message 33 | * 34 | * @param message The message to log 35 | * @param data Optional data to include 36 | */ 37 | verbose(message: string, data?: any): void { 38 | this.logWithLevel('VERBOSE', message, data); 39 | } 40 | 41 | /** 42 | * Log a debug message 43 | * 44 | * @param message The message to log 45 | * @param data Optional data to include 46 | */ 47 | debug(message: string, data?: any): void { 48 | this.logWithLevel('DEBUG', message, data); 49 | } 50 | 51 | /** 52 | * Log an info message 53 | * 54 | * @param message The message to log 55 | * @param data Optional data to include 56 | */ 57 | info(message: string, data?: any): void { 58 | this.logWithLevel('INFO', message, data); 59 | } 60 | 61 | /** 62 | * Log a warning message 63 | * 64 | * @param message The message to log 65 | * @param data Optional data to include 66 | */ 67 | warn(message: string, data?: any): void { 68 | this.logWithLevel('WARN', message, data); 69 | } 70 | 71 | /** 72 | * Log an error message 73 | * 74 | * @param message The message to log 75 | * @param data Optional data to include 76 | */ 77 | error(message: string, data?: any): void { 78 | this.logWithLevel('ERROR', message, data); 79 | } 80 | 81 | /** 82 | * Log a fatal message 83 | * 84 | * @param message The message to log 85 | * @param data Optional data to include 86 | */ 87 | fatal(message: string, data?: any): void { 88 | this.logWithLevel('FATAL', message, data); 89 | } 90 | 91 | /** 92 | * Check if a log level should be displayed based on the current log level setting 93 | * 94 | * @param level The log level to check 95 | * @returns True if the log level should be displayed 96 | */ 97 | private shouldLog(level: string): boolean { 98 | // If log level is NONE, don't log anything 99 | if (this.currentLogLevel === 'NONE') { 100 | return false; 101 | } 102 | 103 | // Get the priority of the current log level and the level being checked 104 | const currentPriority = LOG_LEVEL_PRIORITY[this.currentLogLevel] || 5; // Default to FATAL if not found 105 | const levelPriority = LOG_LEVEL_PRIORITY[level] || 0; // Default to lowest priority if not found 106 | 107 | // Only log if the level priority is >= the current log level priority 108 | return levelPriority >= currentPriority; 109 | } 110 | 111 | /** 112 | * Log a message with a specific level 113 | * Only logs messages at or above the configured log level 114 | * 115 | * @param level The log level 116 | * @param message The message to log 117 | * @param data Optional data to include 118 | */ 119 | private logWithLevel(level: string, message: string, data?: any): void { 120 | // Check if this log level should be displayed 121 | if (!this.shouldLog(level)) { 122 | return; 123 | } 124 | 125 | const timestamp = new Date().toISOString(); 126 | const formattedMessage = `[${timestamp}] [${level}] ${message}`; 127 | 128 | if (data !== undefined) { 129 | if (level === 'VERBOSE') { 130 | // For verbose logs, always use JSON.stringify for the entire data object 131 | console.error(formattedMessage, JSON.stringify(data)); 132 | } else { 133 | // For other log levels, pass the object directly 134 | console.error(formattedMessage, data); 135 | } 136 | } else { 137 | console.error(formattedMessage); 138 | } 139 | } 140 | } 141 | 142 | // Export a singleton instance 143 | export const log = new Logger(); 144 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "outDir": "dist", 9 | "sourceMap": true, 10 | "declaration": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | --------------------------------------------------------------------------------