vidcontext-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vidcontext-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for VidContext — Give your AI agent eyes
|
|
5
|
+
Project-URL: Homepage, https://www.vidcontext.com
|
|
6
|
+
Project-URL: Documentation, https://www.vidcontext.com/app/developer
|
|
7
|
+
Project-URL: Repository, https://github.com/GingerofOzz/vidcontext-mcp
|
|
8
|
+
Project-URL: Issues, https://github.com/GingerofOzz/vidcontext-mcp/issues
|
|
9
|
+
Author-email: VidContext <support@vidcontext.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: ai,analysis,claude,cursor,mcp,vidcontext,video
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Multimedia :: Video
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: httpx>=0.27.0
|
|
24
|
+
Requires-Dist: mcp[cli]>=1.0.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# VidContext MCP Server
|
|
28
|
+
|
|
29
|
+
Give your AI agent eyes. Analyze any video directly from Claude Desktop, Cursor, or Claude Code.
|
|
30
|
+
|
|
31
|
+
VidContext processes video files and returns detailed text descriptions or expert analysis scored across 7 proprietary frameworks — letting any AI model understand video content.
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
### 1. Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install vidcontext-mcp
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or with [uv](https://docs.astral.sh/uv/) (recommended):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv pip install vidcontext-mcp
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Get Your API Key
|
|
48
|
+
|
|
49
|
+
1. Sign up at [vidcontext.com](https://www.vidcontext.com)
|
|
50
|
+
2. Purchase a credit pack (required for API access)
|
|
51
|
+
3. Go to [Developer Settings](https://www.vidcontext.com/app/developer)
|
|
52
|
+
4. Create an API key — save it, it's shown only once
|
|
53
|
+
|
|
54
|
+
### 3. Configure Your AI Tool
|
|
55
|
+
|
|
56
|
+
#### Claude Desktop
|
|
57
|
+
|
|
58
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"mcpServers": {
|
|
63
|
+
"vidcontext": {
|
|
64
|
+
"command": "vidcontext-mcp",
|
|
65
|
+
"env": {
|
|
66
|
+
"VIDCONTEXT_API_KEY": "vc_your_api_key_here"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Restart Claude Desktop. You'll see a hammer icon in the chat input showing VidContext tools.
|
|
74
|
+
|
|
75
|
+
#### Cursor
|
|
76
|
+
|
|
77
|
+
Add to `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` for global):
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"mcpServers": {
|
|
82
|
+
"vidcontext": {
|
|
83
|
+
"command": "vidcontext-mcp",
|
|
84
|
+
"env": {
|
|
85
|
+
"VIDCONTEXT_API_KEY": "vc_your_api_key_here"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
#### Claude Code
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
claude mcp add vidcontext -- vidcontext-mcp
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Then set your API key as an environment variable:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
export VIDCONTEXT_API_KEY="vc_your_api_key_here"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Available Tools
|
|
105
|
+
|
|
106
|
+
### `analyze_video`
|
|
107
|
+
|
|
108
|
+
The main tool. Upload a video file or URL, get back a complete text analysis.
|
|
109
|
+
|
|
110
|
+
**Parameters:**
|
|
111
|
+
- `file_path` (required): Local file path or URL to a video
|
|
112
|
+
- `output_format` (optional): `"context"` (default) or `"analysis"`
|
|
113
|
+
|
|
114
|
+
**Modes:**
|
|
115
|
+
- **Context mode** — Detailed scene-by-scene description with timestamps, transcript, visual elements, audio, and on-screen text. Perfect for giving any AI model "eyes" to understand video content.
|
|
116
|
+
- **Analysis mode** — Expert analysis scored across 7 proprietary frameworks: Hook, Retention, Scripting, CTA, Editing, Performance, and Platform Optimization. Built from a corpus of 2,000+ expert videos.
|
|
117
|
+
|
|
118
|
+
**Example prompts:**
|
|
119
|
+
- "Analyze the video at ~/Downloads/demo.mp4"
|
|
120
|
+
- "Give me an expert analysis of this video: /path/to/video.mov"
|
|
121
|
+
- "Describe what happens in https://example.com/video.mp4"
|
|
122
|
+
|
|
123
|
+
### `check_job_status`
|
|
124
|
+
|
|
125
|
+
Check on a previous analysis job (useful if processing timed out on a long video).
|
|
126
|
+
|
|
127
|
+
**Parameters:**
|
|
128
|
+
- `job_id` (required): The job ID from a previous `analyze_video` call
|
|
129
|
+
|
|
130
|
+
### `check_credits`
|
|
131
|
+
|
|
132
|
+
See your current credit balance, tier, and usage limits.
|
|
133
|
+
|
|
134
|
+
### `get_account`
|
|
135
|
+
|
|
136
|
+
View your account details, subscription status, and credit information.
|
|
137
|
+
|
|
138
|
+
## Pricing
|
|
139
|
+
|
|
140
|
+
- **1 credit = 1 minute of video** (rounded up)
|
|
141
|
+
- Credit packs: 10/$5, 50/$20, 250/$80
|
|
142
|
+
- Subscriptions: Starter $15/mo (40 credits), Pro $35/mo (100 credits), Business $69/mo (250 credits)
|
|
143
|
+
|
|
144
|
+
## Supported Formats
|
|
145
|
+
|
|
146
|
+
MP4, MOV, AVI, MKV, WebM, M4V, FLV, WMV
|
|
147
|
+
|
|
148
|
+
**Limits:**
|
|
149
|
+
- Max file size: 500MB
|
|
150
|
+
- Max duration: 15 minutes
|
|
151
|
+
- Processing time: 30-180 seconds depending on length
|
|
152
|
+
|
|
153
|
+
## Troubleshooting
|
|
154
|
+
|
|
155
|
+
**"VIDCONTEXT_API_KEY not set"** — Make sure your API key is in the `env` section of your MCP config.
|
|
156
|
+
|
|
157
|
+
**"Insufficient credits"** — Buy more at [vidcontext.com](https://www.vidcontext.com).
|
|
158
|
+
|
|
159
|
+
**"Invalid API key"** — Double-check your key at [Developer Settings](https://www.vidcontext.com/app/developer). Keys start with `vc_`.
|
|
160
|
+
|
|
161
|
+
**Tool not showing up** — Restart Claude Desktop/Cursor after editing the config file.
|
|
162
|
+
|
|
163
|
+
## Links
|
|
164
|
+
|
|
165
|
+
- Website: [vidcontext.com](https://www.vidcontext.com)
|
|
166
|
+
- API: [api.vidcontext.com](https://api.vidcontext.com)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# VidContext MCP Server
|
|
2
|
+
|
|
3
|
+
Give your AI agent eyes. Analyze any video directly from Claude Desktop, Cursor, or Claude Code.
|
|
4
|
+
|
|
5
|
+
VidContext processes video files and returns detailed text descriptions or expert analysis scored across 7 proprietary frameworks — letting any AI model understand video content.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
### 1. Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install vidcontext-mcp
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or with [uv](https://docs.astral.sh/uv/) (recommended):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv pip install vidcontext-mcp
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 2. Get Your API Key
|
|
22
|
+
|
|
23
|
+
1. Sign up at [vidcontext.com](https://www.vidcontext.com)
|
|
24
|
+
2. Purchase a credit pack (required for API access)
|
|
25
|
+
3. Go to [Developer Settings](https://www.vidcontext.com/app/developer)
|
|
26
|
+
4. Create an API key — save it, it's shown only once
|
|
27
|
+
|
|
28
|
+
### 3. Configure Your AI Tool
|
|
29
|
+
|
|
30
|
+
#### Claude Desktop
|
|
31
|
+
|
|
32
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"mcpServers": {
|
|
37
|
+
"vidcontext": {
|
|
38
|
+
"command": "vidcontext-mcp",
|
|
39
|
+
"env": {
|
|
40
|
+
"VIDCONTEXT_API_KEY": "vc_your_api_key_here"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Restart Claude Desktop. You'll see a hammer icon in the chat input showing VidContext tools.
|
|
48
|
+
|
|
49
|
+
#### Cursor
|
|
50
|
+
|
|
51
|
+
Add to `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` for global):
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"vidcontext": {
|
|
57
|
+
"command": "vidcontext-mcp",
|
|
58
|
+
"env": {
|
|
59
|
+
"VIDCONTEXT_API_KEY": "vc_your_api_key_here"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### Claude Code
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
claude mcp add vidcontext -- vidcontext-mcp
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Then set your API key as an environment variable:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
export VIDCONTEXT_API_KEY="vc_your_api_key_here"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Available Tools
|
|
79
|
+
|
|
80
|
+
### `analyze_video`
|
|
81
|
+
|
|
82
|
+
The main tool. Upload a video file or URL, get back a complete text analysis.
|
|
83
|
+
|
|
84
|
+
**Parameters:**
|
|
85
|
+
- `file_path` (required): Local file path or URL to a video
|
|
86
|
+
- `output_format` (optional): `"context"` (default) or `"analysis"`
|
|
87
|
+
|
|
88
|
+
**Modes:**
|
|
89
|
+
- **Context mode** — Detailed scene-by-scene description with timestamps, transcript, visual elements, audio, and on-screen text. Perfect for giving any AI model "eyes" to understand video content.
|
|
90
|
+
- **Analysis mode** — Expert analysis scored across 7 proprietary frameworks: Hook, Retention, Scripting, CTA, Editing, Performance, and Platform Optimization. Built from a corpus of 2,000+ expert videos.
|
|
91
|
+
|
|
92
|
+
**Example prompts:**
|
|
93
|
+
- "Analyze the video at ~/Downloads/demo.mp4"
|
|
94
|
+
- "Give me an expert analysis of this video: /path/to/video.mov"
|
|
95
|
+
- "Describe what happens in https://example.com/video.mp4"
|
|
96
|
+
|
|
97
|
+
### `check_job_status`
|
|
98
|
+
|
|
99
|
+
Check on a previous analysis job (useful if processing timed out on a long video).
|
|
100
|
+
|
|
101
|
+
**Parameters:**
|
|
102
|
+
- `job_id` (required): The job ID from a previous `analyze_video` call
|
|
103
|
+
|
|
104
|
+
### `check_credits`
|
|
105
|
+
|
|
106
|
+
See your current credit balance, tier, and usage limits.
|
|
107
|
+
|
|
108
|
+
### `get_account`
|
|
109
|
+
|
|
110
|
+
View your account details, subscription status, and credit information.
|
|
111
|
+
|
|
112
|
+
## Pricing
|
|
113
|
+
|
|
114
|
+
- **1 credit = 1 minute of video** (rounded up)
|
|
115
|
+
- Credit packs: 10/$5, 50/$20, 250/$80
|
|
116
|
+
- Subscriptions: Starter $15/mo (40 credits), Pro $35/mo (100 credits), Business $69/mo (250 credits)
|
|
117
|
+
|
|
118
|
+
## Supported Formats
|
|
119
|
+
|
|
120
|
+
MP4, MOV, AVI, MKV, WebM, M4V, FLV, WMV
|
|
121
|
+
|
|
122
|
+
**Limits:**
|
|
123
|
+
- Max file size: 500MB
|
|
124
|
+
- Max duration: 15 minutes
|
|
125
|
+
- Processing time: 30-180 seconds depending on length
|
|
126
|
+
|
|
127
|
+
## Troubleshooting
|
|
128
|
+
|
|
129
|
+
**"VIDCONTEXT_API_KEY not set"** — Make sure your API key is in the `env` section of your MCP config.
|
|
130
|
+
|
|
131
|
+
**"Insufficient credits"** — Buy more at [vidcontext.com](https://www.vidcontext.com).
|
|
132
|
+
|
|
133
|
+
**"Invalid API key"** — Double-check your key at [Developer Settings](https://www.vidcontext.com/app/developer). Keys start with `vc_`.
|
|
134
|
+
|
|
135
|
+
**Tool not showing up** — Restart Claude Desktop/Cursor after editing the config file.
|
|
136
|
+
|
|
137
|
+
## Links
|
|
138
|
+
|
|
139
|
+
- Website: [vidcontext.com](https://www.vidcontext.com)
|
|
140
|
+
- API: [api.vidcontext.com](https://api.vidcontext.com)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vidcontext-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server for VidContext — Give your AI agent eyes"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "VidContext", email = "support@vidcontext.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["mcp", "video", "analysis", "ai", "claude", "cursor", "vidcontext"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Multimedia :: Video",
|
|
26
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"mcp[cli]>=1.0.0",
|
|
30
|
+
"httpx>=0.27.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://www.vidcontext.com"
|
|
35
|
+
Documentation = "https://www.vidcontext.com/app/developer"
|
|
36
|
+
Repository = "https://github.com/GingerofOzz/vidcontext-mcp"
|
|
37
|
+
Issues = "https://github.com/GingerofOzz/vidcontext-mcp/issues"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/vidcontext_mcp"]
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
vidcontext-mcp = "vidcontext_mcp.server:main"
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# VidContext MCP Server — wraps the VidContext API for use in Claude Desktop, Cursor, and Claude Code.
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from mcp.server.fastmcp import FastMCP
|
|
10
|
+
|
|
11
|
+
mcp = FastMCP(
|
|
12
|
+
"vidcontext",
|
|
13
|
+
instructions=(
|
|
14
|
+
"Video Intelligence API — analyze any video and get detailed text descriptions "
|
|
15
|
+
"or expert framework analysis. This server runs LOCALLY on the user's machine "
|
|
16
|
+
"and can access any local file path. When the user wants to analyze a video, "
|
|
17
|
+
"ask them for the full file path on their computer (e.g. ~/Downloads/video.mp4). "
|
|
18
|
+
"Do NOT ask them to upload the file into the chat — ask for the path instead. "
|
|
19
|
+
"The tool also accepts URLs to videos hosted online. "
|
|
20
|
+
"IMPORTANT: When analyze_video returns results, you MUST present the COMPLETE "
|
|
21
|
+
"output to the user exactly as returned. Do not summarize, truncate, or condense "
|
|
22
|
+
"the output. The full detailed analysis is what the user is paying for. "
|
|
23
|
+
"Use analyze_video to process videos, check_credits to verify balance."
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
SUPPORTED_EXTENSIONS = {".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v", ".flv", ".wmv"}
|
|
28
|
+
MAX_POLL_SECONDS = 600
|
|
29
|
+
MAX_FILE_BYTES = 500 * 1024 * 1024 # 500MB
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_api_url() -> str:
|
|
33
|
+
"""Get API base URL from environment."""
|
|
34
|
+
return os.getenv("VIDCONTEXT_API_URL", "https://api.vidcontext.com")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _headers() -> dict[str, str]:
|
|
38
|
+
"""Build request headers with API key authentication."""
|
|
39
|
+
key = os.getenv("VIDCONTEXT_API_KEY", "")
|
|
40
|
+
if not key:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
"VIDCONTEXT_API_KEY not set. "
|
|
43
|
+
"Get your API key at https://www.vidcontext.com/app/developer"
|
|
44
|
+
)
|
|
45
|
+
return {"X-API-Key": key}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def _api_get(endpoint: str) -> dict:
|
|
49
|
+
"""Authenticated GET request to VidContext API."""
|
|
50
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
51
|
+
resp = await client.get(f"{_get_api_url()}{endpoint}", headers=_headers())
|
|
52
|
+
resp.raise_for_status()
|
|
53
|
+
return resp.json()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def _upload_video(file_path: str, output_format: str) -> dict:
|
|
57
|
+
"""Upload a video file to the VidContext API for processing."""
|
|
58
|
+
path = Path(file_path)
|
|
59
|
+
filename = path.name
|
|
60
|
+
|
|
61
|
+
# Guard against uploading files that exceed the API limit
|
|
62
|
+
file_size = path.stat().st_size
|
|
63
|
+
if file_size > MAX_FILE_BYTES:
|
|
64
|
+
return {"error": f"File too large ({file_size // (1024 * 1024)}MB). Max 500MB."}
|
|
65
|
+
|
|
66
|
+
# Scale timeout with file size: 30s base + 1s per MB
|
|
67
|
+
write_timeout = max(60.0, 30.0 + file_size / (1024 * 1024))
|
|
68
|
+
upload_timeout = httpx.Timeout(connect=10.0, read=300.0, write=write_timeout, pool=30.0)
|
|
69
|
+
|
|
70
|
+
async with httpx.AsyncClient(timeout=upload_timeout) as client:
|
|
71
|
+
with open(file_path, "rb") as f:
|
|
72
|
+
resp = await client.post(
|
|
73
|
+
f"{_get_api_url()}/v1/analyze",
|
|
74
|
+
headers=_headers(),
|
|
75
|
+
files={"file": (filename, f)},
|
|
76
|
+
data={"output_format": output_format},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if resp.status_code == 400:
|
|
80
|
+
detail = resp.json().get("detail", "Bad request")
|
|
81
|
+
return {"error": detail}
|
|
82
|
+
if resp.status_code == 402:
|
|
83
|
+
return {"error": "Insufficient credits. Buy more at https://www.vidcontext.com"}
|
|
84
|
+
if resp.status_code == 413:
|
|
85
|
+
return {"error": "Video file too large (max 500MB for paid users)."}
|
|
86
|
+
if resp.status_code == 429:
|
|
87
|
+
return {"error": "Rate limited. Wait a moment and try again."}
|
|
88
|
+
if resp.status_code == 503:
|
|
89
|
+
return {"error": "System is busy. Try again in a few minutes."}
|
|
90
|
+
|
|
91
|
+
resp.raise_for_status()
|
|
92
|
+
return resp.json()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def _poll_job(job_id: str) -> dict:
|
|
96
|
+
"""Poll job status until complete, failed, or timeout."""
|
|
97
|
+
elapsed = 0
|
|
98
|
+
interval = 3
|
|
99
|
+
|
|
100
|
+
while elapsed < MAX_POLL_SECONDS:
|
|
101
|
+
await asyncio.sleep(interval)
|
|
102
|
+
elapsed += interval
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
result = await _api_get(f"/v1/status/{job_id}")
|
|
106
|
+
except httpx.HTTPStatusError as exc:
|
|
107
|
+
# Fail fast on auth errors — don't poll for 10 minutes with a bad key
|
|
108
|
+
if exc.response.status_code in (401, 403):
|
|
109
|
+
return {"status": "failed", "error": "Authentication failed. Check your API key."}
|
|
110
|
+
# Retry on server errors (5xx)
|
|
111
|
+
if exc.response.status_code >= 500:
|
|
112
|
+
continue
|
|
113
|
+
return {"status": "failed", "error": f"HTTP {exc.response.status_code}"}
|
|
114
|
+
except httpx.HTTPError:
|
|
115
|
+
# Retry on network/timeout errors
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
if result.get("status") in ("complete", "failed"):
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
# Gradually back off: 3s → 5s → 7s → 10s max
|
|
122
|
+
interval = min(interval + 2, 10)
|
|
123
|
+
|
|
124
|
+
return {"status": "timeout", "job_id": job_id}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async def _download_url(url: str) -> str:
|
|
128
|
+
"""Download a video from a URL to a temporary file using streaming."""
|
|
129
|
+
url_path = Path(url.split("?")[0])
|
|
130
|
+
ext = url_path.suffix.lower() if url_path.suffix else ".mp4"
|
|
131
|
+
if ext not in SUPPORTED_EXTENSIONS:
|
|
132
|
+
ext = ".mp4"
|
|
133
|
+
|
|
134
|
+
tmp = tempfile.NamedTemporaryFile(suffix=ext, delete=False)
|
|
135
|
+
try:
|
|
136
|
+
async with httpx.AsyncClient(
|
|
137
|
+
timeout=httpx.Timeout(10.0, read=120.0),
|
|
138
|
+
follow_redirects=True,
|
|
139
|
+
) as client:
|
|
140
|
+
async with client.stream("GET", url) as resp:
|
|
141
|
+
resp.raise_for_status()
|
|
142
|
+
|
|
143
|
+
# Check Content-Length if available — reject oversized files early
|
|
144
|
+
content_length = resp.headers.get("content-length")
|
|
145
|
+
if content_length and int(content_length) > MAX_FILE_BYTES:
|
|
146
|
+
raise ValueError(f"Remote file too large ({int(content_length) // (1024 * 1024)}MB). Max 500MB.")
|
|
147
|
+
|
|
148
|
+
downloaded = 0
|
|
149
|
+
async for chunk in resp.aiter_bytes(chunk_size=65536):
|
|
150
|
+
downloaded += len(chunk)
|
|
151
|
+
if downloaded > MAX_FILE_BYTES:
|
|
152
|
+
raise ValueError("Remote file exceeds 500MB limit.")
|
|
153
|
+
tmp.write(chunk)
|
|
154
|
+
|
|
155
|
+
tmp.close()
|
|
156
|
+
return tmp.name
|
|
157
|
+
except Exception:
|
|
158
|
+
tmp.close()
|
|
159
|
+
Path(tmp.name).unlink(missing_ok=True)
|
|
160
|
+
raise
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@mcp.tool()
|
|
164
|
+
async def analyze_video(
|
|
165
|
+
file_path: str,
|
|
166
|
+
output_format: str = "context",
|
|
167
|
+
) -> str:
|
|
168
|
+
"""Analyze a video and get a detailed text description or expert analysis.
|
|
169
|
+
|
|
170
|
+
This tool runs LOCALLY and can read any file on the user's computer.
|
|
171
|
+
Ask the user for the full file path (e.g. ~/Downloads/video.mp4) or a URL.
|
|
172
|
+
Do NOT ask the user to upload files into chat — ask for the file path instead.
|
|
173
|
+
Do NOT tell the user the server can't access local files — it can.
|
|
174
|
+
|
|
175
|
+
Returns either a detailed scene-by-scene description (context mode) or a
|
|
176
|
+
comprehensive expert analysis scored across 7 proprietary frameworks (analysis mode).
|
|
177
|
+
|
|
178
|
+
IMPORTANT: Always present the COMPLETE returned output to the user without
|
|
179
|
+
summarizing or truncating. The full analysis is what they are paying for.
|
|
180
|
+
|
|
181
|
+
Processing typically takes 30-180 seconds depending on video length.
|
|
182
|
+
Cost: 1 credit per minute of video (rounded up).
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
file_path: Absolute path to a local video file (mp4, mov, avi, mkv, webm) or a URL to a video
|
|
186
|
+
output_format: "context" for detailed description, "analysis" for expert 7-framework scoring
|
|
187
|
+
"""
|
|
188
|
+
if output_format not in ("context", "analysis"):
|
|
189
|
+
return "Error: output_format must be 'context' or 'analysis'"
|
|
190
|
+
|
|
191
|
+
temp_file = None
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
# Handle URL input — stream download to temp file first
|
|
195
|
+
if file_path.startswith(("http://", "https://")):
|
|
196
|
+
try:
|
|
197
|
+
temp_file = await _download_url(file_path)
|
|
198
|
+
except httpx.HTTPError as exc:
|
|
199
|
+
return f"Error downloading video: {exc}"
|
|
200
|
+
except ValueError as exc:
|
|
201
|
+
return f"Error: {exc}"
|
|
202
|
+
upload_path = temp_file
|
|
203
|
+
else:
|
|
204
|
+
# Resolve symlinks and validate path
|
|
205
|
+
path = Path(file_path).resolve()
|
|
206
|
+
if not path.exists():
|
|
207
|
+
return f"Error: File not found: {file_path}"
|
|
208
|
+
if not path.is_file():
|
|
209
|
+
return f"Error: Not a regular file: {file_path}"
|
|
210
|
+
if path.suffix.lower() not in SUPPORTED_EXTENSIONS:
|
|
211
|
+
return (
|
|
212
|
+
f"Error: Unsupported format '{path.suffix}'. "
|
|
213
|
+
f"Supported: {', '.join(sorted(SUPPORTED_EXTENSIONS))}"
|
|
214
|
+
)
|
|
215
|
+
upload_path = str(path)
|
|
216
|
+
|
|
217
|
+
# Upload to VidContext API
|
|
218
|
+
try:
|
|
219
|
+
result = await _upload_video(upload_path, output_format)
|
|
220
|
+
except httpx.HTTPStatusError as exc:
|
|
221
|
+
return f"Error: Upload failed (HTTP {exc.response.status_code})"
|
|
222
|
+
except httpx.HTTPError as exc:
|
|
223
|
+
return f"Error: Upload failed — network error: {exc}"
|
|
224
|
+
|
|
225
|
+
if "error" in result:
|
|
226
|
+
return f"Error: {result['error']}"
|
|
227
|
+
|
|
228
|
+
job_id = result["job_id"]
|
|
229
|
+
|
|
230
|
+
# Poll until complete
|
|
231
|
+
final = await _poll_job(job_id)
|
|
232
|
+
|
|
233
|
+
if final["status"] == "complete":
|
|
234
|
+
text = final.get("result", "")
|
|
235
|
+
time_s = final.get("processing_time_seconds")
|
|
236
|
+
header = "[FULL OUTPUT — present this complete text to the user, do not summarize]\n\n"
|
|
237
|
+
footer = "\n\n---\n"
|
|
238
|
+
if time_s:
|
|
239
|
+
footer += f"Processed in {time_s:.1f} seconds. "
|
|
240
|
+
footer += "1 credit per minute of video (rounded up)."
|
|
241
|
+
return f"{header}{text}{footer}"
|
|
242
|
+
|
|
243
|
+
if final["status"] == "failed":
|
|
244
|
+
return f"Analysis failed: {final.get('error', 'Unknown error')}"
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
f"Still processing after {MAX_POLL_SECONDS}s. "
|
|
248
|
+
f"Check later with: check_job_status('{job_id}')"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
finally:
|
|
252
|
+
if temp_file:
|
|
253
|
+
Path(temp_file).unlink(missing_ok=True)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@mcp.tool()
|
|
257
|
+
async def check_job_status(job_id: str) -> str:
|
|
258
|
+
"""Check the status of a video analysis job.
|
|
259
|
+
|
|
260
|
+
Use this to check on a job that's still processing, or to retrieve
|
|
261
|
+
results from a previous analysis.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
job_id: The job ID returned from analyze_video
|
|
265
|
+
"""
|
|
266
|
+
try:
|
|
267
|
+
result = await _api_get(f"/v1/status/{job_id}")
|
|
268
|
+
except httpx.HTTPStatusError as exc:
|
|
269
|
+
if exc.response.status_code == 404:
|
|
270
|
+
return f"Job '{job_id}' not found. Jobs expire after 24 hours."
|
|
271
|
+
return f"Error: HTTP {exc.response.status_code}"
|
|
272
|
+
except httpx.HTTPError as exc:
|
|
273
|
+
return f"Error: Network error — {exc}"
|
|
274
|
+
except ValueError as exc:
|
|
275
|
+
return str(exc)
|
|
276
|
+
|
|
277
|
+
status = result.get("status", "unknown")
|
|
278
|
+
|
|
279
|
+
if status == "complete":
|
|
280
|
+
text = result.get("result", "No output.")
|
|
281
|
+
time_s = result.get("processing_time_seconds")
|
|
282
|
+
fmt = result.get("output_format", "unknown")
|
|
283
|
+
header = f"Status: Complete ({fmt} mode)"
|
|
284
|
+
if time_s:
|
|
285
|
+
header += f" | {time_s:.1f}s"
|
|
286
|
+
return f"{header}\n\n{text}"
|
|
287
|
+
|
|
288
|
+
if status == "failed":
|
|
289
|
+
return f"Status: Failed\nError: {result.get('error', 'Unknown error')}"
|
|
290
|
+
|
|
291
|
+
if status == "processing":
|
|
292
|
+
return "Status: Processing. Check again in 15-30 seconds."
|
|
293
|
+
|
|
294
|
+
if status == "queued":
|
|
295
|
+
return "Status: Queued. Waiting to start processing."
|
|
296
|
+
|
|
297
|
+
return f"Status: {status}"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@mcp.tool()
|
|
301
|
+
async def check_credits() -> str:
|
|
302
|
+
"""Check your VidContext credit balance and usage limits.
|
|
303
|
+
|
|
304
|
+
Shows current credits, tier, file size limits, and duration limits.
|
|
305
|
+
"""
|
|
306
|
+
try:
|
|
307
|
+
usage = await _api_get("/v1/usage")
|
|
308
|
+
except httpx.HTTPStatusError as exc:
|
|
309
|
+
return f"Error: HTTP {exc.response.status_code}"
|
|
310
|
+
except httpx.HTTPError as exc:
|
|
311
|
+
return f"Error: Network error — {exc}"
|
|
312
|
+
except ValueError as exc:
|
|
313
|
+
return str(exc)
|
|
314
|
+
|
|
315
|
+
tier = usage.get("tier", "unknown")
|
|
316
|
+
credits = usage.get("credits", 0)
|
|
317
|
+
|
|
318
|
+
if tier == "credit":
|
|
319
|
+
return (
|
|
320
|
+
f"Credits: {credits}\n"
|
|
321
|
+
f"Tier: Paid\n"
|
|
322
|
+
f"Max file size: {usage.get('max_file_size_mb', 500)}MB\n"
|
|
323
|
+
f"Max duration: {usage.get('max_duration_seconds', 900) // 60} minutes\n"
|
|
324
|
+
f"Cost: 1 credit per minute of video (rounded up)"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
remaining = usage.get("remaining", 0)
|
|
328
|
+
return (
|
|
329
|
+
f"Free uses remaining: {remaining} of {usage.get('limit', 5)}\n"
|
|
330
|
+
f"Max file size: {usage.get('max_file_size_mb', 100)}MB\n"
|
|
331
|
+
f"Max duration: {usage.get('max_duration_seconds', 60)}s\n"
|
|
332
|
+
f"Get more credits at https://www.vidcontext.com"
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@mcp.tool()
|
|
337
|
+
async def get_account() -> str:
|
|
338
|
+
"""Get your VidContext account information.
|
|
339
|
+
|
|
340
|
+
Shows email, credits, tier, and active subscription details.
|
|
341
|
+
"""
|
|
342
|
+
try:
|
|
343
|
+
me = await _api_get("/v1/me")
|
|
344
|
+
except httpx.HTTPStatusError as exc:
|
|
345
|
+
if exc.response.status_code == 401:
|
|
346
|
+
return "Invalid API key. Check your VIDCONTEXT_API_KEY."
|
|
347
|
+
return f"Error: HTTP {exc.response.status_code}"
|
|
348
|
+
except httpx.HTTPError as exc:
|
|
349
|
+
return f"Error: Network error — {exc}"
|
|
350
|
+
except ValueError as exc:
|
|
351
|
+
return str(exc)
|
|
352
|
+
|
|
353
|
+
if not me.get("authenticated"):
|
|
354
|
+
return "Not authenticated. Set VIDCONTEXT_API_KEY in your MCP config."
|
|
355
|
+
|
|
356
|
+
lines = [
|
|
357
|
+
f"Email: {me.get('email', 'N/A')}",
|
|
358
|
+
f"Name: {me.get('display_name', 'N/A')}",
|
|
359
|
+
f"Credits: {me.get('credits', 0)}",
|
|
360
|
+
f"Tier: {me.get('tier', 'N/A')}",
|
|
361
|
+
]
|
|
362
|
+
|
|
363
|
+
sub = me.get("subscription")
|
|
364
|
+
if sub:
|
|
365
|
+
lines.extend([
|
|
366
|
+
f"\nSubscription: {sub.get('plan', 'N/A').title()} ({sub.get('billing_interval', 'N/A')})",
|
|
367
|
+
f"Credits/period: {sub.get('credits_per_period', 'N/A')}",
|
|
368
|
+
f"Status: {sub.get('status', 'N/A')}",
|
|
369
|
+
f"Renews: {sub.get('current_period_end', 'N/A')}",
|
|
370
|
+
])
|
|
371
|
+
else:
|
|
372
|
+
lines.append("\nNo active subscription")
|
|
373
|
+
|
|
374
|
+
return "\n".join(lines)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def main():
|
|
378
|
+
"""Entry point for the VidContext MCP server."""
|
|
379
|
+
mcp.run(transport="stdio")
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
if __name__ == "__main__":
|
|
383
|
+
main()
|