@mambalabsdev/mcp-youtube-channel-transcript-extractor 1.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mamba Labs
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.
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # YouTube Channel Stats Extractor MCP Server
2
+
3
+ [![Smithery](https://smithery.ai/badge/mambabuilt/mcp-youtube-channel-transcript-extractor)](https://smithery.ai/servers/mambabuilt/mcp-youtube-channel-transcript-extractor) [![Glama score](https://glama.ai/mcp/servers/mambalabsdev/mcp-youtube-channel-transcript-extractor/badges/score.svg)](https://glama.ai/mcp/servers/mambalabsdev/mcp-youtube-channel-transcript-extractor) [![npm version](https://img.shields.io/npm/v/@mambalabsdev/mcp-youtube-channel-transcript-extractor)](https://www.npmjs.com/package/@mambalabsdev/mcp-youtube-channel-transcript-extractor) [![npm downloads](https://img.shields.io/npm/dm/@mambalabsdev/mcp-youtube-channel-transcript-extractor)](https://www.npmjs.com/package/@mambalabsdev/mcp-youtube-channel-transcript-extractor) [![license](https://img.shields.io/github/license/mambalabsdev/mcp-youtube-channel-transcript-extractor)](https://github.com/mambalabsdev/mcp-youtube-channel-transcript-extractor/blob/main/LICENSE)
4
+
5
+ MCP server for the Mamba Labs **YouTube Channel Stats Extractor** actor on Apify.
6
+
7
+ Resolve a company domain or YouTube handle to the channel with subscriber count and channel metadata.
8
+
9
+ ## What it does
10
+
11
+ Resolve a company domain, or a YouTube handle, to that company's YouTube channel. Returns the subscriber count, the stable UC channel id, channel name, creation date and description, as one flat Clay ready row. Subscriber counts are rounded by YouTube above roughly a thousand subscribers, on the page and in its official API alike, and the row flags that. Optional transcript pulling is available and is CURRENTLY GATED by YouTube: the caption track list is readable and the caption content endpoint returns an empty body without a proof of origin token, which the row reports as not_extractable rather than as a video having no captions. No API key needed. Read only; requires an APIFY_TOKEN and consumes Apify credits per call.
12
+
13
+ ## Quick start
14
+
15
+ Add this to your MCP client configuration:
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "mamba-youtube-channel-stats": {
21
+ "command": "npx",
22
+ "args": ["-y", "@mambalabsdev/mcp-youtube-channel-transcript-extractor"],
23
+ "env": { "APIFY_TOKEN": "your-apify-token" }
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ ## Prerequisites
30
+
31
+ - Node.js 18 or newer
32
+ - An Apify API token from [console.apify.com/account/integrations](https://console.apify.com/account/integrations)
33
+
34
+ The actor is pay per event and consumes Apify credits per call. Pricing is on the
35
+ [actor page](https://apify.com/mambalabs/youtube-channel-transcript-extractor).
36
+
37
+ ## Example prompts
38
+
39
+ - "How many YouTube subscribers does stripe.com have?"
40
+ - "Find the YouTube channel id for hubspot.com."
41
+ - "Does gitlab.com run a YouTube channel, and when was it created?"
42
+
43
+ ## Tool and inputs
44
+
45
+ Tool: `get_youtube_channel_stats`
46
+
47
+ | Input | Type | Meaning |
48
+ |---|---|---|
49
+ | `company_domain` | string | Bare company domain, for example shopify.com. Supply this or a handle. With a domain the actor runs full discovery; with a handle it skips straight to |
50
+ | `company_name` | string | Optional. Improves search accuracy and is what the identity gate checks a discovered profile against, so supplying it reduces wrong matches. |
51
+ | `handle` | string | Optional. A YouTube handle such as @shopify, or a full channel URL. Supplying it skips discovery and goes straight to the channel. |
52
+ | `includeFollowerCounts` | boolean | When "true" (default) the profile page is fetched and the counts are extracted. Set "false" to resolve the profile URL only, which is cheaper and need |
53
+ | `skipCache` | boolean | When "false" (default) a successful lookup is cached for seven days and reused. Set "true" to force a fresh fetch. Sent as a string for Clay compatibi |
54
+ | `includeTranscripts` | boolean | When "true", full timestamped transcripts are pulled for the most recent videos on the channel and returned as one row per video. Default is "false" b |
55
+ | `transcriptVideoCount` | string | How many of the channel's most recent videos to pull transcripts for, when transcripts are on. Default 5. Each video is one extra fetch and one extra |
56
+
57
+ ## Reading the output
58
+
59
+ Every row carries a per platform `_status` field, and it is the field to read
60
+ first. The vocabulary is the same across the whole Mamba Labs social family:
61
+
62
+ | Status | Meaning |
63
+ |---|---|
64
+ | `ok` | fetched and parsed, the value is there |
65
+ | `not_found` | we looked and there is no such profile |
66
+ | `not_extractable` | the profile exists and the value is not on the wire to us |
67
+ | `blocked` | the platform refused us, worth retrying later |
68
+ | `identity_mismatch` | we found a real profile and it belongs to someone else |
69
+ | `skipped` | you did not ask for this platform |
70
+
71
+ **`false` and `null` are never interchangeable.** `false` means we looked and the
72
+ answer is no. `null` means we could not look. If you filter for companies with no
73
+ presence, filter on `false`, because `null` rows are unknown rather than absent.
74
+
75
+ ## Full actor documentation
76
+
77
+ [apify.com/mambalabs/youtube-channel-transcript-extractor](https://apify.com/mambalabs/youtube-channel-transcript-extractor)
78
+
79
+ ## Mamba Labs GTM Suite
80
+
81
+ Mamba Labs builds a fleet of GTM enrichment actors that share one flat, Clay
82
+ ready output convention, so their rows join on `company_domain` with no cleaning
83
+ step. Full fleet: [apify.com/mambalabs](https://apify.com/mambalabs)
84
+
85
+ ## License
86
+
87
+ MIT
package/SECURITY.md ADDED
@@ -0,0 +1,31 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ The latest published `1.x` release of
6
+ `@mambalabsdev/mcp-company-social-presence-mapper` receives security updates.
7
+
8
+ | Version | Supported |
9
+ | ------- | ------------------ |
10
+ | 1.x | :white_check_mark: |
11
+ | < 1.0 | :x: |
12
+
13
+ ## Reporting a vulnerability
14
+
15
+ Please do not open a public GitHub issue for security reports.
16
+
17
+ Email security reports to **labs@mambamindsetgrowth.com** with:
18
+
19
+ - a description of the issue and its impact,
20
+ - steps to reproduce, and
21
+ - any relevant logs or proof of concept.
22
+
23
+ We aim to acknowledge reports within 3 business days and to provide a remediation
24
+ timeline after triage. Coordinated disclosure is appreciated.
25
+
26
+ ## Handling of credentials
27
+
28
+ This server reads an `APIFY_TOKEN` from the environment and uses it only to call
29
+ the Apify API on your behalf. The token is never logged or written to disk by this
30
+ package. Treat your `APIFY_TOKEN` as a secret and rotate it at
31
+ https://console.apify.com/account/integrations if you believe it has been exposed.
package/build/index.js ADDED
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { dirname, join } from "node:path";
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { z } from "zod";
8
+ const here = dirname(fileURLToPath(import.meta.url));
9
+ const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"));
10
+ // Distinctive UA so Apify run meta.userAgent marks MCP-originated runs.
11
+ const USER_AGENT = `mambalabs-mcp ${pkg.name}@${pkg.version}`;
12
+ const APIFY_TOKEN = process.env.APIFY_TOKEN;
13
+ // Drop undefined values so optional inputs are not sent to the actor at all.
14
+ function compact(obj) {
15
+ const out = {};
16
+ for (const [k, v] of Object.entries(obj)) {
17
+ if (v !== undefined)
18
+ out[k] = v;
19
+ }
20
+ return out;
21
+ }
22
+ // The actor types its switches as strings ("true"/"false") for Clay
23
+ // compatibility, because Clay sends every input as a string and a boolean typed
24
+ // field silently receives "false" and reads it as truthy. The model gets a real
25
+ // boolean and the actor gets the string it validates.
26
+ function boolToString(v) {
27
+ return v === undefined ? undefined : v ? "true" : "false";
28
+ }
29
+ // actorPath is the actor's IMMUTABLE Apify actor id, not its slug, so a Store
30
+ // rename never breaks these calls.
31
+ async function runActor(actorPath, actorLabel, input) {
32
+ if (!APIFY_TOKEN) {
33
+ return { isError: true, content: [{ type: "text", text: "APIFY_TOKEN is not set. Create a token at https://console.apify.com/account/integrations and set it as the APIFY_TOKEN environment variable." }] };
34
+ }
35
+ // memory=1024 is deliberate and matches the actor's declared
36
+ // defaultRunOptions.memoryMbytes. run-sync-get-dataset-items runs at 2048 MB
37
+ // unless told otherwise, and `apify-actor-start` bills once per GB with a
38
+ // minimum of one, so leaving the default in place would charge the caller
39
+ // more start events per run than the actor asks for. Keep this in step with
40
+ // the actor's defaultRunOptions.
41
+ const url = `https://api.apify.com/v2/acts/${actorPath}/run-sync-get-dataset-items?timeout=300&memory=1024`;
42
+ let response;
43
+ try {
44
+ response = await fetch(url, {
45
+ method: "POST",
46
+ headers: {
47
+ Authorization: `Bearer ${APIFY_TOKEN}`,
48
+ "Content-Type": "application/json",
49
+ "User-Agent": USER_AGENT,
50
+ },
51
+ body: JSON.stringify(input),
52
+ });
53
+ }
54
+ catch (err) {
55
+ const message = err instanceof Error ? err.message : String(err);
56
+ return { isError: true, content: [{ type: "text", text: `Could not reach the Apify API: ${message}` }] };
57
+ }
58
+ if (!response.ok) {
59
+ let detail = "";
60
+ try {
61
+ const body = (await response.json());
62
+ if (body?.error?.message)
63
+ detail = ` ${body.error.message}`;
64
+ }
65
+ catch {
66
+ detail = "";
67
+ }
68
+ let message;
69
+ switch (response.status) {
70
+ case 401:
71
+ message = "Invalid Apify token. Check your APIFY_TOKEN environment variable.";
72
+ break;
73
+ case 402:
74
+ message = "Insufficient Apify credits. Check your account balance at https://console.apify.com/billing";
75
+ break;
76
+ case 408:
77
+ message = `The ${actorLabel} run timed out after 300 seconds. Try again, or run the actor on Apify directly for longer jobs.`;
78
+ break;
79
+ default:
80
+ message = `Apify request to ${actorLabel} failed with status ${response.status}.${detail}`;
81
+ }
82
+ return { isError: true, content: [{ type: "text", text: message }] };
83
+ }
84
+ // A 2xx normally carries the dataset array. Pass actor output through
85
+ // unchanged: the wrapper must never reinterpret a status field, because
86
+ // not_extractable, blocked and not_found are different answers and collapsing
87
+ // them is exactly the defect the actor was built to avoid.
88
+ const items = await response.json();
89
+ return { content: [{ type: "text", text: JSON.stringify(items, null, 2) }] };
90
+ }
91
+ const server = new McpServer({
92
+ name: "mamba-youtube-channel-stats",
93
+ version: pkg.version,
94
+ });
95
+ // YouTube Channel Stats Extractor (immutable actor ID 6hSpD3kd2s2IQ4VSs)
96
+ server.registerTool("get_youtube_channel_stats", {
97
+ title: "Get YouTube Channel Stats",
98
+ description: "Resolve a company domain, or a YouTube handle, to that company's YouTube channel. Returns the subscriber count, the stable UC channel id, channel name, creation date and description, as one flat Clay ready row. Subscriber counts are rounded by YouTube above roughly a thousand subscribers, on the page and in its official API alike, and the row flags that. Optional transcript pulling is available and is CURRENTLY GATED by YouTube: the caption track list is readable and the caption content endpoint returns an empty body without a proof of origin token, which the row reports as not_extractable rather than as a video having no captions. No API key needed. Read only; requires an APIFY_TOKEN and consumes Apify credits per call.",
99
+ annotations: {
100
+ title: "Get YouTube Channel Stats",
101
+ readOnlyHint: true,
102
+ destructiveHint: false,
103
+ idempotentHint: true,
104
+ openWorldHint: true,
105
+ },
106
+ inputSchema: {
107
+ company_domain: z.string()
108
+ .optional()
109
+ .describe("Bare company domain, for example shopify.com. Supply this or a handle. With a domain the actor runs full discovery; with a handle it skips straight to the fetch."),
110
+ company_name: z.string()
111
+ .optional()
112
+ .describe("Optional. Improves search accuracy and is what the identity gate checks a discovered profile against, so supplying it reduces wrong matches."),
113
+ handle: z.string()
114
+ .optional()
115
+ .describe("Optional. A YouTube handle such as @shopify, or a full channel URL. Supplying it skips discovery and goes straight to the channel."),
116
+ includeFollowerCounts: z.boolean()
117
+ .optional()
118
+ .describe("When \"true\" (default) the profile page is fetched and the counts are extracted. Set \"false\" to resolve the profile URL only, which is cheaper and needs no proxy. Sent as a string for Clay compatibility."),
119
+ skipCache: z.boolean()
120
+ .optional()
121
+ .describe("When \"false\" (default) a successful lookup is cached for seven days and reused. Set \"true\" to force a fresh fetch. Sent as a string for Clay compatibility."),
122
+ includeTranscripts: z.boolean()
123
+ .optional()
124
+ .describe("When \"true\", full timestamped transcripts are pulled for the most recent videos on the channel and returned as one row per video. Default is \"false\" because transcripts are the expensive half of this actor and most buyers want channel stats only. Sent as a string for Clay compatibility."),
125
+ transcriptVideoCount: z.string()
126
+ .optional()
127
+ .describe("How many of the channel's most recent videos to pull transcripts for, when transcripts are on. Default 5. Each video is one extra fetch and one extra billable transcript event, so this is the dial that sets the cost of a transcript run."),
128
+ },
129
+ }, async ({ company_domain, company_name, handle, includeFollowerCounts, skipCache, includeTranscripts, transcriptVideoCount }) => {
130
+ return runActor("6hSpD3kd2s2IQ4VSs", "YouTube Channel Stats Extractor", compact({
131
+ company_domain,
132
+ company_name,
133
+ handle,
134
+ includeFollowerCounts: boolToString(includeFollowerCounts),
135
+ skipCache: boolToString(skipCache),
136
+ includeTranscripts: boolToString(includeTranscripts),
137
+ transcriptVideoCount,
138
+ }));
139
+ });
140
+ const transport = new StdioServerTransport();
141
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@mambalabsdev/mcp-youtube-channel-transcript-extractor",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for the Mamba Labs YouTube Channel Stats Extractor actor on Apify. Resolve a company domain or YouTube handle to the channel with subscriber count and channel metadata.",
5
+ "type": "module",
6
+ "bin": {
7
+ "mcp-youtube-channel-transcript-extractor": "./build/index.js"
8
+ },
9
+ "files": [
10
+ "build",
11
+ "README.md",
12
+ "LICENSE",
13
+ "SECURITY.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc && chmod +x build/index.js",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "keywords": [
20
+ "mcp",
21
+ "model-context-protocol",
22
+ "youtube",
23
+ "channel-stats",
24
+ "subscribers",
25
+ "transcripts",
26
+ "apify",
27
+ "clay"
28
+ ],
29
+ "author": "Mamba Labs <labs@mambamindsetgrowth.com>",
30
+ "license": "MIT",
31
+ "homepage": "https://apify.com/mambalabs",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/mambalabsdev/mcp-youtube-channel-transcript-extractor"
35
+ },
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "^1.12.0",
41
+ "zod": "^3.23.8"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^20.14.0",
45
+ "typescript": "^5.6.0"
46
+ },
47
+ "mcpName": "com.mambabuilt/mcp-youtube-channel-transcript-extractor"
48
+ }