hasdata-facebook-mcp 1.0.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,21 @@
1
+ ---
2
+ name: Something in the README is wrong
3
+ about: A tool table, a response sample or a documented behaviour does not match reality
4
+ labels: documentation
5
+ ---
6
+
7
+ **Where in the README**
8
+
9
+ Section or heading.
10
+
11
+ **What it says**
12
+
13
+ Quote the line.
14
+
15
+ **The call you made**
16
+
17
+ Tool name and arguments, or the equivalent REST URL with your key removed.
18
+
19
+ **What came back**
20
+
21
+ Trimmed response, with anything private removed.
@@ -0,0 +1,31 @@
1
+ # The tool contract is checked on a schedule as well as on push, because the upstream tool list
2
+ # can change without a single commit in this repository.
3
+ name: tool contract
4
+
5
+ on:
6
+ push:
7
+ branches: [main]
8
+ pull_request:
9
+ schedule:
10
+ - cron: '0 6 * * 1'
11
+ workflow_dispatch:
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ contract:
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 5
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: actions/setup-node@v4
23
+ with:
24
+ node-version: '22'
25
+ # Forks cannot read repository secrets. The suite skips its live checks when the key is
26
+ # absent, so a pull request from a fork stays green instead of failing for a reason the
27
+ # contributor cannot fix.
28
+ - name: Assert the tool list still matches the README
29
+ env:
30
+ HASDATA_API_KEY: ${{ secrets.HASDATA_API_KEY }}
31
+ run: npm test
@@ -0,0 +1,75 @@
1
+ # Publishes the npm and PyPI wrapper packages on a version tag, using OIDC
2
+ # trusted publishing. No NPM_TOKEN or PYPI_TOKEN is stored anywhere: GitHub
3
+ # mints a short-lived OIDC token per run, and npmjs.org / pypi.org accept it
4
+ # because this repo + workflow are configured as trusted publishers.
5
+ #
6
+ # One-time setup, done once per package on the registries (not in this repo):
7
+ # npmjs.org -> package settings -> Trusted Publisher -> GitHub Actions,
8
+ # repo HasData/facebook-mcp, workflow publish.yml
9
+ # pypi.org -> the hasdata org -> Publishing -> add a trusted publisher
10
+ # (pending publisher works before the first release),
11
+ # repo HasData/facebook-mcp, workflow publish.yml
12
+ #
13
+ # The MCP registry entry (com.hasdata/facebook) is NOT published here. It uses
14
+ # domain auth, which would need the namespace-wide Ed25519 key as a secret in
15
+ # every repo. That key stays off CI; the registry entry is published by hand
16
+ # when server.json changes, after the package versions below are live.
17
+ #
18
+ # Release: bump nothing by hand. Tag the commit `vX.Y.Z` and push the tag; the
19
+ # tag is the single source of the version and is written into both manifests.
20
+
21
+ name: publish
22
+
23
+ on:
24
+ push:
25
+ tags: ['v*.*.*']
26
+
27
+ permissions:
28
+ contents: read
29
+ id-token: write
30
+
31
+ jobs:
32
+ npm:
33
+ runs-on: ubuntu-latest
34
+ steps:
35
+ - uses: actions/checkout@v4
36
+ # No registry-url here on purpose. With it, setup-node writes an .npmrc
37
+ # carrying _authToken=${NODE_AUTH_TOKEN}, which resolves to a placeholder
38
+ # when no token is passed. npm then authenticates with that garbage instead
39
+ # of falling back to OIDC, and a scoped package answers 404.
40
+ - uses: actions/setup-node@v4
41
+ with:
42
+ node-version: '24'
43
+ # OIDC trusted publishing landed in npm 11.5.1. Node 24 already ships a
44
+ # newer npm than that, but pinning the upgrade here keeps the job working
45
+ # if the runner image drifts back.
46
+ - name: Upgrade npm for OIDC trusted publishing
47
+ run: |
48
+ npm install -g npm@latest
49
+ npm -v
50
+ - name: Set version from the tag
51
+ run: npm version "${GITHUB_REF_NAME#v}" --no-git-tag-version --allow-same-version
52
+ - name: Publish to npm (OIDC, no token)
53
+ run: npm publish --access public
54
+
55
+ pypi:
56
+ runs-on: ubuntu-latest
57
+ steps:
58
+ - uses: actions/checkout@v4
59
+ - uses: actions/setup-python@v5
60
+ with:
61
+ python-version: '3.12'
62
+ - name: Set version from the tag
63
+ run: |
64
+ python - "${GITHUB_REF_NAME#v}" <<'PY'
65
+ import re, sys
66
+ v = sys.argv[1]
67
+ p = "pyproject.toml"
68
+ t = open(p, encoding="utf-8").read()
69
+ t = re.sub(r'(?m)^version = ".*"$', f'version = "{v}"', t, count=1)
70
+ open(p, "w", encoding="utf-8", newline="\n").write(t)
71
+ PY
72
+ - name: Build the wheel and sdist
73
+ run: pipx run build
74
+ - name: Publish to PyPI (OIDC, no token)
75
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,11 @@
1
+ node_modules/
2
+ package-lock.json
3
+ dist/
4
+ build/
5
+ *.egg-info/
6
+ __pycache__/
7
+ *.pyc
8
+ .env
9
+ .env.*
10
+ .DS_Store
11
+ *.log
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HasData
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.
@@ -0,0 +1,366 @@
1
+ Metadata-Version: 2.5
2
+ Name: hasdata-facebook-mcp
3
+ Version: 1.0.0
4
+ Summary: MCP server for Facebook through HasData's hosted API. 1,000 free credits every month.
5
+ Project-URL: Homepage, https://docs.hasdata.com/apis/facebook/profile
6
+ Project-URL: Repository, https://github.com/HasData/facebook-mcp
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: facebook,hasdata,lead-enrichment,marketing,mcp,model-context-protocol,social-media
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: mcp-proxy>=0.12.0
12
+ Requires-Dist: mcp<2,>=1.17
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Facebook MCP Server
16
+
17
+ <!-- mcp-name: com.hasdata/facebook -->
18
+
19
+ A hosted Model Context Protocol (MCP) server that gives Claude, Cursor, Windsurf and any other MCP client one read-only Facebook tool. Look up a public page or profile by its handle and get the page record with its exact like count, contact details and owner, plus the post feed with reactions, comments and shares, all as structured JSON, with no Meta developer app and nothing to host.
20
+
21
+ It reads public Facebook pages that a signed-out visitor can see. Groups and private personal accounts are out of scope.
22
+
23
+ **1,000 free credits every month, no card required**, which is 100 Facebook calls at the 10-credit rate.
24
+
25
+ ```
26
+ https://mcp.hasdata.com/api/mcp?apis=facebook
27
+ ```
28
+
29
+ [![Glama score](https://glama.ai/mcp/servers/HasData/facebook-mcp/badges/score.svg)](https://glama.ai/mcp/servers/HasData/facebook-mcp)
30
+ [![tool contract](https://github.com/HasData/facebook-mcp/actions/workflows/contract.yml/badge.svg)](https://github.com/HasData/facebook-mcp/actions/workflows/contract.yml)
31
+ [![MCP](https://img.shields.io/badge/MCP-remote%20%7C%20streamable%20HTTP-6366f1?style=flat-square)](https://mcp.hasdata.com/api/mcp?apis=facebook)
32
+ [![Tools](https://img.shields.io/badge/tools-1-10b981?style=flat-square)](#tools)
33
+ [![npm](https://img.shields.io/npm/v/@hasdata/facebook-mcp?style=flat-square&logo=npm&label=npm&color=cb3837)](https://www.npmjs.com/package/@hasdata/facebook-mcp)
34
+ [![PyPI](https://img.shields.io/pypi/v/hasdata-facebook-mcp?style=flat-square&logo=pypi&logoColor=white&label=PyPI&color=3775a9)](https://pypi.org/project/hasdata-facebook-mcp/)
35
+ [![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
36
+
37
+ ## Contents
38
+
39
+ - [What you need](#what-you-need)
40
+ - [Quick start](#quick-start)
41
+ - [Example prompts](#example-prompts)
42
+ - [Tools](#tools)
43
+ - [Errors and failure paths](#errors-and-failure-paths)
44
+ - [Pricing, free tier and limits](#pricing-free-tier-and-limits)
45
+ - [How it compares](#how-it-compares)
46
+ - [FAQ](#faq)
47
+ - [HasData links](#hasdata-links)
48
+ - [Development](#development)
49
+ - [Contributing](#contributing)
50
+ - [License](#license)
51
+
52
+ ## What you need
53
+
54
+ An MCP client and a HasData API key from the [dashboard](https://app.hasdata.com/sign-up?utm_source=github&utm_medium=syndication&utm_campaign=facebook-mcp), free to create with no card, and the free tier covers about 100 calls a month at the 10-credit rate. This is a remote server, so the simplest path is a URL and an `x-api-key` header, with no container to run. A client that only speaks stdio reaches it through a thin launcher, published as `@hasdata/facebook-mcp` on npm and `hasdata-facebook-mcp` on PyPI, shown below.
55
+
56
+ ## Quick start
57
+
58
+ The server URL is the same for every client. We run it hands-on in Claude Code and Claude Desktop. The other blocks follow each client's own documented format for a remote server.
59
+
60
+ | Field | Value |
61
+ | :--- | :--- |
62
+ | URL | `https://mcp.hasdata.com/api/mcp?apis=facebook` |
63
+ | Transport | HTTP, streamable |
64
+ | Auth header | `x-api-key: HASDATA_API_KEY` |
65
+
66
+ Clients with OAuth support can add the same URL as a connector and sign in without putting a key in a config file.
67
+
68
+ <details>
69
+ <summary><b>Claude Code</b></summary>
70
+
71
+ ```bash
72
+ claude mcp add --transport http facebook "https://mcp.hasdata.com/api/mcp?apis=facebook" \
73
+ --header "x-api-key: HASDATA_API_KEY"
74
+ ```
75
+
76
+ </details>
77
+
78
+ <details>
79
+ <summary><b>Claude Desktop</b></summary>
80
+
81
+ Settings, then Connectors, then Add custom connector, then paste `https://mcp.hasdata.com/api/mcp?apis=facebook` and sign in.
82
+
83
+ For the config-file route, Claude Desktop loads only local (stdio) servers, so it reaches a remote server through a stdio launcher. The `@hasdata/facebook-mcp` package is that launcher, and it reads the key from the environment. Add this to `claude_desktop_config.json`:
84
+
85
+ ```json
86
+ {
87
+ "mcpServers": {
88
+ "facebook": {
89
+ "command": "npx",
90
+ "args": ["-y", "@hasdata/facebook-mcp"],
91
+ "env": { "HASDATA_API_KEY": "YOUR_KEY" }
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ For Python instead of Node, swap the launcher for the PyPI package, which `uvx` runs without a manual install:
98
+
99
+ ```json
100
+ {
101
+ "mcpServers": {
102
+ "facebook": {
103
+ "command": "uvx",
104
+ "args": ["hasdata-facebook-mcp"],
105
+ "env": { "HASDATA_API_KEY": "YOUR_KEY" }
106
+ }
107
+ }
108
+ }
109
+ ```
110
+
111
+ </details>
112
+
113
+ <details>
114
+ <summary><b>Cursor</b></summary>
115
+
116
+ `~/.cursor/mcp.json` for every project, or `.cursor/mcp.json` for one:
117
+
118
+ ```json
119
+ {
120
+ "mcpServers": {
121
+ "facebook": {
122
+ "url": "https://mcp.hasdata.com/api/mcp?apis=facebook",
123
+ "headers": { "x-api-key": "HASDATA_API_KEY" }
124
+ }
125
+ }
126
+ }
127
+ ```
128
+
129
+ </details>
130
+
131
+ <details>
132
+ <summary><b>Windsurf</b></summary>
133
+
134
+ `~/.codeium/windsurf/mcp_config.json`. Windsurf calls the field `serverUrl`, not `url`:
135
+
136
+ ```json
137
+ {
138
+ "mcpServers": {
139
+ "facebook": {
140
+ "serverUrl": "https://mcp.hasdata.com/api/mcp?apis=facebook",
141
+ "headers": { "x-api-key": "HASDATA_API_KEY" }
142
+ }
143
+ }
144
+ }
145
+ ```
146
+
147
+ </details>
148
+
149
+ <details>
150
+ <summary><b>VS Code</b></summary>
151
+
152
+ `.vscode/mcp.json` in the workspace:
153
+
154
+ ```json
155
+ {
156
+ "servers": {
157
+ "facebook": {
158
+ "type": "http",
159
+ "url": "https://mcp.hasdata.com/api/mcp?apis=facebook",
160
+ "headers": { "x-api-key": "HASDATA_API_KEY" }
161
+ }
162
+ }
163
+ }
164
+ ```
165
+
166
+ </details>
167
+
168
+ ## Example prompts
169
+
170
+ - How many people like the Nike page, and how many are talking about it this week?
171
+ - Pull the contact details and the confirmed owner of this Facebook page.
172
+ - Read the last ten posts on this page and tell me which got the most shares.
173
+ - Compare the reaction mix on this brand's posts with its competitor's.
174
+ - Is this Facebook page verified, and what category does it list?
175
+ - Walk this page's feed back to the start of the month and list the posts with video.
176
+
177
+ One call returns the page record and the newest post. Walking the feed takes one more call per three posts, using the token the previous response returns.
178
+
179
+ ## Tools
180
+
181
+ One tool, 10 credits per successful call.
182
+
183
+ ### Get Facebook profile
184
+
185
+ [`hasdata_facebook_profile_getFacebookProfile`](https://docs.hasdata.com/apis/facebook/profile?utm_source=github&utm_medium=syndication&utm_campaign=facebook-mcp)
186
+
187
+ A public page or profile, with its feed.
188
+
189
+ | Tool | Credits | What it returns |
190
+ | :--- | :--- | :--- |
191
+ | `hasdata_facebook_profile_getFacebookProfile` | 10 | The page record with likes, followers, contact details and owner, the newest post, the photo strip, and a token for the next three posts |
192
+
193
+ | Parameter | Type | Required | Notes |
194
+ | :--- | :--- | :--- | :--- |
195
+ | `handle` | string | yes | The username, with or without `@`, or the numeric id from a `profile.php?id=…` URL |
196
+ | `language` | string | | Language the page is rendered in, one of 32 codes such as `en`, `de`, `pt` or `zh-hans` |
197
+ | `nextPageToken` | string | | Cursor from the previous response, to read the next three posts |
198
+
199
+ The first call returns four blocks. `profile` is the page record, `posts` holds the newest post, `photos` is the strip of recent images, and `pagination` carries `postsPerPage`, `nextPage` and the `nextPageToken` to continue with.
200
+
201
+ Every call made with a token returns `posts` and `pagination` only, three posts at a time, with a fresh token until the feed ends and the token disappears.
202
+
203
+ ```json
204
+ {
205
+ "profile": {
206
+ "id": "100044541544829",
207
+ "pageId": "15087023444",
208
+ "name": "Nike",
209
+ "username": "nike",
210
+ "url": "https://www.facebook.com/nike",
211
+ "category": "Sportswear Store",
212
+ "biography": "Just Do It.",
213
+ "likesCount": 39545204,
214
+ "talkingAboutCount": 173971,
215
+ "checkInsCount": 18299,
216
+ "followersCount": "39M followers",
217
+ "followingCount": "24 following",
218
+ "verified": true,
219
+ "website": "nike.com",
220
+ "websiteUrl": "http://nike.com/",
221
+ "phone": "+48 58 881 27 61",
222
+ "owner": { "name": "NIKE, Inc.", "isConfirmed": true },
223
+ "profilePicUrl": "https://scontent.fmex5-1.fna.fbcdn.net/v/t39.30808-1/284964043_10159903868513445_7696353984967674128_n.jpg",
224
+ "coverPhotoUrl": "https://scontent.fmex25-1.fna.fbcdn.net/v/t39.30808-6/285211224_10159903868008445_5477337468887983165_n.png"
225
+ },
226
+ "posts": [
227
+ {
228
+ "id": "1393461115481927",
229
+ "url": "https://www.facebook.com/reel/2166091230582141/",
230
+ "text": "Leave your limits at the surface. #JustDoIt",
231
+ "hashtags": ["#JustDoIt"],
232
+ "timestamp": "2025-09-15T16:01:59.000Z",
233
+ "reactionsCount": 7885,
234
+ "commentsCount": 2461,
235
+ "sharesCount": 1124,
236
+ "reactions": [{ "type": "Like", "count": 6491 }],
237
+ "media": [{ "id": "2166091230582141", "type": "Video" }]
238
+ }
239
+ ],
240
+ "photos": [{ "id": "1095811278580247", "type": "Photo", "image": "https://scontent.fmex22-1.fna.fbcdn.net/v/t39.30808-6/471313661_18515827156020081_2460706748949541958_n.jpg" }],
241
+ "pagination": { "postsPerPage": 3, "nextPage": 2, "nextPageToken": "eyJjdXJzb3IiOiJDZzhPYjNKbllXNXBZMTlqZFhKemIzS…" }
242
+ }
243
+ ```
244
+
245
+ ## Errors and failure paths
246
+
247
+ Plan for these rather than assuming a happy path.
248
+
249
+ **Three of the counts are numbers and two are strings, and that is not a bug.** `likesCount`, `talkingAboutCount` and `checkInsCount` are the exact figures Facebook publishes. `followersCount` and `followingCount` arrive as the rounded, localised text the page shows, `"39M followers"` in English and `"39 Mio. Follower"` when `language` is `de`. Compare with the numbers, display the strings.
250
+
251
+ **`language` changes the strings, not the numbers.** `category` moved from `Sportswear Store` to `Sportbekleidungsgeschäft` under `de`, while `likesCount` stayed a number. Pick the language for the reader, not for the maths.
252
+
253
+ **A page that does not exist, was deleted or is not public still answers 200.** The response then carries `error` instead of `profile`, and the call is billed. Test for `profile` before you read it. Groups and private personal accounts land here too.
254
+
255
+ **The first page of the feed holds one post, not three.** `postsPerPage` says 3, and pages two onward do return three, but the opening response carries the newest post alone alongside the profile and photos. Count what you got rather than what the field promises.
256
+
257
+ **Later pages drop `profile` and `photos`.** A call made with `nextPageToken` returns `posts` and `pagination` only. Keep the profile from the first response instead of expecting it again.
258
+
259
+ **The token is the only way forward.** `nextPage` is a page number for your own bookkeeping, and there is no parameter that takes it. Pass the previous `nextPageToken` unchanged, and stop when a response arrives without one.
260
+
261
+ **Contact details are what the page shows to the request, not a canonical record.** A global brand page can surface a regional phone number, and `website` is the display text while `websiteUrl` is the link. Read `owner.isConfirmed` before you treat the owner name as verified.
262
+
263
+ **Image URLs are Facebook CDN links with signed parameters.** They expire. Fetch what you need promptly and store the file, not the URL.
264
+
265
+ Results that carry data also carry a `requestMetadata.id` worth quoting in support.
266
+
267
+ ## Pricing, free tier and limits
268
+
269
+ The Facebook tool costs **10 credits per successful call**. Response size does not change the price, and a page of the feed costs the same as the opening call, so a thirty-post read is one call plus ten more.
270
+
271
+ The free tier is **1,000 credits every month with no card**, which is 100 Facebook calls at the base rate. It renews with the billing cycle, so a low-volume agent runs on the free tier indefinitely.
272
+
273
+ Paid plans start at **$49 a month** for 200,000 credits, which is 20,000 calls. The unit price falls with volume, from **$2.45 per 1,000 calls** on the entry plan to **$1.00** on Business, **$0.84** on Growth and **$0.74** on the largest [high-volume plans](https://hasdata.com/prices?utm_source=github&utm_medium=syndication&utm_campaign=facebook-mcp).
274
+
275
+ Your plan also sets concurrency. The free tier allows 1 request at a time, Startup 15, Business 30, Growth 50, and the high-volume plans run from 200 to 1,500. Retry on the 429 with a backoff in anything unattended, because an agent that enriches a list of pages will reach the ceiling before you do.
276
+
277
+ A request that comes back non-200 is not billed. A successful call that finds no page is still a call.
278
+
279
+ ## How it compares
280
+
281
+ Meta's Graph API is the official route to page data, and it is built for a different situation.
282
+
283
+ | | Meta Graph API | This server |
284
+ | :--- | :--- | :--- |
285
+ | Eligibility | A Meta developer app, an access token, and App Review for pages you do not manage | An API key |
286
+ | Pages you can read | Your own pages in full, others only through reviewed permissions | Any public page or profile |
287
+ | Setup | App creation, permissions, token handling, review | One header |
288
+ | Post reactions | Per-type counts on pages you manage | Per-type counts on any public page |
289
+ | Language of the record | Your app's locale | Any of 32, per call |
290
+ | Cost | Free within rate limits | Paid past the free tier, 10 credits a call |
291
+
292
+ The row that decides it is which pages you can read. The Graph API is the right tool for pages you administer, and its Page Public Content Access permission for everything else needs a business verification and a review with a stated use case. When the page is yours, use the Graph API, it is free and complete.
293
+
294
+ ## FAQ
295
+
296
+ ### Is there an official Facebook MCP server?
297
+
298
+ Meta does not publish one for reading public pages. This one is maintained by HasData and reads public Facebook pages.
299
+
300
+ ### What is a Facebook MCP server?
301
+
302
+ An MCP server exposes tools an AI client can call. This one turns a public Facebook page and its feed into JSON an agent can reason over, without a browser or a scraping library in your stack.
303
+
304
+ ### Do I need a Facebook account or a Meta developer app?
305
+
306
+ No. The only credential is your HasData key.
307
+
308
+ ### Can I read a personal profile or a group?
309
+
310
+ A public personal profile works the same way as a page. Private accounts and groups are not supported and come back with the not-available error.
311
+
312
+ ### Can I pass a numeric id instead of a username?
313
+
314
+ Yes. Both the `id` and the `pageId` a response returns resolve as `handle`, so a page reached once by name can be tracked by id afterwards.
315
+
316
+ ### How do I read the whole feed?
317
+
318
+ Call once with the handle, then keep calling with the `nextPageToken` from each response until a response arrives without one. Three posts come back per token.
319
+
320
+ ### Why is `followersCount` text while `likesCount` is a number?
321
+
322
+ Because that is how Facebook publishes them. The like count and the talking-about count are exact on the page, and the follower count is shown rounded and localised. The response passes both through as they are.
323
+
324
+ ### Can I use this together with other HasData APIs?
325
+
326
+ Yes. One key covers everything, and one endpoint serves them all through the `apis` parameter. Point a client at `?apis=facebook,instagram` to get both tool sets in one connection, or at [`mcp.hasdata.com/api/mcp`](https://docs.hasdata.com/mcp-server?utm_source=github&utm_medium=syndication&utm_campaign=facebook-mcp) for the full catalogue.
327
+
328
+ ### Is HasData affiliated with Meta or Facebook?
329
+
330
+ No. HasData is an independent service and is not affiliated with, endorsed by, or sponsored by Meta. Facebook is a trademark of its respective owner. The tools work with publicly available data only, and you are responsible for using the results in line with Meta's terms and the law that applies to you.
331
+
332
+ ### Compliance and personal data
333
+
334
+ A page record for a business is a business record, and a public personal profile is personal data in the plainest sense, with a name, a photo, a bio and a public feed. The tool does not tell the two apart, so your purpose has to. Keep to what your use case needs, do not build profiles of individuals you have no business with, and check your obligations under the GDPR, the CCPA and Meta's terms before storing anything. Contact details on a page are published for customers, and marketing to them is regulated separately again.
335
+
336
+ ## HasData links
337
+
338
+ - [Facebook Profile API documentation](https://docs.hasdata.com/apis/facebook/profile?utm_source=github&utm_medium=syndication&utm_campaign=facebook-mcp), the REST endpoint behind this tool
339
+ - [MCP server documentation](https://docs.hasdata.com/mcp-server?utm_source=github&utm_medium=syndication&utm_campaign=facebook-mcp)
340
+ - [Pricing](https://hasdata.com/prices?utm_source=github&utm_medium=syndication&utm_campaign=facebook-mcp)
341
+ - [Dashboard](https://app.hasdata.com/sign-up?utm_source=github&utm_medium=syndication&utm_campaign=facebook-mcp)
342
+
343
+ Other HasData MCP servers: [Instagram](https://github.com/HasData/instagram-mcp), [TikTok](https://github.com/HasData/tiktok-mcp), [YouTube](https://github.com/HasData/youtube-mcp), [Google Search](https://github.com/HasData/google-search-mcp), [Google Images](https://github.com/HasData/google-images-mcp), [Google Scholar](https://github.com/HasData/google-scholar-mcp), [Google Maps](https://github.com/HasData/google-maps-mcp), [Google Trends](https://github.com/HasData/google-trends-mcp), [Google Flights](https://github.com/HasData/google-flights-mcp), [Bing](https://github.com/HasData/bing-mcp), [DuckDuckGo](https://github.com/HasData/duckduckgo-mcp), [Amazon](https://github.com/HasData/amazon-mcp), [Walmart](https://github.com/HasData/walmart-mcp), [Shopify](https://github.com/HasData/shopify-mcp), [Yelp](https://github.com/HasData/yelp-mcp), [Yellow Pages](https://github.com/HasData/yellowpages-mcp), [Zillow](https://github.com/HasData/zillow-mcp), [Redfin](https://github.com/HasData/redfin-mcp), [Airbnb](https://github.com/HasData/airbnb-mcp), [Booking.com](https://github.com/HasData/booking-mcp), [Indeed](https://github.com/HasData/indeed-mcp), [Glassdoor](https://github.com/HasData/glassdoor-mcp), [Web Scraping](https://github.com/HasData/web-scraping-mcp).
344
+
345
+ ## Development
346
+
347
+ The launcher is a thin stdio bridge to the remote server, so there is nothing to build.
348
+
349
+ ```bash
350
+ npm install
351
+ HASDATA_API_KEY=your_key_here npm test
352
+ ```
353
+
354
+ The tests in `test/` assert the tool contract, the part that can break without a commit here. They check that `?apis=facebook` returns the one expected tool, that its name has not changed, that it still requires `handle` and carries a description, that `language` still offers the codes this README names, and that the key in use is actually accepted.
355
+
356
+ One test reads a live page and asserts the two things this README leans on: that `likesCount` is a number rather than display text, and that `pagination.nextPageToken` arrives, because without the token the feed walk this README documents does not exist. That call costs 10 credits, which is the price of a canary that can fail for the right reason.
357
+
358
+ The contract suite also runs weekly on a schedule, because the upstream tool list can change without anyone touching this repository.
359
+
360
+ ## Contributing
361
+
362
+ A tool table, a response sample or a documented behaviour that does not match reality is worth an issue. There is a template for exactly that. Pull requests are welcome for the same, and for anything in the launcher.
363
+
364
+ ## License
365
+
366
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,352 @@
1
+ # Facebook MCP Server
2
+
3
+ <!-- mcp-name: com.hasdata/facebook -->
4
+
5
+ A hosted Model Context Protocol (MCP) server that gives Claude, Cursor, Windsurf and any other MCP client one read-only Facebook tool. Look up a public page or profile by its handle and get the page record with its exact like count, contact details and owner, plus the post feed with reactions, comments and shares, all as structured JSON, with no Meta developer app and nothing to host.
6
+
7
+ It reads public Facebook pages that a signed-out visitor can see. Groups and private personal accounts are out of scope.
8
+
9
+ **1,000 free credits every month, no card required**, which is 100 Facebook calls at the 10-credit rate.
10
+
11
+ ```
12
+ https://mcp.hasdata.com/api/mcp?apis=facebook
13
+ ```
14
+
15
+ [![Glama score](https://glama.ai/mcp/servers/HasData/facebook-mcp/badges/score.svg)](https://glama.ai/mcp/servers/HasData/facebook-mcp)
16
+ [![tool contract](https://github.com/HasData/facebook-mcp/actions/workflows/contract.yml/badge.svg)](https://github.com/HasData/facebook-mcp/actions/workflows/contract.yml)
17
+ [![MCP](https://img.shields.io/badge/MCP-remote%20%7C%20streamable%20HTTP-6366f1?style=flat-square)](https://mcp.hasdata.com/api/mcp?apis=facebook)
18
+ [![Tools](https://img.shields.io/badge/tools-1-10b981?style=flat-square)](#tools)
19
+ [![npm](https://img.shields.io/npm/v/@hasdata/facebook-mcp?style=flat-square&logo=npm&label=npm&color=cb3837)](https://www.npmjs.com/package/@hasdata/facebook-mcp)
20
+ [![PyPI](https://img.shields.io/pypi/v/hasdata-facebook-mcp?style=flat-square&logo=pypi&logoColor=white&label=PyPI&color=3775a9)](https://pypi.org/project/hasdata-facebook-mcp/)
21
+ [![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
22
+
23
+ ## Contents
24
+
25
+ - [What you need](#what-you-need)
26
+ - [Quick start](#quick-start)
27
+ - [Example prompts](#example-prompts)
28
+ - [Tools](#tools)
29
+ - [Errors and failure paths](#errors-and-failure-paths)
30
+ - [Pricing, free tier and limits](#pricing-free-tier-and-limits)
31
+ - [How it compares](#how-it-compares)
32
+ - [FAQ](#faq)
33
+ - [HasData links](#hasdata-links)
34
+ - [Development](#development)
35
+ - [Contributing](#contributing)
36
+ - [License](#license)
37
+
38
+ ## What you need
39
+
40
+ An MCP client and a HasData API key from the [dashboard](https://app.hasdata.com/sign-up?utm_source=github&utm_medium=syndication&utm_campaign=facebook-mcp), free to create with no card, and the free tier covers about 100 calls a month at the 10-credit rate. This is a remote server, so the simplest path is a URL and an `x-api-key` header, with no container to run. A client that only speaks stdio reaches it through a thin launcher, published as `@hasdata/facebook-mcp` on npm and `hasdata-facebook-mcp` on PyPI, shown below.
41
+
42
+ ## Quick start
43
+
44
+ The server URL is the same for every client. We run it hands-on in Claude Code and Claude Desktop. The other blocks follow each client's own documented format for a remote server.
45
+
46
+ | Field | Value |
47
+ | :--- | :--- |
48
+ | URL | `https://mcp.hasdata.com/api/mcp?apis=facebook` |
49
+ | Transport | HTTP, streamable |
50
+ | Auth header | `x-api-key: HASDATA_API_KEY` |
51
+
52
+ Clients with OAuth support can add the same URL as a connector and sign in without putting a key in a config file.
53
+
54
+ <details>
55
+ <summary><b>Claude Code</b></summary>
56
+
57
+ ```bash
58
+ claude mcp add --transport http facebook "https://mcp.hasdata.com/api/mcp?apis=facebook" \
59
+ --header "x-api-key: HASDATA_API_KEY"
60
+ ```
61
+
62
+ </details>
63
+
64
+ <details>
65
+ <summary><b>Claude Desktop</b></summary>
66
+
67
+ Settings, then Connectors, then Add custom connector, then paste `https://mcp.hasdata.com/api/mcp?apis=facebook` and sign in.
68
+
69
+ For the config-file route, Claude Desktop loads only local (stdio) servers, so it reaches a remote server through a stdio launcher. The `@hasdata/facebook-mcp` package is that launcher, and it reads the key from the environment. Add this to `claude_desktop_config.json`:
70
+
71
+ ```json
72
+ {
73
+ "mcpServers": {
74
+ "facebook": {
75
+ "command": "npx",
76
+ "args": ["-y", "@hasdata/facebook-mcp"],
77
+ "env": { "HASDATA_API_KEY": "YOUR_KEY" }
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ For Python instead of Node, swap the launcher for the PyPI package, which `uvx` runs without a manual install:
84
+
85
+ ```json
86
+ {
87
+ "mcpServers": {
88
+ "facebook": {
89
+ "command": "uvx",
90
+ "args": ["hasdata-facebook-mcp"],
91
+ "env": { "HASDATA_API_KEY": "YOUR_KEY" }
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ </details>
98
+
99
+ <details>
100
+ <summary><b>Cursor</b></summary>
101
+
102
+ `~/.cursor/mcp.json` for every project, or `.cursor/mcp.json` for one:
103
+
104
+ ```json
105
+ {
106
+ "mcpServers": {
107
+ "facebook": {
108
+ "url": "https://mcp.hasdata.com/api/mcp?apis=facebook",
109
+ "headers": { "x-api-key": "HASDATA_API_KEY" }
110
+ }
111
+ }
112
+ }
113
+ ```
114
+
115
+ </details>
116
+
117
+ <details>
118
+ <summary><b>Windsurf</b></summary>
119
+
120
+ `~/.codeium/windsurf/mcp_config.json`. Windsurf calls the field `serverUrl`, not `url`:
121
+
122
+ ```json
123
+ {
124
+ "mcpServers": {
125
+ "facebook": {
126
+ "serverUrl": "https://mcp.hasdata.com/api/mcp?apis=facebook",
127
+ "headers": { "x-api-key": "HASDATA_API_KEY" }
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ </details>
134
+
135
+ <details>
136
+ <summary><b>VS Code</b></summary>
137
+
138
+ `.vscode/mcp.json` in the workspace:
139
+
140
+ ```json
141
+ {
142
+ "servers": {
143
+ "facebook": {
144
+ "type": "http",
145
+ "url": "https://mcp.hasdata.com/api/mcp?apis=facebook",
146
+ "headers": { "x-api-key": "HASDATA_API_KEY" }
147
+ }
148
+ }
149
+ }
150
+ ```
151
+
152
+ </details>
153
+
154
+ ## Example prompts
155
+
156
+ - How many people like the Nike page, and how many are talking about it this week?
157
+ - Pull the contact details and the confirmed owner of this Facebook page.
158
+ - Read the last ten posts on this page and tell me which got the most shares.
159
+ - Compare the reaction mix on this brand's posts with its competitor's.
160
+ - Is this Facebook page verified, and what category does it list?
161
+ - Walk this page's feed back to the start of the month and list the posts with video.
162
+
163
+ One call returns the page record and the newest post. Walking the feed takes one more call per three posts, using the token the previous response returns.
164
+
165
+ ## Tools
166
+
167
+ One tool, 10 credits per successful call.
168
+
169
+ ### Get Facebook profile
170
+
171
+ [`hasdata_facebook_profile_getFacebookProfile`](https://docs.hasdata.com/apis/facebook/profile?utm_source=github&utm_medium=syndication&utm_campaign=facebook-mcp)
172
+
173
+ A public page or profile, with its feed.
174
+
175
+ | Tool | Credits | What it returns |
176
+ | :--- | :--- | :--- |
177
+ | `hasdata_facebook_profile_getFacebookProfile` | 10 | The page record with likes, followers, contact details and owner, the newest post, the photo strip, and a token for the next three posts |
178
+
179
+ | Parameter | Type | Required | Notes |
180
+ | :--- | :--- | :--- | :--- |
181
+ | `handle` | string | yes | The username, with or without `@`, or the numeric id from a `profile.php?id=…` URL |
182
+ | `language` | string | | Language the page is rendered in, one of 32 codes such as `en`, `de`, `pt` or `zh-hans` |
183
+ | `nextPageToken` | string | | Cursor from the previous response, to read the next three posts |
184
+
185
+ The first call returns four blocks. `profile` is the page record, `posts` holds the newest post, `photos` is the strip of recent images, and `pagination` carries `postsPerPage`, `nextPage` and the `nextPageToken` to continue with.
186
+
187
+ Every call made with a token returns `posts` and `pagination` only, three posts at a time, with a fresh token until the feed ends and the token disappears.
188
+
189
+ ```json
190
+ {
191
+ "profile": {
192
+ "id": "100044541544829",
193
+ "pageId": "15087023444",
194
+ "name": "Nike",
195
+ "username": "nike",
196
+ "url": "https://www.facebook.com/nike",
197
+ "category": "Sportswear Store",
198
+ "biography": "Just Do It.",
199
+ "likesCount": 39545204,
200
+ "talkingAboutCount": 173971,
201
+ "checkInsCount": 18299,
202
+ "followersCount": "39M followers",
203
+ "followingCount": "24 following",
204
+ "verified": true,
205
+ "website": "nike.com",
206
+ "websiteUrl": "http://nike.com/",
207
+ "phone": "+48 58 881 27 61",
208
+ "owner": { "name": "NIKE, Inc.", "isConfirmed": true },
209
+ "profilePicUrl": "https://scontent.fmex5-1.fna.fbcdn.net/v/t39.30808-1/284964043_10159903868513445_7696353984967674128_n.jpg",
210
+ "coverPhotoUrl": "https://scontent.fmex25-1.fna.fbcdn.net/v/t39.30808-6/285211224_10159903868008445_5477337468887983165_n.png"
211
+ },
212
+ "posts": [
213
+ {
214
+ "id": "1393461115481927",
215
+ "url": "https://www.facebook.com/reel/2166091230582141/",
216
+ "text": "Leave your limits at the surface. #JustDoIt",
217
+ "hashtags": ["#JustDoIt"],
218
+ "timestamp": "2025-09-15T16:01:59.000Z",
219
+ "reactionsCount": 7885,
220
+ "commentsCount": 2461,
221
+ "sharesCount": 1124,
222
+ "reactions": [{ "type": "Like", "count": 6491 }],
223
+ "media": [{ "id": "2166091230582141", "type": "Video" }]
224
+ }
225
+ ],
226
+ "photos": [{ "id": "1095811278580247", "type": "Photo", "image": "https://scontent.fmex22-1.fna.fbcdn.net/v/t39.30808-6/471313661_18515827156020081_2460706748949541958_n.jpg" }],
227
+ "pagination": { "postsPerPage": 3, "nextPage": 2, "nextPageToken": "eyJjdXJzb3IiOiJDZzhPYjNKbllXNXBZMTlqZFhKemIzS…" }
228
+ }
229
+ ```
230
+
231
+ ## Errors and failure paths
232
+
233
+ Plan for these rather than assuming a happy path.
234
+
235
+ **Three of the counts are numbers and two are strings, and that is not a bug.** `likesCount`, `talkingAboutCount` and `checkInsCount` are the exact figures Facebook publishes. `followersCount` and `followingCount` arrive as the rounded, localised text the page shows, `"39M followers"` in English and `"39 Mio. Follower"` when `language` is `de`. Compare with the numbers, display the strings.
236
+
237
+ **`language` changes the strings, not the numbers.** `category` moved from `Sportswear Store` to `Sportbekleidungsgeschäft` under `de`, while `likesCount` stayed a number. Pick the language for the reader, not for the maths.
238
+
239
+ **A page that does not exist, was deleted or is not public still answers 200.** The response then carries `error` instead of `profile`, and the call is billed. Test for `profile` before you read it. Groups and private personal accounts land here too.
240
+
241
+ **The first page of the feed holds one post, not three.** `postsPerPage` says 3, and pages two onward do return three, but the opening response carries the newest post alone alongside the profile and photos. Count what you got rather than what the field promises.
242
+
243
+ **Later pages drop `profile` and `photos`.** A call made with `nextPageToken` returns `posts` and `pagination` only. Keep the profile from the first response instead of expecting it again.
244
+
245
+ **The token is the only way forward.** `nextPage` is a page number for your own bookkeeping, and there is no parameter that takes it. Pass the previous `nextPageToken` unchanged, and stop when a response arrives without one.
246
+
247
+ **Contact details are what the page shows to the request, not a canonical record.** A global brand page can surface a regional phone number, and `website` is the display text while `websiteUrl` is the link. Read `owner.isConfirmed` before you treat the owner name as verified.
248
+
249
+ **Image URLs are Facebook CDN links with signed parameters.** They expire. Fetch what you need promptly and store the file, not the URL.
250
+
251
+ Results that carry data also carry a `requestMetadata.id` worth quoting in support.
252
+
253
+ ## Pricing, free tier and limits
254
+
255
+ The Facebook tool costs **10 credits per successful call**. Response size does not change the price, and a page of the feed costs the same as the opening call, so a thirty-post read is one call plus ten more.
256
+
257
+ The free tier is **1,000 credits every month with no card**, which is 100 Facebook calls at the base rate. It renews with the billing cycle, so a low-volume agent runs on the free tier indefinitely.
258
+
259
+ Paid plans start at **$49 a month** for 200,000 credits, which is 20,000 calls. The unit price falls with volume, from **$2.45 per 1,000 calls** on the entry plan to **$1.00** on Business, **$0.84** on Growth and **$0.74** on the largest [high-volume plans](https://hasdata.com/prices?utm_source=github&utm_medium=syndication&utm_campaign=facebook-mcp).
260
+
261
+ Your plan also sets concurrency. The free tier allows 1 request at a time, Startup 15, Business 30, Growth 50, and the high-volume plans run from 200 to 1,500. Retry on the 429 with a backoff in anything unattended, because an agent that enriches a list of pages will reach the ceiling before you do.
262
+
263
+ A request that comes back non-200 is not billed. A successful call that finds no page is still a call.
264
+
265
+ ## How it compares
266
+
267
+ Meta's Graph API is the official route to page data, and it is built for a different situation.
268
+
269
+ | | Meta Graph API | This server |
270
+ | :--- | :--- | :--- |
271
+ | Eligibility | A Meta developer app, an access token, and App Review for pages you do not manage | An API key |
272
+ | Pages you can read | Your own pages in full, others only through reviewed permissions | Any public page or profile |
273
+ | Setup | App creation, permissions, token handling, review | One header |
274
+ | Post reactions | Per-type counts on pages you manage | Per-type counts on any public page |
275
+ | Language of the record | Your app's locale | Any of 32, per call |
276
+ | Cost | Free within rate limits | Paid past the free tier, 10 credits a call |
277
+
278
+ The row that decides it is which pages you can read. The Graph API is the right tool for pages you administer, and its Page Public Content Access permission for everything else needs a business verification and a review with a stated use case. When the page is yours, use the Graph API, it is free and complete.
279
+
280
+ ## FAQ
281
+
282
+ ### Is there an official Facebook MCP server?
283
+
284
+ Meta does not publish one for reading public pages. This one is maintained by HasData and reads public Facebook pages.
285
+
286
+ ### What is a Facebook MCP server?
287
+
288
+ An MCP server exposes tools an AI client can call. This one turns a public Facebook page and its feed into JSON an agent can reason over, without a browser or a scraping library in your stack.
289
+
290
+ ### Do I need a Facebook account or a Meta developer app?
291
+
292
+ No. The only credential is your HasData key.
293
+
294
+ ### Can I read a personal profile or a group?
295
+
296
+ A public personal profile works the same way as a page. Private accounts and groups are not supported and come back with the not-available error.
297
+
298
+ ### Can I pass a numeric id instead of a username?
299
+
300
+ Yes. Both the `id` and the `pageId` a response returns resolve as `handle`, so a page reached once by name can be tracked by id afterwards.
301
+
302
+ ### How do I read the whole feed?
303
+
304
+ Call once with the handle, then keep calling with the `nextPageToken` from each response until a response arrives without one. Three posts come back per token.
305
+
306
+ ### Why is `followersCount` text while `likesCount` is a number?
307
+
308
+ Because that is how Facebook publishes them. The like count and the talking-about count are exact on the page, and the follower count is shown rounded and localised. The response passes both through as they are.
309
+
310
+ ### Can I use this together with other HasData APIs?
311
+
312
+ Yes. One key covers everything, and one endpoint serves them all through the `apis` parameter. Point a client at `?apis=facebook,instagram` to get both tool sets in one connection, or at [`mcp.hasdata.com/api/mcp`](https://docs.hasdata.com/mcp-server?utm_source=github&utm_medium=syndication&utm_campaign=facebook-mcp) for the full catalogue.
313
+
314
+ ### Is HasData affiliated with Meta or Facebook?
315
+
316
+ No. HasData is an independent service and is not affiliated with, endorsed by, or sponsored by Meta. Facebook is a trademark of its respective owner. The tools work with publicly available data only, and you are responsible for using the results in line with Meta's terms and the law that applies to you.
317
+
318
+ ### Compliance and personal data
319
+
320
+ A page record for a business is a business record, and a public personal profile is personal data in the plainest sense, with a name, a photo, a bio and a public feed. The tool does not tell the two apart, so your purpose has to. Keep to what your use case needs, do not build profiles of individuals you have no business with, and check your obligations under the GDPR, the CCPA and Meta's terms before storing anything. Contact details on a page are published for customers, and marketing to them is regulated separately again.
321
+
322
+ ## HasData links
323
+
324
+ - [Facebook Profile API documentation](https://docs.hasdata.com/apis/facebook/profile?utm_source=github&utm_medium=syndication&utm_campaign=facebook-mcp), the REST endpoint behind this tool
325
+ - [MCP server documentation](https://docs.hasdata.com/mcp-server?utm_source=github&utm_medium=syndication&utm_campaign=facebook-mcp)
326
+ - [Pricing](https://hasdata.com/prices?utm_source=github&utm_medium=syndication&utm_campaign=facebook-mcp)
327
+ - [Dashboard](https://app.hasdata.com/sign-up?utm_source=github&utm_medium=syndication&utm_campaign=facebook-mcp)
328
+
329
+ Other HasData MCP servers: [Instagram](https://github.com/HasData/instagram-mcp), [TikTok](https://github.com/HasData/tiktok-mcp), [YouTube](https://github.com/HasData/youtube-mcp), [Google Search](https://github.com/HasData/google-search-mcp), [Google Images](https://github.com/HasData/google-images-mcp), [Google Scholar](https://github.com/HasData/google-scholar-mcp), [Google Maps](https://github.com/HasData/google-maps-mcp), [Google Trends](https://github.com/HasData/google-trends-mcp), [Google Flights](https://github.com/HasData/google-flights-mcp), [Bing](https://github.com/HasData/bing-mcp), [DuckDuckGo](https://github.com/HasData/duckduckgo-mcp), [Amazon](https://github.com/HasData/amazon-mcp), [Walmart](https://github.com/HasData/walmart-mcp), [Shopify](https://github.com/HasData/shopify-mcp), [Yelp](https://github.com/HasData/yelp-mcp), [Yellow Pages](https://github.com/HasData/yellowpages-mcp), [Zillow](https://github.com/HasData/zillow-mcp), [Redfin](https://github.com/HasData/redfin-mcp), [Airbnb](https://github.com/HasData/airbnb-mcp), [Booking.com](https://github.com/HasData/booking-mcp), [Indeed](https://github.com/HasData/indeed-mcp), [Glassdoor](https://github.com/HasData/glassdoor-mcp), [Web Scraping](https://github.com/HasData/web-scraping-mcp).
330
+
331
+ ## Development
332
+
333
+ The launcher is a thin stdio bridge to the remote server, so there is nothing to build.
334
+
335
+ ```bash
336
+ npm install
337
+ HASDATA_API_KEY=your_key_here npm test
338
+ ```
339
+
340
+ The tests in `test/` assert the tool contract, the part that can break without a commit here. They check that `?apis=facebook` returns the one expected tool, that its name has not changed, that it still requires `handle` and carries a description, that `language` still offers the codes this README names, and that the key in use is actually accepted.
341
+
342
+ One test reads a live page and asserts the two things this README leans on: that `likesCount` is a number rather than display text, and that `pagination.nextPageToken` arrives, because without the token the feed walk this README documents does not exist. That call costs 10 credits, which is the price of a canary that can fail for the right reason.
343
+
344
+ The contract suite also runs weekly on a schedule, because the upstream tool list can change without anyone touching this repository.
345
+
346
+ ## Contributing
347
+
348
+ A tool table, a response sample or a documented behaviour that does not match reality is worth an issue. There is a template for exactly that. Pull requests are welcome for the same, and for anything in the launcher.
349
+
350
+ ## License
351
+
352
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
3
+ "maintainers": [
4
+ "HasData",
5
+ "sergey-ermakovich"
6
+ ]
7
+ }
@@ -0,0 +1,28 @@
1
+ """Thin launcher for HasData's hosted Facebook MCP server.
2
+
3
+ Connects an MCP client to the remote streamable-HTTP endpoint through mcp-proxy.
4
+ The server runs on HasData's infrastructure. This package only proxies stdio to it.
5
+ """
6
+ import os
7
+ import sys
8
+ import subprocess
9
+
10
+ URL = "https://mcp.hasdata.com/api/mcp?apis=facebook"
11
+
12
+
13
+ def main() -> None:
14
+ key = os.environ.get("HASDATA_API_KEY")
15
+ if not key:
16
+ sys.stderr.write(
17
+ "HASDATA_API_KEY is not set. Create a free key at https://app.hasdata.com "
18
+ "and set HASDATA_API_KEY.\n"
19
+ )
20
+ raise SystemExit(1)
21
+ args = [
22
+ sys.executable, "-m", "mcp_proxy", URL,
23
+ "--transport=streamablehttp",
24
+ "--headers", "x-api-key", key,
25
+ ]
26
+ rc = subprocess.call(args)
27
+ # subprocess returns -N when mcp_proxy is killed by signal N; map to 128+N.
28
+ raise SystemExit(rc if rc >= 0 else 128 - rc)
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ // Thin launcher: connects an MCP client to HasData's hosted Facebook MCP server
3
+ // (streamable HTTP) through the mcp-remote stdio bridge. The server runs remotely.
4
+ // This package only proxies, so nothing here scrapes anything.
5
+ import { spawn } from 'node:child_process';
6
+ import { createRequire } from 'node:module';
7
+ import { dirname, join } from 'node:path';
8
+
9
+ const URL = 'https://mcp.hasdata.com/api/mcp?apis=facebook';
10
+ const key = process.env.HASDATA_API_KEY;
11
+ if (!key) {
12
+ process.stderr.write('HASDATA_API_KEY is not set. Create a free key at https://app.hasdata.com and set HASDATA_API_KEY.\n');
13
+ process.exit(1);
14
+ }
15
+ // Resolve mcp-remote's CLI from its own package.json bin, so a future layout change
16
+ // or an exports map does not break a hardcoded deep path.
17
+ const require = createRequire(import.meta.url);
18
+ const pkg = require('mcp-remote/package.json');
19
+ const proxy = join(dirname(require.resolve('mcp-remote/package.json')), pkg.bin['mcp-remote']);
20
+ const child = spawn(process.execPath, [proxy, URL, '--header', `x-api-key:${key}`], { stdio: 'inherit' });
21
+ child.on('exit', (code, signal) => {
22
+ if (signal) process.kill(process.pid, signal);
23
+ else process.exit(code ?? 0);
24
+ });
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
3
+ "mcpServers": {
4
+ "facebook": {
5
+ "type": "streamable-http",
6
+ "url": "https://mcp.hasdata.com/api/mcp?apis=facebook",
7
+ "headers": {
8
+ "x-api-key": "YOUR_HASDATA_API_KEY"
9
+ }
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@hasdata/facebook-mcp",
3
+ "mcpName": "com.hasdata/facebook",
4
+ "version": "1.0.0",
5
+ "description": "MCP server for Facebook through HasData's hosted API: a public page or profile by handle with likes, followers, contact details and the post feed with reactions, paged three posts at a time. No Meta developer app. 1,000 free credits every month.",
6
+ "type": "module",
7
+ "bin": {
8
+ "hasdata-facebook-mcp": "index.mjs"
9
+ },
10
+ "files": [
11
+ "index.mjs",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "test": "node --test"
17
+ },
18
+ "dependencies": {
19
+ "mcp-remote": "^0.1.43"
20
+ },
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/HasData/facebook-mcp.git"
28
+ },
29
+ "homepage": "https://docs.hasdata.com/apis/facebook/profile",
30
+ "keywords": [
31
+ "mcp",
32
+ "facebook",
33
+ "facebook-mcp",
34
+ "social-media",
35
+ "marketing",
36
+ "lead-enrichment",
37
+ "model-context-protocol",
38
+ "hasdata",
39
+ "scraper"
40
+ ]
41
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
+ "name": "hasdata-facebook-mcp",
4
+ "version": "1.0.0",
5
+ "description": "Facebook MCP server for Cursor. Public pages and profiles by handle with likes, followers, talking-about counts, contact details and the post feed with reactions, as structured JSON. Remote MCP server hosted by HasData, connects over streamable HTTP with your HasData API key.",
6
+ "author": {
7
+ "name": "HasData",
8
+ "url": "https://hasdata.com"
9
+ },
10
+ "homepage": "https://docs.hasdata.com/apis/facebook/profile",
11
+ "repository": "https://github.com/HasData/facebook-mcp",
12
+ "license": "MIT",
13
+ "keywords": [
14
+ "mcp",
15
+ "facebook",
16
+ "facebook-mcp",
17
+ "social-media",
18
+ "marketing",
19
+ "lead-enrichment",
20
+ "model-context-protocol",
21
+ "hasdata",
22
+ "scraper"
23
+ ]
24
+ }
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "hasdata-facebook-mcp"
3
+ version = "1.0.0"
4
+ description = "MCP server for Facebook through HasData's hosted API. 1,000 free credits every month."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "MIT" }
8
+ keywords = ["mcp","facebook","social-media","marketing","lead-enrichment","model-context-protocol","hasdata"]
9
+ # mcp is pinned below 2 because mcp-proxy 0.12.0 imports request_ctx, removed in the mcp 2.x SDK.
10
+ dependencies = ["mcp-proxy>=0.12.0", "mcp>=1.17,<2"]
11
+
12
+ [project.urls]
13
+ Homepage = "https://docs.hasdata.com/apis/facebook/profile"
14
+ Repository = "https://github.com/HasData/facebook-mcp"
15
+
16
+ [project.scripts]
17
+ hasdata-facebook-mcp = "hasdata_facebook_mcp:main"
18
+
19
+ [build-system]
20
+ requires = ["hatchling"]
21
+ build-backend = "hatchling.build"
22
+
23
+ [tool.hatch.build.targets.wheel]
24
+ packages = ["hasdata_facebook_mcp"]
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "com.hasdata/facebook",
4
+ "title": "HasData Facebook",
5
+ "description": "Public Facebook pages and profiles with likes, followers, contact details and posts, as JSON.",
6
+ "version": "1.0.0",
7
+ "websiteUrl": "https://docs.hasdata.com/apis/facebook/profile",
8
+ "repository": {
9
+ "url": "https://github.com/HasData/facebook-mcp",
10
+ "source": "github"
11
+ },
12
+ "remotes": [
13
+ {
14
+ "type": "streamable-http",
15
+ "url": "https://mcp.hasdata.com/api/mcp?apis=facebook",
16
+ "headers": [
17
+ {
18
+ "name": "x-api-key",
19
+ "description": "HasData API key, created at app.hasdata.com. 1,000 free credits every month, no card.",
20
+ "isRequired": true,
21
+ "isSecret": true
22
+ }
23
+ ]
24
+ }
25
+ ],
26
+ "packages": [
27
+ {
28
+ "registryType": "npm",
29
+ "identifier": "@hasdata/facebook-mcp",
30
+ "version": "1.0.0",
31
+ "transport": {
32
+ "type": "stdio"
33
+ },
34
+ "environmentVariables": [
35
+ {
36
+ "name": "HASDATA_API_KEY",
37
+ "description": "HasData API key, created at app.hasdata.com. 1,000 free credits every month, no card.",
38
+ "isRequired": true,
39
+ "isSecret": true
40
+ }
41
+ ]
42
+ }
43
+ ]
44
+ }
@@ -0,0 +1,158 @@
1
+ // Tool contract test.
2
+ //
3
+ // The README promises one tool with a specific name and parameter set. The upstream list can
4
+ // change without a single commit here, and the README would start lying silently. These checks
5
+ // catch that before a user does.
6
+ //
7
+ // One live call serves two checks. Listing tools accepts any non-empty key, so a contract check
8
+ // that only lists tools stays green with a revoked or mistyped key. The same response also
9
+ // carries the numeric like count and the feed token the README leans on, so both are asserted
10
+ // against one call. That call costs 10 credits, which is the price of a canary that can fail
11
+ // for the right reason.
12
+ //
13
+ // Run: HASDATA_API_KEY=your_key_here npm test
14
+
15
+ import { test } from 'node:test';
16
+ import assert from 'node:assert/strict';
17
+
18
+ const ENDPOINT = 'https://mcp.hasdata.com/api/mcp?apis=facebook';
19
+ const KEY = process.env.HASDATA_API_KEY;
20
+ const TIMEOUT_MS = 45_000;
21
+
22
+ const TOOL = 'hasdata_facebook_profile_getFacebookProfile';
23
+ const REQUIRED = ['handle'];
24
+ const PARAMS = ['language', 'nextPageToken'];
25
+ // Language codes the README names.
26
+ const LANGUAGES = ['en', 'de', 'pt', 'zh-hans'];
27
+
28
+ // A streamable HTTP body arrives either as plain JSON or as server-sent events. One SSE event
29
+ // can span several data: lines, several events can share one response, and a server is free to
30
+ // send progress notifications before the answer. So collect every event and pick the message
31
+ // carrying our request id instead of trusting the first data: line.
32
+ function parseRpc(raw, id) {
33
+ const trimmed = raw.trim();
34
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) return JSON.parse(trimmed);
35
+
36
+ const messages = [];
37
+ for (const event of trimmed.split(/\r?\n\r?\n+/)) {
38
+ const data = event
39
+ .split(/\r?\n/)
40
+ .filter((l) => l.startsWith('data:'))
41
+ .map((l) => l.slice(5).replace(/^ /, ''))
42
+ .join('\n');
43
+ if (!data || data === '[DONE]') continue;
44
+ try {
45
+ messages.push(JSON.parse(data));
46
+ } catch {
47
+ // A keep-alive or a partial event is not our response.
48
+ }
49
+ }
50
+ assert.ok(messages.length, `no JSON-RPC message in the response: ${raw.slice(0, 300)}`);
51
+ const match = messages.find((m) => m.id === id);
52
+ assert.ok(match, `no message with id ${id} in the response: ${raw.slice(0, 300)}`);
53
+ return match;
54
+ }
55
+
56
+ let nextId = 1;
57
+
58
+ async function rpc(method, params = {}) {
59
+ // The CI key sits on the free plan, where concurrency is 1. When several of
60
+ // these repos are pushed at once their contract runs collide, and HasData
61
+ // answers 429 with code concurrency_limit straight away rather than queueing.
62
+ // That is a plan limit, not a broken contract, so the call is retried before
63
+ // the test gives up. A 401 still fails on the first attempt.
64
+ for (let attempt = 1; ; attempt++) {
65
+ const id = nextId++;
66
+ const res = await fetch(ENDPOINT, {
67
+ method: 'POST',
68
+ headers: {
69
+ 'x-api-key': KEY,
70
+ 'Content-Type': 'application/json',
71
+ // The server answers over streamable HTTP, so accept both a plain body and a stream.
72
+ Accept: 'application/json, text/event-stream',
73
+ },
74
+ body: JSON.stringify({ jsonrpc: '2.0', id, method, params }),
75
+ signal: AbortSignal.timeout(TIMEOUT_MS),
76
+ });
77
+ assert.equal(res.status, 200, `${method} returned ${res.status}`);
78
+ const raw = await res.text();
79
+ if (raw.includes('concurrency_limit') && attempt < 5) {
80
+ await new Promise((r) => setTimeout(r, attempt * 4000));
81
+ continue;
82
+ }
83
+ return { raw, body: parseRpc(raw, id) };
84
+ }
85
+ }
86
+
87
+ // One network round trip for every test that needs the list.
88
+ let toolsPromise;
89
+ function listTools() {
90
+ toolsPromise ??= rpc('tools/list').then(({ body }) => {
91
+ assert.ok(body.result?.tools, 'the response carried no result.tools');
92
+ return body.result.tools;
93
+ });
94
+ return toolsPromise;
95
+ }
96
+
97
+ // One paid round trip, shared by the checks that need a real answer.
98
+ let profilePromise;
99
+ function liveProfile() {
100
+ profilePromise ??= rpc('tools/call', { name: TOOL, arguments: { handle: 'nike' } });
101
+ return profilePromise;
102
+ }
103
+
104
+ const live = { skip: KEY ? false : 'HASDATA_API_KEY is not set, skipping the live checks' };
105
+
106
+ test('apis=facebook exposes the documented tool and nothing else', live, async () => {
107
+ const tools = await listTools();
108
+ const names = tools.map((t) => t.name).sort().join(', ');
109
+ assert.equal(tools.length, 1, `expected 1 tool, got ${tools.length}: ${names}`);
110
+ assert.equal(tools[0].name, TOOL, `the tool is now called ${tools[0].name}`);
111
+ });
112
+
113
+ test('the tool still requires a handle and carries a description', live, async () => {
114
+ const [tool] = await listTools();
115
+ const required = tool.inputSchema?.required ?? [];
116
+ for (const param of REQUIRED) {
117
+ assert.ok(required.includes(param), `${TOOL} should require ${param}, declares: ${required.join(', ') || 'nothing'}`);
118
+ }
119
+ assert.ok((tool.description || '').trim().length > 20, `${TOOL} has an empty or near-empty description`);
120
+ });
121
+
122
+ test('the parameters and language codes the README documents are still in the schema', live, async () => {
123
+ const [tool] = await listTools();
124
+ const props = tool.inputSchema?.properties ?? {};
125
+ for (const param of PARAMS) {
126
+ assert.ok(props[param], `${TOOL} no longer accepts ${param}`);
127
+ }
128
+ const offered = props.language?.enum ?? [];
129
+ for (const code of LANGUAGES) {
130
+ assert.ok(offered.includes(code), `language no longer accepts ${code}, offers: ${offered.join(', ') || 'no enum'}`);
131
+ }
132
+ });
133
+
134
+ test('the key is accepted by HasData', live, async () => {
135
+ const { raw } = await liveProfile();
136
+ assert.ok(!raw.includes('401 Unauthorized'), 'HasData rejected the key');
137
+ assert.ok(!raw.includes('"isError":true'), `the tool call failed: ${raw.slice(0, 300)}`);
138
+ });
139
+
140
+ // The README tells readers to compare on likesCount because it is a number, and to walk the
141
+ // feed with pagination.nextPageToken. Both claims are about the live response shape, and a
142
+ // parser change that broke either would leave a green tools list behind it.
143
+ test('a live page still returns a numeric like count and a feed token', live, async () => {
144
+ const { body } = await liveProfile();
145
+ const text = body.result?.content?.[0]?.text ?? '';
146
+ const payload = JSON.parse(text);
147
+ const json = payload.json;
148
+
149
+ assert.ok(json?.profile, `no profile object in the response: ${text.slice(0, 300)}`);
150
+ assert.equal(typeof json.profile.likesCount, 'number', `likesCount is no longer a number: ${JSON.stringify(json.profile.likesCount)}`);
151
+ assert.ok(json.profile.name, 'the profile carried no name');
152
+
153
+ assert.ok(Array.isArray(json.posts) && json.posts.length, 'the opening response carried no posts');
154
+ assert.ok(
155
+ json.pagination?.nextPageToken,
156
+ 'no pagination.nextPageToken in the opening response. The README documents the feed walk on that token, so revisit it or drop this test.'
157
+ );
158
+ });