framefetch-mcp 0.2.0

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.
Files changed (4) hide show
  1. package/README.md +13 -0
  2. package/mcp-tools.json +92 -0
  3. package/mcp.js +109 -0
  4. package/package.json +14 -0
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # framefetch-mcp
2
+
3
+ Local **stdio MCP bridge** for [FrameFetch](https://framefetch.net) — any social-video URL → metadata, insights, Whisper transcript, and parametric frames across YouTube/Shorts, TikTok, Reddit, Instagram, Pinterest.
4
+
5
+ ```bash
6
+ npx framefetch-mcp
7
+ ```
8
+
9
+ ```json
10
+ { "mcpServers": { "framefetch": { "command": "npx", "args": ["-y", "framefetch-mcp"], "env": { "FRAMEFETCH_API_KEY": "<key>" } } } }
11
+ ```
12
+
13
+ `tools/list` works with no key; tool calls use `FRAMEFETCH_API_KEY` (or x402). Override with `FRAMEFETCH_MCP_URL`. Proxies to `framefetch.net/mcp`. Full client: [framefetch-client](https://github.com/MarvinRey7879/framefetch-client).
package/mcp-tools.json ADDED
@@ -0,0 +1,92 @@
1
+ [
2
+ {
3
+ "name": "framefetch_extract",
4
+ "description": "Extract structured data from ONE public social-video URL (YouTube incl. Shorts, TikTok, Instagram Reels, Pinterest, Reddit). Purpose: turn a video link into metadata (title, author, duration, date), insights (views/likes/comments), a transcript (captions, or Whisper when there are none — works on TikTok/Reddit too), and/or parametrically-sampled video frames. When to use: you have a video URL and need its text, stats, or frames for analysis, summarization, or grounding a model. When NOT to use: non-video pages, private/login-walled content, or bulk crawling (one URL per call). Returns: one JSON object containing only the requested fields plus a `cost` block (micro-USD). Frames come back as time-limited signed image URLs. Cost/latency: metadata is sub-cent and fast; transcript is billed per audio-minute and frames per frame (both also incur bandwidth) — request only the fields you need and downscale frames via `width` to control cost. Billing: a free tier covers light use; agents can also pay per call with x402 (USDC) with no account. Example: { \"url\": \"https://www.youtube.com/watch?v=...\", \"fields\": [\"metadata\",\"transcript\"], \"frames\": { \"mode\": \"fps\", \"fps\": 1, \"width\": 480 } }",
5
+ "inputSchema": {
6
+ "type": "object",
7
+ "properties": {
8
+ "url": {
9
+ "type": "string",
10
+ "description": "Public video URL from a supported platform."
11
+ },
12
+ "fields": {
13
+ "type": "array",
14
+ "items": {
15
+ "type": "string",
16
+ "enum": [
17
+ "metadata",
18
+ "insights",
19
+ "transcript",
20
+ "frames"
21
+ ]
22
+ },
23
+ "description": "Which data to return. Default [\"metadata\"]. Request the minimum you need."
24
+ },
25
+ "frames": {
26
+ "type": "object",
27
+ "description": "Frame-sampling spec; required when \"frames\" is requested.",
28
+ "properties": {
29
+ "mode": {
30
+ "type": "string",
31
+ "enum": [
32
+ "all",
33
+ "every_n",
34
+ "fps",
35
+ "range"
36
+ ]
37
+ },
38
+ "n": {
39
+ "type": "number",
40
+ "description": "every_n: take every Nth source frame"
41
+ },
42
+ "fps": {
43
+ "type": "number",
44
+ "description": "fps/range: frames per second (max 60)"
45
+ },
46
+ "from": {
47
+ "type": "number",
48
+ "description": "range: start second"
49
+ },
50
+ "to": {
51
+ "type": "number",
52
+ "description": "range: end second"
53
+ },
54
+ "format": {
55
+ "type": "string",
56
+ "enum": [
57
+ "jpg",
58
+ "png",
59
+ "webp"
60
+ ]
61
+ },
62
+ "width": {
63
+ "type": "number",
64
+ "description": "downscale width in px (16–7680)"
65
+ }
66
+ },
67
+ "required": [
68
+ "mode"
69
+ ]
70
+ },
71
+ "verbosity": {
72
+ "type": "string",
73
+ "enum": [
74
+ "concise",
75
+ "detailed"
76
+ ]
77
+ }
78
+ },
79
+ "required": [
80
+ "url"
81
+ ]
82
+ }
83
+ },
84
+ {
85
+ "name": "framefetch_platform_capabilities",
86
+ "description": "Return a JSON matrix of which data types (metadata, insights, transcript, frames) each supported platform provides — YouTube, YouTube Shorts, TikTok, Instagram Reels, Pinterest, Reddit. Purpose: check what is available for a platform BEFORE calling framefetch_extract, so you only request supported fields. No input required.",
87
+ "inputSchema": {
88
+ "type": "object",
89
+ "properties": {}
90
+ }
91
+ }
92
+ ]
package/mcp.js ADDED
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ // framefetch — zero-dependency stdio MCP bridge.
3
+ // Proxies Model Context Protocol calls to the hosted FrameFetch MCP server.
4
+ // tools/list is served from an embedded snapshot (mcp-tools.json) so introspection
5
+ // always succeeds, even before the first network round-trip; tool calls are
6
+ // forwarded to the remote endpoint.
7
+ //
8
+ // Env:
9
+ // FRAMEFETCH_MCP_URL override the remote MCP endpoint (default https://framefetch.net/mcp)
10
+ // FRAMEFETCH_API_KEY optional bearer key for authenticated (non-x402) calls
11
+ import { stdin, stdout, env, exit } from "node:process";
12
+ import { readFileSync } from "node:fs";
13
+
14
+ const REMOTE = env.FRAMEFETCH_MCP_URL || "https://framefetch.net/mcp";
15
+ const API_KEY = env.FRAMEFETCH_API_KEY || "";
16
+ const PROTOCOL = "2025-06-18";
17
+ const SERVER_INFO = { name: "framefetch", version: "1.0.0" };
18
+
19
+ let STATIC_TOOLS = [];
20
+ try {
21
+ STATIC_TOOLS = JSON.parse(readFileSync(new URL("./mcp-tools.json", import.meta.url), "utf8"));
22
+ } catch { /* snapshot optional; remote tools/list still works */ }
23
+
24
+ stdout.on("error", () => {}); // ignore EPIPE if the client closes the pipe early
25
+
26
+ function send(msg) {
27
+ stdout.write(JSON.stringify(msg) + "\n");
28
+ }
29
+
30
+ // Remote may answer with plain JSON or an SSE frame ("event: message\ndata: {...}").
31
+ function parseRpc(text) {
32
+ const s = text.trim();
33
+ if (s.startsWith("{")) {
34
+ try { return JSON.parse(s); } catch { /* fall through to SSE parse */ }
35
+ }
36
+ for (const line of s.split(/\r?\n/)) {
37
+ const m = line.match(/^data:\s*(.*)$/);
38
+ if (m) {
39
+ try { return JSON.parse(m[1]); } catch { /* ignore non-JSON data lines */ }
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+
45
+ async function remoteRpc(method, params) {
46
+ const res = await fetch(REMOTE, {
47
+ method: "POST",
48
+ headers: {
49
+ "content-type": "application/json",
50
+ "accept": "application/json, text/event-stream",
51
+ ...(API_KEY ? { authorization: `Bearer ${API_KEY}` } : {}),
52
+ },
53
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
54
+ });
55
+ const j = parseRpc(await res.text());
56
+ if (!j) throw new Error("invalid response from remote MCP");
57
+ if (j.error) throw new Error(j.error.message || "remote MCP error");
58
+ return j.result;
59
+ }
60
+
61
+ async function listTools() {
62
+ try {
63
+ const r = await remoteRpc("tools/list", {});
64
+ if (r && Array.isArray(r.tools) && r.tools.length) return r.tools;
65
+ } catch { /* remote unreachable — fall back to embedded snapshot */ }
66
+ return STATIC_TOOLS;
67
+ }
68
+
69
+ async function handle(msg) {
70
+ const { id, method, params } = msg;
71
+ if (method === "initialize")
72
+ return send({ jsonrpc: "2.0", id, result: { protocolVersion: PROTOCOL, capabilities: { tools: {} }, serverInfo: SERVER_INFO } });
73
+ if (method === "ping")
74
+ return send({ jsonrpc: "2.0", id, result: {} });
75
+ if (typeof method === "string" && method.startsWith("notifications/"))
76
+ return; // notifications carry no id and need no reply
77
+ if (method === "tools/list")
78
+ return send({ jsonrpc: "2.0", id, result: { tools: await listTools() } });
79
+ if (method === "tools/call") {
80
+ try {
81
+ return send({ jsonrpc: "2.0", id, result: await remoteRpc("tools/call", params) });
82
+ } catch (e) {
83
+ return send({ jsonrpc: "2.0", id, result: { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true } });
84
+ }
85
+ }
86
+ if (id !== undefined)
87
+ return send({ jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } });
88
+ }
89
+
90
+ let buf = "";
91
+ let pending = 0;
92
+ let ended = false;
93
+ const tryExit = () => { if (ended && pending === 0) setImmediate(() => exit(0)); };
94
+ stdin.setEncoding("utf8");
95
+ stdin.on("data", (chunk) => {
96
+ buf += chunk;
97
+ let i;
98
+ while ((i = buf.indexOf("\n")) >= 0) {
99
+ const line = buf.slice(0, i).trim();
100
+ buf = buf.slice(i + 1);
101
+ if (!line) continue;
102
+ let msg;
103
+ try { msg = JSON.parse(line); } catch { continue; }
104
+ pending++;
105
+ Promise.resolve(handle(msg)).catch(() => {}).finally(() => { pending--; tryExit(); });
106
+ }
107
+ });
108
+ // Exit only once stdin is closed AND every in-flight response has been flushed.
109
+ stdin.on("end", () => { ended = true; tryExit(); });
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "framefetch-mcp",
3
+ "version": "0.2.0",
4
+ "description": "Local stdio MCP bridge for FrameFetch — any social-video URL to metadata, transcript, insights & parametric frames across YouTube/Shorts, TikTok, Reddit, Instagram, Pinterest. Proxies to the hosted FrameFetch MCP server.",
5
+ "type": "module",
6
+ "bin": { "framefetch-mcp": "mcp.js" },
7
+ "files": ["mcp.js", "mcp-tools.json", "README.md"],
8
+ "engines": { "node": ">=18" },
9
+ "keywords": ["framefetch", "mcp", "video", "transcript", "youtube", "tiktok", "ai-agent", "stdio", "x402", "whisper"],
10
+ "homepage": "https://framefetch.net",
11
+ "repository": { "type": "git", "url": "git+https://github.com/MarvinRey7879/framefetch-client.git" },
12
+ "license": "MIT",
13
+ "author": "FrameFetch"
14
+ }