├── .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 | [](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 |
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 | 
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 |
--------------------------------------------------------------------------------