@mambalabsdev/mcp-meta-brand-presence-mapper 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 +21 -0
- package/README.md +86 -0
- package/SECURITY.md +31 -0
- package/build/index.js +137 -0
- package/package.json +49 -0
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,86 @@
|
|
|
1
|
+
# Instagram Threads Facebook Brand Presence Mapper MCP Server
|
|
2
|
+
|
|
3
|
+
[](https://smithery.ai/servers/mambabuilt/mcp-meta-brand-presence-mapper) [](https://glama.ai/mcp/servers/mambalabsdev/mcp-meta-brand-presence-mapper) [](https://www.npmjs.com/package/@mambalabsdev/mcp-meta-brand-presence-mapper) [](https://www.npmjs.com/package/@mambalabsdev/mcp-meta-brand-presence-mapper) [](https://github.com/mambalabsdev/mcp-meta-brand-presence-mapper/blob/main/LICENSE)
|
|
4
|
+
|
|
5
|
+
MCP server for the Mamba Labs **Instagram Threads Facebook Brand Presence Mapper** actor on Apify.
|
|
6
|
+
|
|
7
|
+
Resolve a company domain to its Instagram, Threads and Facebook accounts with follower and post counts.
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
Resolve a company domain to its Instagram, Threads and Facebook accounts with follower and post counts, as one flat Clay ready row. The Threads handle is derived from the resolved Instagram handle at no extra discovery cost. Instagram and Threads counts are rounded by Meta and the row carries both the rounded integer and the platform's own display string. Facebook is best effort: Meta serves a login wall to anonymous clients, so blocked is a normal answer there and never a zero. 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-meta-brand-presence-mapper": {
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["-y", "@mambalabsdev/mcp-meta-brand-presence-mapper"],
|
|
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/meta-brand-presence-mapper).
|
|
36
|
+
|
|
37
|
+
## Example prompts
|
|
38
|
+
|
|
39
|
+
- "Get the Instagram and Threads follower counts for glossier.com."
|
|
40
|
+
- "Has oatly.com adopted Threads yet, and how many posts do they have?"
|
|
41
|
+
- "Find the Facebook Page for everlane.com."
|
|
42
|
+
|
|
43
|
+
## Tool and inputs
|
|
44
|
+
|
|
45
|
+
Tool: `map_meta_brand_presence`
|
|
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. The Instagram handle with or without the leading @. Supplying it skips Instagram discovery AND gives Threads its handle for free, because Th |
|
|
52
|
+
| `platforms` | array | Which of the three Meta surfaces to map. Default is all three. Dropping Facebook is the common choice: it is the least reliable of the three and it co |
|
|
53
|
+
| `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 |
|
|
54
|
+
| `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 |
|
|
55
|
+
|
|
56
|
+
## Reading the output
|
|
57
|
+
|
|
58
|
+
Every row carries a per platform `_status` field, and it is the field to read
|
|
59
|
+
first. The vocabulary is the same across the whole Mamba Labs social family:
|
|
60
|
+
|
|
61
|
+
| Status | Meaning |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `ok` | fetched and parsed, the value is there |
|
|
64
|
+
| `not_found` | we looked and there is no such profile |
|
|
65
|
+
| `not_extractable` | the profile exists and the value is not on the wire to us |
|
|
66
|
+
| `blocked` | the platform refused us, worth retrying later |
|
|
67
|
+
| `identity_mismatch` | we found a real profile and it belongs to someone else |
|
|
68
|
+
| `skipped` | you did not ask for this platform |
|
|
69
|
+
|
|
70
|
+
**`false` and `null` are never interchangeable.** `false` means we looked and the
|
|
71
|
+
answer is no. `null` means we could not look. If you filter for companies with no
|
|
72
|
+
presence, filter on `false`, because `null` rows are unknown rather than absent.
|
|
73
|
+
|
|
74
|
+
## Full actor documentation
|
|
75
|
+
|
|
76
|
+
[apify.com/mambalabs/meta-brand-presence-mapper](https://apify.com/mambalabs/meta-brand-presence-mapper)
|
|
77
|
+
|
|
78
|
+
## Mamba Labs GTM Suite
|
|
79
|
+
|
|
80
|
+
Mamba Labs builds a fleet of GTM enrichment actors that share one flat, Clay
|
|
81
|
+
ready output convention, so their rows join on `company_domain` with no cleaning
|
|
82
|
+
step. Full fleet: [apify.com/mambalabs](https://apify.com/mambalabs)
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
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,137 @@
|
|
|
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=512 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=512`;
|
|
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-meta-brand-presence-mapper",
|
|
93
|
+
version: pkg.version,
|
|
94
|
+
});
|
|
95
|
+
// Instagram Threads Facebook Brand Presence Mapper (immutable actor ID HXh65I0JBjPsunbY8)
|
|
96
|
+
server.registerTool("map_meta_brand_presence", {
|
|
97
|
+
title: "Map Instagram Threads and Facebook Presence",
|
|
98
|
+
description: "Resolve a company domain to its Instagram, Threads and Facebook accounts with follower and post counts, as one flat Clay ready row. The Threads handle is derived from the resolved Instagram handle at no extra discovery cost. Instagram and Threads counts are rounded by Meta and the row carries both the rounded integer and the platform's own display string. Facebook is best effort: Meta serves a login wall to anonymous clients, so blocked is a normal answer there and never a zero. Read only; requires an APIFY_TOKEN and consumes Apify credits per call.",
|
|
99
|
+
annotations: {
|
|
100
|
+
title: "Map Instagram Threads and Facebook Presence",
|
|
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. The Instagram handle with or without the leading @. Supplying it skips Instagram discovery AND gives Threads its handle for free, because Threads handles are Instagram handles (5 of 5 measured)."),
|
|
116
|
+
platforms: z.array(z.enum(["instagram", "threads", "facebook"]))
|
|
117
|
+
.optional()
|
|
118
|
+
.describe("Which of the three Meta surfaces to map. Default is all three. Dropping Facebook is the common choice: it is the least reliable of the three and it costs a fetch to find that out."),
|
|
119
|
+
includeFollowerCounts: z.boolean()
|
|
120
|
+
.optional()
|
|
121
|
+
.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."),
|
|
122
|
+
skipCache: z.boolean()
|
|
123
|
+
.optional()
|
|
124
|
+
.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."),
|
|
125
|
+
},
|
|
126
|
+
}, async ({ company_domain, company_name, handle, platforms, includeFollowerCounts, skipCache }) => {
|
|
127
|
+
return runActor("HXh65I0JBjPsunbY8", "Instagram Threads Facebook Brand Presence Mapper", compact({
|
|
128
|
+
company_domain,
|
|
129
|
+
company_name,
|
|
130
|
+
handle,
|
|
131
|
+
platforms,
|
|
132
|
+
includeFollowerCounts: boolToString(includeFollowerCounts),
|
|
133
|
+
skipCache: boolToString(skipCache),
|
|
134
|
+
}));
|
|
135
|
+
});
|
|
136
|
+
const transport = new StdioServerTransport();
|
|
137
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mambalabsdev/mcp-meta-brand-presence-mapper",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for the Mamba Labs Instagram Threads Facebook Brand Presence Mapper actor on Apify. Resolve a company domain to its Instagram, Threads and Facebook accounts with follower and post counts.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-meta-brand-presence-mapper": "./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
|
+
"instagram",
|
|
23
|
+
"threads",
|
|
24
|
+
"facebook",
|
|
25
|
+
"social-media",
|
|
26
|
+
"follower-counts",
|
|
27
|
+
"apify",
|
|
28
|
+
"clay"
|
|
29
|
+
],
|
|
30
|
+
"author": "Mamba Labs <labs@mambamindsetgrowth.com>",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"homepage": "https://apify.com/mambalabs",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/mambalabsdev/mcp-meta-brand-presence-mapper"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
42
|
+
"zod": "^3.23.8"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^20.14.0",
|
|
46
|
+
"typescript": "^5.6.0"
|
|
47
|
+
},
|
|
48
|
+
"mcpName": "com.mambabuilt/mcp-meta-brand-presence-mapper"
|
|
49
|
+
}
|