# Directory Structure
```
├── .gitignore
├── .python-version
├── images
│ ├── magnet-claude.png
│ ├── magnet-webcam.jpg
│ ├── orange-claude.png
│ └── orange-webcam.jpg
├── pyproject.toml
├── README.md
├── uv.lock
└── videocapture_mcp.py
```
# Files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
```
1 | 3.10
2 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Python-generated files
2 | __pycache__/
3 | *.py[oc]
4 | build/
5 | dist/
6 | wheels/
7 | *.egg-info
8 |
9 | # Virtual environments
10 | .venv
11 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Video Still Capture MCP
2 |
3 | **A Model Context Protocol server for accessing and controlling webcams via OpenCV**
4 |
5 | ## Overview
6 |
7 | Video Still Capture MCP is a Python implementation of the Model Context Protocol (MCP) that provides AI assistants with the ability to access and control webcams and video sources through OpenCV. This server exposes a set of tools that allow language models to capture images, manipulate camera settings, and manage video connections. There is no video capture.
8 |
9 | ## Examples
10 |
11 | Here are some examples of the Video Still Capture MCP server in action:
12 |
13 | ### Orange Example
14 | Left: Claude's view of the image | Right: Actual webcam capture
15 | :-------------------------:|:-------------------------:
16 |  | 
17 |
18 | ### Magnet Example
19 | Left: Claude's view of the image | Right: Actual webcam capture
20 | :-------------------------:|:-------------------------:
21 |  | 
22 |
23 | ## Installation
24 |
25 | ### Prerequisites
26 |
27 | - Python 3.10+
28 | - [OpenCV](https://opencv.org/) (`opencv-python`)
29 | - [MCP Python SDK](https://modelcontextprotocol.io/docs/)
30 | - [UV](https://astral.sh/uv/) (optional)
31 |
32 | ### Installation from source
33 |
34 | ```bash
35 | git clone https://github.com/13rac1/videocapture-mcp.git
36 | cd videocapture-mcp
37 | pip install -e .
38 | ```
39 |
40 | Run the MCP server:
41 |
42 | ```bash
43 | mcp dev videocapture_mcp.py
44 | ```
45 |
46 | ## Integrating with Claude for Desktop
47 |
48 | ### macOS/Linux
49 |
50 | Edit your Claude Desktop configuration:
51 |
52 | ```bash
53 | # Mac
54 | nano ~/Library/Application\ Support/Claude/claude_desktop_config.json
55 | # Linux
56 | nano ~/.config/Claude/claude_desktop_config.json
57 | ```
58 |
59 | Add this MCP server configuration:
60 |
61 | ```json
62 | {
63 | "mcpServers": {
64 | "VideoCapture ": {
65 | "command": "uv",
66 | "args": [
67 | "run",
68 | "--with",
69 | "mcp[cli]",
70 | "--with",
71 | "numpy",
72 | "--with",
73 | "opencv-python",
74 | "mcp",
75 | "run",
76 | "/ABSOLUTE_PATH/videocapture_mcp.py"
77 | ]
78 | }
79 | }
80 | }
81 | ```
82 |
83 | Ensure you replace `/ABSOLUTE_PATH/videocapture-mcp` with the project's absolute path.
84 |
85 | ### Windows
86 |
87 | Edit your Claude Desktop configuration:
88 |
89 | ```powershell
90 | nano $env:AppData\Claude\claude_desktop_config.json
91 | ```
92 |
93 | Add this MCP server configuration:
94 |
95 | ```json
96 | {
97 | "mcpServers": {
98 | "VideoCapture": {
99 | "command": "uv",
100 | "args": [
101 | "run",
102 | "--with",
103 | "mcp[cli]",
104 | "--with",
105 | "numpy",
106 | "--with",
107 | "opencv-python",
108 | "mcp",
109 | "run",
110 | "C:\ABSOLUTE_PATH\videocapture-mcp\videocapture_mcp.py"
111 | ]
112 | }
113 | }
114 | }
115 | ```
116 |
117 | Ensure you replace `C:\ABSOLUTE_PATH\videocapture-mcp` with the project's absolute path.
118 |
119 | ### Using the Installation Command
120 |
121 | Alternatively, you can use the `mcp` CLI to install the server:
122 |
123 | ```bash
124 | mcp install videocapture_mcp.py
125 | ```
126 |
127 | This will automatically configure Claude Desktop to use your videocapture MCP server.
128 |
129 | Once integrated, Claude will be able to access your webcam or video source when requested. Simply ask Claude to take a photo or perform any webcam-related task.
130 |
131 | ## Features
132 |
133 | - **Quick Image Capture**: Capture a single image from a webcam without managing connections
134 | - **Connection Management**: Open, manage, and close camera connections
135 | - **Video Properties**: Read and adjust camera settings like brightness, contrast, and resolution
136 | - **Image Processing**: Basic image transformations like horizontal flipping
137 |
138 | ## Tools Reference
139 |
140 | ### `quick_capture`
141 |
142 | Quickly open a camera, capture a single frame, and close it.
143 |
144 | ```python
145 | quick_capture(device_index: int = 0, flip: bool = False) -> Image
146 | ```
147 |
148 | - **device_index**: Camera index (0 is usually the default webcam)
149 | - **flip**: Whether to horizontally flip the image
150 | - **Returns**: The captured frame as an Image object
151 |
152 | ### `open_camera`
153 |
154 | Open a connection to a camera device.
155 |
156 | ```python
157 | open_camera(device_index: int = 0, name: Optional[str] = None) -> str
158 | ```
159 |
160 | - **device_index**: Camera index (0 is usually the default webcam)
161 | - **name**: Optional name to identify this camera connection
162 | - **Returns**: Connection ID for the opened camera
163 |
164 | ### `capture_frame`
165 |
166 | Capture a single frame from the specified video source.
167 |
168 | ```python
169 | capture_frame(connection_id: str, flip: bool = False) -> Image
170 | ```
171 |
172 | - **connection_id**: ID of the previously opened video connection
173 | - **flip**: Whether to horizontally flip the image
174 | - **Returns**: The captured frame as an Image object
175 |
176 | ### `get_video_properties`
177 |
178 | Get properties of the video source.
179 |
180 | ```python
181 | get_video_properties(connection_id: str) -> dict
182 | ```
183 |
184 | - **connection_id**: ID of the previously opened video connection
185 | - **Returns**: Dictionary of video properties (width, height, fps, etc.)
186 |
187 | ### `set_video_property`
188 |
189 | Set a property of the video source.
190 |
191 | ```python
192 | set_video_property(connection_id: str, property_name: str, value: float) -> bool
193 | ```
194 |
195 | - **connection_id**: ID of the previously opened video connection
196 | - **property_name**: Name of the property to set (width, height, brightness, etc.)
197 | - **value**: Value to set
198 | - **Returns**: True if successful, False otherwise
199 |
200 | ### `close_connection`
201 |
202 | Close a video connection and release resources.
203 |
204 | ```python
205 | close_connection(connection_id: str) -> bool
206 | ```
207 |
208 | - **connection_id**: ID of the connection to close
209 | - **Returns**: True if successful
210 |
211 | ### `list_active_connections`
212 |
213 | List all active video connections.
214 |
215 | ```python
216 | list_active_connections() -> list
217 | ```
218 |
219 | - **Returns**: List of active connection IDs
220 |
221 | ## Example Usage
222 |
223 | Here's how an AI assistant might use the Webcam MCP server:
224 |
225 | 1. **Take a quick photo**:
226 | ```
227 | I'll take a photo using your webcam.
228 | ```
229 | (The AI would call `quick_capture()` behind the scenes)
230 |
231 | 2. **Open a persistent connection**:
232 | ```
233 | I'll open a connection to your webcam so we can take multiple photos.
234 | ```
235 | (The AI would call `open_camera()` and store the connection ID)
236 |
237 | 3. **Adjust camera settings**:
238 | ```
239 | Let me increase the brightness of the webcam feed.
240 | ```
241 | (The AI would call `set_video_property()` with the appropriate parameters)
242 |
243 | ## Advanced Usage
244 |
245 | ### Resource Management
246 |
247 | The server automatically manages camera resources, ensuring all connections are properly released when the server shuts down. For long-running applications, it's good practice to explicitly close connections when they're no longer needed.
248 |
249 | ### Multiple Cameras
250 |
251 | If your system has multiple cameras, you can specify the device index when opening a connection:
252 |
253 | ```python
254 | # Open the second webcam (index 1)
255 | connection_id = open_camera(device_index=1)
256 | ```
257 |
258 | ## Troubleshooting
259 |
260 | - **Camera Not Found**: Ensure your webcam is properly connected and not in use by another application
261 | - **Permission Issues**: Some systems require explicit permission to access the camera
262 | - **OpenCV Installation**: If you encounter issues with OpenCV, refer to the [official installation guide](https://docs.opencv.org/master/d5/de5/tutorial_py_setup_in_windows.html)
263 |
264 | ## License
265 |
266 | This project is licensed under the MIT License - see the LICENSE file for details.
267 |
268 | ## Contributing
269 |
270 | Contributions are welcome! Please feel free to submit a Pull Request.
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
1 | [project]
2 | name = "videocapture-mcp"
3 | version = "0.1.0"
4 | description = "Model Context Protocol (MCP) server to capture from an OpenCV-compatible webcam"
5 | readme = "README.md"
6 | requires-python = ">=3.10"
7 | dependencies = [
8 | "httpx>=0.28.1",
9 | "mcp[cli]>=1.4.1",
10 | "opencv-python>=4.11.0.86",
11 | ]
12 |
13 | [[project.authors]]
14 | name = "13rac1"
15 |
16 | [project.scripts]
17 | videocapture_mcp = "videocapture_mcp.main"
```
--------------------------------------------------------------------------------
/videocapture_mcp.py:
--------------------------------------------------------------------------------
```python
1 | import cv2
2 | from contextlib import asynccontextmanager
3 | from collections.abc import AsyncIterator
4 | from dataclasses import dataclass
5 | from datetime import datetime
6 | from typing import Optional, Dict
7 | from mcp.server.fastmcp import FastMCP, Image
8 |
9 | # Store active video capture objects
10 | active_captures: Dict[str, cv2.VideoCapture] = {}
11 |
12 | # Define our application context
13 | @dataclass
14 | class AppContext:
15 | active_captures: Dict[str, cv2.VideoCapture]
16 |
17 | @asynccontextmanager
18 | async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
19 | """Manage application lifecycle with camera resource cleanup"""
20 | # Initialize on startup
21 | #print("Starting VideoCapture MCP Server")
22 | try:
23 | # Pass the active_captures dictionary in the context
24 | yield AppContext(active_captures=active_captures)
25 | finally:
26 | # Cleanup on shutdown
27 | #print("Shutting down VideoCapture MCP Server")
28 | for connection_id, cap in active_captures.items():
29 | cap.release()
30 | active_captures.clear()
31 |
32 | # Initialize the FastMCP server with lifespan
33 | mcp = FastMCP("VideoCapture",
34 | description="Provides access to camera and video streams via OpenCV",
35 | dependencies=["opencv-python", "numpy"],
36 | lifespan=app_lifespan)
37 |
38 | def main():
39 | """Main entry point for the VideoCapture Server"""
40 |
41 | mcp.run()
42 |
43 | @mcp.tool()
44 | def quick_capture(device_index: int = 0, flip: bool = False) -> Image:
45 | """
46 | Quickly open a camera, capture a single frame, and close it.
47 | If the camera is already open, use the existing connection.
48 |
49 | Args:
50 | device_index: Camera index (0 is usually the default webcam)
51 | flip: Whether to horizontally flip the image
52 |
53 | Returns:
54 | The captured frame as an Image object
55 | """
56 | # Check if this device is already open
57 | device_key = None
58 | for key, cap in active_captures.items():
59 | if key.startswith(f"camera_{device_index}_"):
60 | device_key = key
61 | break
62 |
63 | # If device is not already open, open it temporarily
64 | temp_connection = False
65 | if device_key is None:
66 | device_key = open_camera(device_index)
67 | temp_connection = True
68 |
69 | try:
70 | # Capture the frame
71 | frame = capture_frame(device_key, flip)
72 | return frame
73 | finally:
74 | # Close the connection if we opened it temporarily
75 | if temp_connection:
76 | close_connection(device_key)
77 |
78 | @mcp.tool()
79 | def open_camera(device_index: int = 0, name: Optional[str] = None) -> str:
80 | """
81 | Open a connection to a camera device.
82 |
83 | Args:
84 | device_index: Camera index (0 is usually the default webcam)
85 | name: Optional name to identify this camera connection
86 |
87 | Returns:
88 | Connection ID for the opened camera
89 | """
90 | if name is None:
91 | name = f"camera_{device_index}_{datetime.now().strftime('%Y%m%d%H%M%S')}"
92 |
93 | cap = cv2.VideoCapture(device_index)
94 | if not cap.isOpened():
95 | raise ValueError(f"Failed to open camera at index {device_index}")
96 |
97 | active_captures[name] = cap
98 | return name
99 |
100 | @mcp.tool()
101 | def capture_frame(connection_id: str, flip: bool = False) -> Image:
102 | """
103 | Capture a single frame from the specified video source.
104 |
105 | Args:
106 | connection_id: ID of the previously opened video connection
107 | flip: Whether to horizontally flip the image
108 |
109 | Returns:
110 | The captured frame as an Image object
111 | """
112 | if connection_id not in active_captures:
113 | raise ValueError(f"No active connection with ID: {connection_id}")
114 |
115 | cap = active_captures[connection_id]
116 | ret, frame = cap.read()
117 |
118 | if not ret:
119 | raise RuntimeError(f"Failed to capture frame from {connection_id}")
120 |
121 | if flip:
122 | frame = cv2.flip(frame, 1) # 1 for horizontal flip
123 |
124 |
125 | # Encode the image as PNG
126 | _, png_data = cv2.imencode('.png', frame)
127 |
128 | # Return as MCP Image object
129 | return Image(data=png_data.tobytes(),
130 | format="png")
131 |
132 | @mcp.tool()
133 | def get_video_properties(connection_id: str) -> dict:
134 | """
135 | Get properties of the video source.
136 |
137 | Args:
138 | connection_id: ID of the previously opened video connection
139 |
140 | Returns:
141 | Dictionary of video properties
142 | """
143 | if connection_id not in active_captures:
144 | raise ValueError(f"No active connection with ID: {connection_id}")
145 |
146 | cap = active_captures[connection_id]
147 |
148 | properties = {
149 | "width": int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
150 | "height": int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),
151 | "fps": cap.get(cv2.CAP_PROP_FPS),
152 | "frame_count": int(cap.get(cv2.CAP_PROP_FRAME_COUNT)),
153 | "brightness": cap.get(cv2.CAP_PROP_BRIGHTNESS),
154 | "contrast": cap.get(cv2.CAP_PROP_CONTRAST),
155 | "saturation": cap.get(cv2.CAP_PROP_SATURATION),
156 | "format": int(cap.get(cv2.CAP_PROP_FORMAT))
157 | }
158 |
159 | return properties
160 |
161 | @mcp.tool()
162 | def set_video_property(connection_id: str, property_name: str, value: float) -> bool:
163 | """
164 | Set a property of the video source.
165 |
166 | Args:
167 | connection_id: ID of the previously opened video connection
168 | property_name: Name of the property to set (width, height, brightness, etc.)
169 | value: Value to set
170 |
171 | Returns:
172 | True if successful, False otherwise
173 | """
174 | if connection_id not in active_captures:
175 | raise ValueError(f"No active connection with ID: {connection_id}")
176 |
177 | cap = active_captures[connection_id]
178 |
179 | property_map = {
180 | "width": cv2.CAP_PROP_FRAME_WIDTH,
181 | "height": cv2.CAP_PROP_FRAME_HEIGHT,
182 | "fps": cv2.CAP_PROP_FPS,
183 | "brightness": cv2.CAP_PROP_BRIGHTNESS,
184 | "contrast": cv2.CAP_PROP_CONTRAST,
185 | "saturation": cv2.CAP_PROP_SATURATION,
186 | "auto_exposure": cv2.CAP_PROP_AUTO_EXPOSURE,
187 | "auto_focus": cv2.CAP_PROP_AUTOFOCUS
188 | }
189 |
190 | if property_name not in property_map:
191 | raise ValueError(f"Unknown property: {property_name}")
192 |
193 | return cap.set(property_map[property_name], value)
194 |
195 | @mcp.tool()
196 | def close_connection(connection_id: str) -> bool:
197 | """
198 | Close a video connection and release resources.
199 |
200 | Args:
201 | connection_id: ID of the connection to close
202 |
203 | Returns:
204 | True if successful
205 | """
206 | if connection_id not in active_captures:
207 | raise ValueError(f"No active connection with ID: {connection_id}")
208 |
209 | active_captures[connection_id].release()
210 | del active_captures[connection_id]
211 | return True
212 |
213 | @mcp.tool()
214 | def list_active_connections() -> list:
215 | """
216 | List all active video connections.
217 |
218 | Returns:
219 | List of active connection IDs
220 | """
221 | return list(active_captures.keys())
222 |
223 | mcp.run(transport='stdio')
224 |
225 | # For: $ mcp run videocapture_mcp.py
226 | def run():
227 | main()
228 |
229 | if __name__ == "__main__":
230 | main()
231 |
```