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,4 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
@@ -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,2 @@
1
+ # VidContext MCP Server — Give your AI agent eyes.
2
+ __version__ = "0.1.0"
@@ -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()