@praise25/meta-mcp-server 0.1.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 +292 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.js +46 -0
- package/dist/constants.d.ts +21 -0
- package/dist/constants.js +28 -0
- package/dist/context.d.ts +9 -0
- package/dist/context.js +8 -0
- package/dist/errors.d.ts +41 -0
- package/dist/errors.js +90 -0
- package/dist/helpers/cache.d.ts +6 -0
- package/dist/helpers/cache.js +28 -0
- package/dist/helpers/format.d.ts +17 -0
- package/dist/helpers/format.js +28 -0
- package/dist/helpers/graph-client.d.ts +56 -0
- package/dist/helpers/graph-client.js +169 -0
- package/dist/helpers/schema.d.ts +30 -0
- package/dist/helpers/schema.js +69 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +36 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +18 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +14 -0
- package/dist/tools/ads/get-account.d.ts +29 -0
- package/dist/tools/ads/get-account.js +45 -0
- package/dist/tools/ads/get-creative.d.ts +29 -0
- package/dist/tools/ads/get-creative.js +37 -0
- package/dist/tools/ads/get-insights.d.ts +129 -0
- package/dist/tools/ads/get-insights.js +151 -0
- package/dist/tools/ads/list-accounts.d.ts +54 -0
- package/dist/tools/ads/list-accounts.js +59 -0
- package/dist/tools/ads/list-ads.d.ts +53 -0
- package/dist/tools/ads/list-ads.js +59 -0
- package/dist/tools/ads/list-adsets.d.ts +49 -0
- package/dist/tools/ads/list-adsets.js +54 -0
- package/dist/tools/ads/list-campaigns.d.ts +45 -0
- package/dist/tools/ads/list-campaigns.js +64 -0
- package/dist/tools/ads/list-custom-audiences.d.ts +41 -0
- package/dist/tools/ads/list-custom-audiences.js +41 -0
- package/dist/tools/business/list-assets.d.ts +37 -0
- package/dist/tools/business/list-assets.js +136 -0
- package/dist/tools/business/list-businesses.d.ts +37 -0
- package/dist/tools/business/list-businesses.js +81 -0
- package/dist/tools/business/list-system-users.d.ts +41 -0
- package/dist/tools/business/list-system-users.js +73 -0
- package/dist/tools/catalog/get-diagnostics.d.ts +29 -0
- package/dist/tools/catalog/get-diagnostics.js +26 -0
- package/dist/tools/catalog/list-products.d.ts +45 -0
- package/dist/tools/catalog/list-products.js +49 -0
- package/dist/tools/catalog/list.d.ts +54 -0
- package/dist/tools/catalog/list.js +48 -0
- package/dist/tools/instagram/get-account.d.ts +29 -0
- package/dist/tools/instagram/get-account.js +34 -0
- package/dist/tools/instagram/get-audience-demographics.d.ts +41 -0
- package/dist/tools/instagram/get-audience-demographics.js +46 -0
- package/dist/tools/instagram/get-media-insights.d.ts +29 -0
- package/dist/tools/instagram/get-media-insights.js +43 -0
- package/dist/tools/instagram/list-accounts.d.ts +33 -0
- package/dist/tools/instagram/list-accounts.js +63 -0
- package/dist/tools/instagram/list-media.d.ts +41 -0
- package/dist/tools/instagram/list-media.js +42 -0
- package/dist/tools/meta/graph-read.d.ts +33 -0
- package/dist/tools/meta/graph-read.js +71 -0
- package/dist/tools/overview/business-overview.d.ts +49 -0
- package/dist/tools/overview/business-overview.js +188 -0
- package/dist/tools/pages/get-insights.d.ts +41 -0
- package/dist/tools/pages/get-insights.js +49 -0
- package/dist/tools/pages/get-post-insights.d.ts +29 -0
- package/dist/tools/pages/get-post-insights.js +36 -0
- package/dist/tools/pages/get.d.ts +29 -0
- package/dist/tools/pages/get.js +50 -0
- package/dist/tools/pages/list-posts.d.ts +53 -0
- package/dist/tools/pages/list-posts.js +54 -0
- package/dist/tools/pages/list-reviews.d.ts +41 -0
- package/dist/tools/pages/list-reviews.js +37 -0
- package/dist/tools/pages/list-videos.d.ts +41 -0
- package/dist/tools/pages/list-videos.js +40 -0
- package/dist/tools/pages/list.d.ts +41 -0
- package/dist/tools/pages/list.js +39 -0
- package/dist/tools/pixels/get-stats.d.ts +41 -0
- package/dist/tools/pixels/get-stats.js +34 -0
- package/dist/tools/pixels/list.d.ts +41 -0
- package/dist/tools/pixels/list.js +38 -0
- package/dist/tools/register.d.ts +3 -0
- package/dist/tools/register.js +82 -0
- package/dist/tools/shared.d.ts +20 -0
- package/dist/tools/shared.js +55 -0
- package/dist/tools/token/health.d.ts +17 -0
- package/dist/tools/token/health.js +59 -0
- package/dist/tools/token/inspect.d.ts +26 -0
- package/dist/tools/token/inspect.js +88 -0
- package/dist/tools/whatsapp/get-analytics.d.ts +57 -0
- package/dist/tools/whatsapp/get-analytics.js +66 -0
- package/dist/tools/whatsapp/list-phone-numbers.d.ts +41 -0
- package/dist/tools/whatsapp/list-phone-numbers.js +35 -0
- package/dist/tools/whatsapp/list-templates.d.ts +45 -0
- package/dist/tools/whatsapp/list-templates.js +44 -0
- package/dist/tools/whatsapp/list-wabas.d.ts +54 -0
- package/dist/tools/whatsapp/list-wabas.js +48 -0
- package/dist/types/meta.d.ts +46 -0
- package/dist/types/meta.js +1 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CashToken Rewards
|
|
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,292 @@
|
|
|
1
|
+
# meta-mcp-server
|
|
2
|
+
|
|
3
|
+
> **Read-only Model Context Protocol server for Meta Business Manager.** Plug-in for AI assistants ("ChatGPT for marketing insights") that surfaces Pages, Instagram Business, Marketing API ad insights, Pixels, Commerce catalogs, and WhatsApp Business data — all read-only by construction.
|
|
4
|
+
|
|
5
|
+
[](#read-only-by-construction)
|
|
6
|
+
[](./governance/standards/sdgp-main.md)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why this exists
|
|
12
|
+
|
|
13
|
+
CashToken Marketing operates several Pages, Instagram Business accounts, Meta ad accounts, Pixels and (in future) Catalogs / WhatsApp Business assets across the `Hodusoft` Business Manager. Insight retrieval today is fragmented across Business Suite UIs, Ads Manager exports, and ad-hoc Graph API calls. This MCP server consolidates **read-only** access into a single tool surface that any MCP-aware AI assistant can plug into — turning fragmented UIs into one conversational interface for the marketing team.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Read-only by construction
|
|
18
|
+
|
|
19
|
+
This server cannot write to Meta. Period. Four layers of defence:
|
|
20
|
+
|
|
21
|
+
1. **HTTP interceptor** — every axios request passes through a guard that throws `ReadOnlyViolationError` if the method isn't `GET`, before the request leaves the process. ([`src/helpers/graph-client.ts`](./src/helpers/graph-client.ts))
|
|
22
|
+
2. **No write API surface** — the `GraphClient` class exposes only `get()` and `getAllPages()`. No `post()`, `put()`, `patch()`, or `delete()` exist anywhere in `src/`.
|
|
23
|
+
3. **All tools annotated** `readOnlyHint: true, destructiveHint: false` — MCP clients surface this to end users.
|
|
24
|
+
4. **Belt-and-braces token scopes** (operator responsibility) — issue the system-user token with read-only scopes (`ads_read`, `pages_read_engagement`, `read_insights`, `instagram_basic`, `instagram_manage_insights`). Even if the server were compromised, the token itself cannot write. See [`ADR-20260421-Read-Only-HTTP-Enforcement.md`](./governance/project-docs/adr/ADR-20260421-Read-Only-HTTP-Enforcement.md).
|
|
25
|
+
|
|
26
|
+
Verify any time:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm run test:readonly
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Tools (36)
|
|
35
|
+
|
|
36
|
+
| Domain | Tools |
|
|
37
|
+
|---|---|
|
|
38
|
+
| **Discovery** (6) | `meta_token_inspect`, `meta_health_check`, `meta_graph_read`, `meta_business_list`, `meta_business_list_assets`, `meta_business_list_system_users` |
|
|
39
|
+
| **Ads / Marketing API** (8) | `meta_ads_list_accounts`, `meta_ads_get_account`, `meta_ads_list_campaigns`, `meta_ads_list_adsets`, `meta_ads_list_ads`, `meta_ads_get_insights` ⭐, `meta_ads_get_creative`, `meta_ads_list_custom_audiences` |
|
|
40
|
+
| **Pages** (7) | `meta_page_list`, `meta_page_get`, `meta_page_list_posts`, `meta_page_get_post_insights`, `meta_page_get_insights`, `meta_page_list_reviews`, `meta_page_list_videos` |
|
|
41
|
+
| **Instagram** (5) | `meta_ig_list_accounts`, `meta_ig_get_account`, `meta_ig_list_media`, `meta_ig_get_media_insights`, `meta_ig_get_audience_demographics` |
|
|
42
|
+
| **Pixels** (2) | `meta_pixel_list`, `meta_pixel_get_stats` |
|
|
43
|
+
| **Catalog** (3) | `meta_catalog_list`, `meta_catalog_list_products`, `meta_catalog_get_diagnostics` |
|
|
44
|
+
| **WhatsApp** (4) | `meta_whatsapp_list_wabas`, `meta_whatsapp_list_phone_numbers`, `meta_whatsapp_list_templates`, `meta_whatsapp_get_analytics` |
|
|
45
|
+
| **Eagle's-eye** (1) | `meta_business_overview` ⭐⭐⭐ — single-call consolidated snapshot across the whole business |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Quickstart
|
|
50
|
+
|
|
51
|
+
### 1. Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git clone git@github.com:feladeveloper/meta-mcp-server.git
|
|
55
|
+
cd meta-mcp-server
|
|
56
|
+
npm install
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. Configure environment
|
|
60
|
+
|
|
61
|
+
Copy `.env.example` → `.env` and fill in:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
META_ACCESS_TOKEN=... # System-user token (see Token Provisioning below)
|
|
65
|
+
META_APP_SECRET=... # App secret of the app that issued the token (Hodusoft app: 193481170220592)
|
|
66
|
+
META_API_VERSION=v23.0
|
|
67
|
+
META_CACHE_TTL_SECONDS=120
|
|
68
|
+
META_MAX_AUTO_PAGES=5
|
|
69
|
+
LOG_LEVEL=info
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Optional allowlists (if set, the server refuses to operate on IDs outside the allowlist):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
META_ALLOWED_BUSINESS_IDS=133767790806312
|
|
76
|
+
META_ALLOWED_AD_ACCOUNT_IDS=act_146517954996436,...
|
|
77
|
+
META_ALLOWED_PAGE_IDS=138368686823692,...
|
|
78
|
+
META_ALLOWED_IG_USER_IDS=17841406467396631,...
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 3. Build and run
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npm run build
|
|
85
|
+
node dist/index.js # stdio MCP server
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Or in development:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm run dev
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
For an MCP Inspector session against the built server:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npm run inspect
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 4. Wire into a client
|
|
101
|
+
|
|
102
|
+
Example (Claude Desktop or any MCP client) — fetches the published package on demand via `npx`:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"mcpServers": {
|
|
107
|
+
"meta": {
|
|
108
|
+
"command": "npx",
|
|
109
|
+
"args": ["-y", "@praise25/meta-mcp-server"],
|
|
110
|
+
"env": {
|
|
111
|
+
"META_ACCESS_TOKEN": "...",
|
|
112
|
+
"META_APP_SECRET": "..."
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
For a global install (`npm install -g @praise25/meta-mcp-server`), use the bin directly:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"mcpServers": {
|
|
124
|
+
"meta": {
|
|
125
|
+
"command": "meta-business-manager-mcp-server",
|
|
126
|
+
"env": {
|
|
127
|
+
"META_ACCESS_TOKEN": "...",
|
|
128
|
+
"META_APP_SECRET": "..."
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
For local development against a clone of this repo:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"mcpServers": {
|
|
140
|
+
"meta": {
|
|
141
|
+
"command": "node",
|
|
142
|
+
"args": ["/absolute/path/to/meta-mcp-server/dist/index.js"],
|
|
143
|
+
"env": {
|
|
144
|
+
"META_ACCESS_TOKEN": "...",
|
|
145
|
+
"META_APP_SECRET": "..."
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Token Provisioning (Cashtoken-specific)
|
|
155
|
+
|
|
156
|
+
The system user `AI_Insights_Reader` (id `122093391782492654`) under the `Hodusoft` business (id `133767790806312`) is the canonical identity for this server. Procedure to issue / rotate its token:
|
|
157
|
+
|
|
158
|
+
1. Business Settings → System Users → `AI_Insights_Reader` → **Generate New Token**
|
|
159
|
+
2. Pick the **Hodusoft** app (id `193481170220592`)
|
|
160
|
+
3. In the scope picker, untick everything, then tick:
|
|
161
|
+
- `business_management`, `ads_management` (Meta only exposes management here; server still blocks writes), `pages_read_engagement`, `pages_read_user_content`, `read_insights`, `instagram_basic`, `instagram_manage_insights`, `whatsapp_business_management`, `catalog_management`
|
|
162
|
+
4. Copy the token (Meta only shows it once)
|
|
163
|
+
5. Paste over `META_ACCESS_TOKEN` in `.env`
|
|
164
|
+
6. Verify with `meta_health_check` and `meta_token_inspect`
|
|
165
|
+
|
|
166
|
+
Token TTL is ~60 days. Set a calendar reminder; rotation procedure documented in [`/governance/project-docs/runbook.md`](./governance/project-docs/runbook.md).
|
|
167
|
+
|
|
168
|
+
See also: [`ADR-20260421-System-User-Token-Pattern.md`](./governance/project-docs/adr/ADR-20260421-System-User-Token-Pattern.md).
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Repository layout
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
meta-mcp-server/
|
|
176
|
+
├── CLAUDE.md # AI agent rules (governance)
|
|
177
|
+
├── .cursorrules # Cursor agent rules
|
|
178
|
+
├── .github/
|
|
179
|
+
│ ├── copilot-instructions.md # Copilot agent rules
|
|
180
|
+
│ ├── ISSUE_TEMPLATE/
|
|
181
|
+
│ └── workflows/ # CI/CD (GitHub Actions, Sentinel status check)
|
|
182
|
+
├── .claude/skills/ # 23 governance skills (scaffolding, review, git-ops, …)
|
|
183
|
+
├── .sentinelrc # Sentinel governance plugin config
|
|
184
|
+
├── CHANGELOG.md # SemVer release history
|
|
185
|
+
├── README.md
|
|
186
|
+
├── governance/ # Governance assets — see /governance/standards/
|
|
187
|
+
│ ├── standards/ # SDGP policies, coding standards
|
|
188
|
+
│ ├── templates/ # Doc templates (specs, ADRs, deviations, project docs)
|
|
189
|
+
│ └── project-docs/ # Project documents
|
|
190
|
+
│ ├── 1-vision-doc.md
|
|
191
|
+
│ ├── 2-brd.md
|
|
192
|
+
│ ├── 3-prd.md
|
|
193
|
+
│ ├── 5-tad.md
|
|
194
|
+
│ ├── runbook.md
|
|
195
|
+
│ ├── solution-doc-architecture.md
|
|
196
|
+
│ ├── specs/ # Feature specs
|
|
197
|
+
│ ├── adr/ # Architecture Decision Records
|
|
198
|
+
│ └── deviations/ # Governance deviation logs
|
|
199
|
+
├── src/ # Implementation (see TAD)
|
|
200
|
+
│ ├── index.ts # stdio entrypoint
|
|
201
|
+
│ ├── server.ts # MCP server wiring
|
|
202
|
+
│ ├── config.ts # env + allowlists
|
|
203
|
+
│ ├── errors.ts # MetaError, ReadOnlyViolationError
|
|
204
|
+
│ ├── logger.ts # pino, stderr only, redacts secrets
|
|
205
|
+
│ ├── constants.ts
|
|
206
|
+
│ ├── context.ts
|
|
207
|
+
│ ├── helpers/
|
|
208
|
+
│ │ ├── graph-client.ts # GET-only axios client + retry + cache + appsecret_proof
|
|
209
|
+
│ │ ├── cache.ts # LRU TTL cache
|
|
210
|
+
│ │ ├── format.ts # JSON / Markdown response formatting
|
|
211
|
+
│ │ └── schema.ts # Shared Zod shapes (pagination, date presets, IDs)
|
|
212
|
+
│ ├── tools/ # 36 tool implementations grouped by domain
|
|
213
|
+
│ │ ├── token/ meta/ business/ ads/ pages/ instagram/ pixels/ catalog/ whatsapp/ overview/
|
|
214
|
+
│ │ ├── shared.ts # runList / runGet / errorResult helpers
|
|
215
|
+
│ │ └── register.ts # Centralized tool registration
|
|
216
|
+
│ └── types/
|
|
217
|
+
└── tests/
|
|
218
|
+
└── read-only-guard.mjs # Runtime proof that POST/PUT/PATCH/DELETE are blocked
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Governance
|
|
224
|
+
|
|
225
|
+
This project is initialized from the [`cashtokenrewards/project-governance-template`](https://github.com/cashtokenrewards/project-governance-template) and follows the **Software Development Governance Policy (SDGP)** in [`/governance/standards/sdgp-main.md`](./governance/standards/sdgp-main.md).
|
|
226
|
+
|
|
227
|
+
**Three absolute rules:**
|
|
228
|
+
1. No feature is built without an approved spec. ([`/governance/project-docs/specs/`](./governance/project-docs/specs/))
|
|
229
|
+
2. No ADR is written without a parent feature spec. ([`/governance/project-docs/adr/`](./governance/project-docs/adr/))
|
|
230
|
+
3. No implementation begins without the spec and all required ADRs approved.
|
|
231
|
+
|
|
232
|
+
The current implementation (commit zero) was bootstrapped against an initial pass of governance docs:
|
|
233
|
+
|
|
234
|
+
| Doc | Path |
|
|
235
|
+
|---|---|
|
|
236
|
+
| Vision | [`governance/project-docs/1-vision-doc.md`](./governance/project-docs/1-vision-doc.md) |
|
|
237
|
+
| BRD | [`governance/project-docs/2-brd.md`](./governance/project-docs/2-brd.md) |
|
|
238
|
+
| PRD | [`governance/project-docs/3-prd.md`](./governance/project-docs/3-prd.md) |
|
|
239
|
+
| TAD | [`governance/project-docs/5-tad.md`](./governance/project-docs/5-tad.md) |
|
|
240
|
+
| Runbook | [`governance/project-docs/runbook.md`](./governance/project-docs/runbook.md) |
|
|
241
|
+
| Specs | [`governance/project-docs/specs/`](./governance/project-docs/specs/) |
|
|
242
|
+
| ADRs | [`governance/project-docs/adr/`](./governance/project-docs/adr/) |
|
|
243
|
+
| Deviations | [`governance/project-docs/deviations/`](./governance/project-docs/deviations/) |
|
|
244
|
+
|
|
245
|
+
**Branch model:** Gitflow. `main` (production), `dev` (integration). Short-lived branches: `feature-`, `fix-`, `release-`, `hotfix-`, `docs-`. All merges `--no-ff`. See [`/governance/standards/sdgp-main.md`](./governance/standards/sdgp-main.md) §7.4.
|
|
246
|
+
|
|
247
|
+
**AI agent rules:** [`CLAUDE.md`](./CLAUDE.md), [`.cursorrules`](./.cursorrules), [`.github/copilot-instructions.md`](./.github/copilot-instructions.md). Sentinel keeps these in sync with the central governance config.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Sentinel
|
|
252
|
+
|
|
253
|
+
This repository is tracked by the [Sentinel governance plugin](https://github.com/feladeveloper/sentinel-claude-plugin). Configuration lives at [`.sentinelrc`](./.sentinelrc). On any clone:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
export SENTINEL_GITHUB_TOKEN="ghp_..." # Personal access token with repo:read
|
|
257
|
+
sentinel sync # Pull latest org-level governance into CLAUDE.md
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`/sentinel-sync` and `/sentinel-status` slash commands are also available inside Claude Code once the plugin is installed.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Scripts
|
|
265
|
+
|
|
266
|
+
| Script | What it does |
|
|
267
|
+
|---|---|
|
|
268
|
+
| `npm run build` | Clean + compile TypeScript → `dist/` |
|
|
269
|
+
| `npm run dev` | Run with `tsx` (no build step) |
|
|
270
|
+
| `npm run start` | Run built `dist/index.js` |
|
|
271
|
+
| `npm run inspect` | Build + open MCP Inspector |
|
|
272
|
+
| `npm run check:types` | `tsc --noEmit` |
|
|
273
|
+
| `npm run test:readonly` | Build + runtime proof that POST/PUT/PATCH/DELETE are blocked by the Graph client |
|
|
274
|
+
| `npm test` | (placeholder for jest suite — see [`SPEC-07-eval-suite.md`](./governance/project-docs/specs/) when added) |
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Status
|
|
279
|
+
|
|
280
|
+
| Aspect | State |
|
|
281
|
+
|---|---|
|
|
282
|
+
| Implementation | **v0.1.0 — bootstrapped, 36 tools, build clean, read-only guard verified** |
|
|
283
|
+
| Governance docs | Initial pass — Vision / BRD / PRD / TAD / Runbook / 3 ADRs / 1 deviation drafted |
|
|
284
|
+
| App-level (Meta) | Hodusoft app in **development tier** for Marketing API. Standard Access via App Review pending. |
|
|
285
|
+
| Asset coverage | All 3 Pages discoverable; 1 Page (CashToken) currently assigned to AI_Insights_Reader; 5 ad accounts visible; 3 Pixels visible; 1 IG (cashtokenhq) discovered via Page link |
|
|
286
|
+
| Open scopes | `read_insights`, `instagram_manage_insights`, `whatsapp_business_management`, `catalog_management` may need to be added to the Hodusoft app before they appear in the token picker |
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## License
|
|
291
|
+
|
|
292
|
+
MIT — see [LICENSE](./LICENSE).
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
appSecret: string | undefined;
|
|
4
|
+
apiVersion: string;
|
|
5
|
+
httpTimeoutMs: number;
|
|
6
|
+
cacheTtlSeconds: number;
|
|
7
|
+
maxAutoPages: number;
|
|
8
|
+
allowedBusinessIds: Set<string> | null;
|
|
9
|
+
allowedAdAccountIds: Set<string> | null;
|
|
10
|
+
allowedPageIds: Set<string> | null;
|
|
11
|
+
allowedIgUserIds: Set<string> | null;
|
|
12
|
+
logLevel: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function loadConfig(): Config;
|
|
15
|
+
export declare function assertAllowed(kind: "business" | "ad_account" | "page" | "ig_user", id: string, config: Config): void;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { DEFAULT_API_VERSION } from "./constants.js";
|
|
2
|
+
function parseAllowlist(raw) {
|
|
3
|
+
if (!raw || !raw.trim())
|
|
4
|
+
return null;
|
|
5
|
+
const items = raw
|
|
6
|
+
.split(",")
|
|
7
|
+
.map((s) => s.trim())
|
|
8
|
+
.filter(Boolean);
|
|
9
|
+
return items.length ? new Set(items) : null;
|
|
10
|
+
}
|
|
11
|
+
function parseNumber(raw, fallback) {
|
|
12
|
+
if (raw == null || raw === "")
|
|
13
|
+
return fallback;
|
|
14
|
+
const parsed = Number(raw);
|
|
15
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
16
|
+
}
|
|
17
|
+
export function loadConfig() {
|
|
18
|
+
const accessToken = process.env.META_ACCESS_TOKEN?.trim();
|
|
19
|
+
if (!accessToken) {
|
|
20
|
+
throw new Error("META_ACCESS_TOKEN is required. Set it in the environment or .env file.");
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
accessToken,
|
|
24
|
+
appSecret: process.env.META_APP_SECRET?.trim() || undefined,
|
|
25
|
+
apiVersion: process.env.META_API_VERSION?.trim() || DEFAULT_API_VERSION,
|
|
26
|
+
httpTimeoutMs: parseNumber(process.env.META_HTTP_TIMEOUT_MS, 30_000),
|
|
27
|
+
cacheTtlSeconds: parseNumber(process.env.META_CACHE_TTL_SECONDS, 120),
|
|
28
|
+
maxAutoPages: parseNumber(process.env.META_MAX_AUTO_PAGES, 5),
|
|
29
|
+
allowedBusinessIds: parseAllowlist(process.env.META_ALLOWED_BUSINESS_IDS),
|
|
30
|
+
allowedAdAccountIds: parseAllowlist(process.env.META_ALLOWED_AD_ACCOUNT_IDS),
|
|
31
|
+
allowedPageIds: parseAllowlist(process.env.META_ALLOWED_PAGE_IDS),
|
|
32
|
+
allowedIgUserIds: parseAllowlist(process.env.META_ALLOWED_IG_USER_IDS),
|
|
33
|
+
logLevel: process.env.LOG_LEVEL?.trim() || "info",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function assertAllowed(kind, id, config) {
|
|
37
|
+
const allowlist = {
|
|
38
|
+
business: config.allowedBusinessIds,
|
|
39
|
+
ad_account: config.allowedAdAccountIds,
|
|
40
|
+
page: config.allowedPageIds,
|
|
41
|
+
ig_user: config.allowedIgUserIds,
|
|
42
|
+
}[kind];
|
|
43
|
+
if (allowlist && !allowlist.has(id)) {
|
|
44
|
+
throw new Error(`${kind} '${id}' is not in the configured META_ALLOWED_${kind.toUpperCase()}_IDS allowlist.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const GRAPH_BASE_URL = "https://graph.facebook.com";
|
|
2
|
+
export declare const DEFAULT_API_VERSION = "v23.0";
|
|
3
|
+
export declare const CHARACTER_LIMIT = 25000;
|
|
4
|
+
export declare const DEFAULT_PAGE_LIMIT = 25;
|
|
5
|
+
export declare const MAX_PAGE_LIMIT = 500;
|
|
6
|
+
export declare const SERVER_NAME = "meta-business-manager-mcp-server";
|
|
7
|
+
export declare const SERVER_VERSION = "0.1.0";
|
|
8
|
+
export declare const META_ERROR_CODES: {
|
|
9
|
+
readonly UNKNOWN: 1;
|
|
10
|
+
readonly SERVICE_TEMPORARILY_UNAVAILABLE: 2;
|
|
11
|
+
readonly APPLICATION_LIMIT_REACHED: 4;
|
|
12
|
+
readonly USER_REQUEST_LIMIT_REACHED: 17;
|
|
13
|
+
readonly PAGE_LIMIT_REACHED: 32;
|
|
14
|
+
readonly AD_ACCOUNT_THROTTLED: 613;
|
|
15
|
+
readonly INVALID_ACCESS_TOKEN: 190;
|
|
16
|
+
readonly PERMISSION_DENIED: 10;
|
|
17
|
+
readonly REQUIRES_REVIEW_PERMISSION: 200;
|
|
18
|
+
};
|
|
19
|
+
export declare const RETRYABLE_META_CODES: Set<number>;
|
|
20
|
+
export declare const MAX_RETRIES = 3;
|
|
21
|
+
export declare const BASE_RETRY_DELAY_MS = 1000;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const GRAPH_BASE_URL = "https://graph.facebook.com";
|
|
2
|
+
export const DEFAULT_API_VERSION = "v23.0";
|
|
3
|
+
export const CHARACTER_LIMIT = 25_000;
|
|
4
|
+
export const DEFAULT_PAGE_LIMIT = 25;
|
|
5
|
+
export const MAX_PAGE_LIMIT = 500;
|
|
6
|
+
export const SERVER_NAME = "meta-business-manager-mcp-server";
|
|
7
|
+
export const SERVER_VERSION = "0.1.0";
|
|
8
|
+
export const META_ERROR_CODES = {
|
|
9
|
+
UNKNOWN: 1,
|
|
10
|
+
SERVICE_TEMPORARILY_UNAVAILABLE: 2,
|
|
11
|
+
APPLICATION_LIMIT_REACHED: 4,
|
|
12
|
+
USER_REQUEST_LIMIT_REACHED: 17,
|
|
13
|
+
PAGE_LIMIT_REACHED: 32,
|
|
14
|
+
AD_ACCOUNT_THROTTLED: 613,
|
|
15
|
+
INVALID_ACCESS_TOKEN: 190,
|
|
16
|
+
PERMISSION_DENIED: 10,
|
|
17
|
+
REQUIRES_REVIEW_PERMISSION: 200,
|
|
18
|
+
};
|
|
19
|
+
export const RETRYABLE_META_CODES = new Set([
|
|
20
|
+
META_ERROR_CODES.UNKNOWN,
|
|
21
|
+
META_ERROR_CODES.SERVICE_TEMPORARILY_UNAVAILABLE,
|
|
22
|
+
META_ERROR_CODES.APPLICATION_LIMIT_REACHED,
|
|
23
|
+
META_ERROR_CODES.USER_REQUEST_LIMIT_REACHED,
|
|
24
|
+
META_ERROR_CODES.PAGE_LIMIT_REACHED,
|
|
25
|
+
META_ERROR_CODES.AD_ACCOUNT_THROTTLED,
|
|
26
|
+
]);
|
|
27
|
+
export const MAX_RETRIES = 3;
|
|
28
|
+
export const BASE_RETRY_DELAY_MS = 1000;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Config } from "./config.js";
|
|
2
|
+
import { GraphClient } from "./helpers/graph-client.js";
|
|
3
|
+
import type { Logger } from "./logger.js";
|
|
4
|
+
export interface ToolContext {
|
|
5
|
+
config: Config;
|
|
6
|
+
graph: GraphClient;
|
|
7
|
+
logger: Logger;
|
|
8
|
+
}
|
|
9
|
+
export declare function createContext(config: Config, logger: Logger): ToolContext;
|
package/dist/context.js
ADDED
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface MetaApiError {
|
|
2
|
+
message: string;
|
|
3
|
+
type?: string;
|
|
4
|
+
code?: number;
|
|
5
|
+
error_subcode?: number;
|
|
6
|
+
fbtrace_id?: string;
|
|
7
|
+
error_user_title?: string;
|
|
8
|
+
error_user_msg?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class MetaError extends Error {
|
|
11
|
+
readonly code: number | undefined;
|
|
12
|
+
readonly subcode: number | undefined;
|
|
13
|
+
readonly fbtraceId: string | undefined;
|
|
14
|
+
readonly type: string | undefined;
|
|
15
|
+
readonly httpStatus: number | undefined;
|
|
16
|
+
readonly retryable: boolean;
|
|
17
|
+
readonly hint: string;
|
|
18
|
+
constructor(message: string, opts?: {
|
|
19
|
+
code?: number;
|
|
20
|
+
subcode?: number;
|
|
21
|
+
fbtraceId?: string;
|
|
22
|
+
type?: string;
|
|
23
|
+
httpStatus?: number;
|
|
24
|
+
hint?: string;
|
|
25
|
+
});
|
|
26
|
+
toJSON(): {
|
|
27
|
+
error: string;
|
|
28
|
+
message: string;
|
|
29
|
+
code: number | undefined;
|
|
30
|
+
subcode: number | undefined;
|
|
31
|
+
type: string | undefined;
|
|
32
|
+
fbtrace_id: string | undefined;
|
|
33
|
+
http_status: number | undefined;
|
|
34
|
+
retryable: boolean;
|
|
35
|
+
hint: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export declare function normalizeAxiosError(err: unknown): MetaError;
|
|
39
|
+
export declare class ReadOnlyViolationError extends Error {
|
|
40
|
+
constructor(method: string, url: string);
|
|
41
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { RETRYABLE_META_CODES } from "./constants.js";
|
|
2
|
+
export class MetaError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
subcode;
|
|
5
|
+
fbtraceId;
|
|
6
|
+
type;
|
|
7
|
+
httpStatus;
|
|
8
|
+
retryable;
|
|
9
|
+
hint;
|
|
10
|
+
constructor(message, opts = {}) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "MetaError";
|
|
13
|
+
this.code = opts.code;
|
|
14
|
+
this.subcode = opts.subcode;
|
|
15
|
+
this.fbtraceId = opts.fbtraceId;
|
|
16
|
+
this.type = opts.type;
|
|
17
|
+
this.httpStatus = opts.httpStatus;
|
|
18
|
+
this.retryable = opts.code != null && RETRYABLE_META_CODES.has(opts.code);
|
|
19
|
+
this.hint = opts.hint ?? deriveHint(opts.code, opts.subcode, opts.httpStatus);
|
|
20
|
+
}
|
|
21
|
+
toJSON() {
|
|
22
|
+
return {
|
|
23
|
+
error: "MetaError",
|
|
24
|
+
message: this.message,
|
|
25
|
+
code: this.code,
|
|
26
|
+
subcode: this.subcode,
|
|
27
|
+
type: this.type,
|
|
28
|
+
fbtrace_id: this.fbtraceId,
|
|
29
|
+
http_status: this.httpStatus,
|
|
30
|
+
retryable: this.retryable,
|
|
31
|
+
hint: this.hint,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function deriveHint(code, subcode, httpStatus) {
|
|
36
|
+
if (code === 190) {
|
|
37
|
+
if (subcode === 463)
|
|
38
|
+
return "Access token has expired. Generate a new system-user token.";
|
|
39
|
+
if (subcode === 467)
|
|
40
|
+
return "Access token is invalid. Re-issue from Business Settings → System Users.";
|
|
41
|
+
return "Access token error. Check META_ACCESS_TOKEN is valid and tied to the correct app.";
|
|
42
|
+
}
|
|
43
|
+
if (code === 10 || code === 200) {
|
|
44
|
+
return "Permission denied. Verify the system user is assigned to the target asset and that the app has the required permissions (e.g. ads_read, pages_read_engagement).";
|
|
45
|
+
}
|
|
46
|
+
if (code === 4 || code === 17 || code === 32) {
|
|
47
|
+
return "Rate-limited by Meta. Retry with backoff or narrow the query (shorter date range, fewer breakdowns).";
|
|
48
|
+
}
|
|
49
|
+
if (code === 613) {
|
|
50
|
+
return "Ad account throttled. Reduce insight query complexity or wait before retrying.";
|
|
51
|
+
}
|
|
52
|
+
if (code === 100) {
|
|
53
|
+
return "Invalid parameter. Check field names, ID format (ad accounts need 'act_' prefix), and date presets.";
|
|
54
|
+
}
|
|
55
|
+
if (httpStatus === 404) {
|
|
56
|
+
return "Resource not found. Check the ID and that it is assigned to the configured system user.";
|
|
57
|
+
}
|
|
58
|
+
if (httpStatus === 403) {
|
|
59
|
+
return "Forbidden. The token lacks access to this asset — assign it via Business Settings.";
|
|
60
|
+
}
|
|
61
|
+
return "See https://developers.facebook.com/docs/graph-api/guides/error-handling/ for troubleshooting.";
|
|
62
|
+
}
|
|
63
|
+
export function normalizeAxiosError(err) {
|
|
64
|
+
const axiosErr = err;
|
|
65
|
+
if (axiosErr?.isAxiosError) {
|
|
66
|
+
const apiErr = axiosErr.response?.data?.error;
|
|
67
|
+
if (apiErr) {
|
|
68
|
+
return new MetaError(apiErr.message, {
|
|
69
|
+
code: apiErr.code,
|
|
70
|
+
subcode: apiErr.error_subcode,
|
|
71
|
+
fbtraceId: apiErr.fbtrace_id,
|
|
72
|
+
type: apiErr.type,
|
|
73
|
+
httpStatus: axiosErr.response?.status,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
const status = axiosErr.response?.status;
|
|
77
|
+
return new MetaError(axiosErr.message || "HTTP request failed", {
|
|
78
|
+
httpStatus: status,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (err instanceof Error)
|
|
82
|
+
return new MetaError(err.message);
|
|
83
|
+
return new MetaError(String(err));
|
|
84
|
+
}
|
|
85
|
+
export class ReadOnlyViolationError extends Error {
|
|
86
|
+
constructor(method, url) {
|
|
87
|
+
super(`Refusing ${method} ${url}: this MCP server is read-only. Only GET requests are permitted by the Graph client.`);
|
|
88
|
+
this.name = "ReadOnlyViolationError";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { LRUCache } from "lru-cache";
|
|
2
|
+
class NoopCache {
|
|
3
|
+
get() {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
set() {
|
|
7
|
+
/* noop */
|
|
8
|
+
}
|
|
9
|
+
clear() {
|
|
10
|
+
/* noop */
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function createCache(ttlSeconds) {
|
|
14
|
+
if (ttlSeconds <= 0)
|
|
15
|
+
return new NoopCache();
|
|
16
|
+
const cache = new LRUCache({
|
|
17
|
+
max: 500,
|
|
18
|
+
ttl: ttlSeconds * 1000,
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
get: (k) => cache.get(k),
|
|
22
|
+
set: (k, v) => {
|
|
23
|
+
if (v != null && typeof v === "object")
|
|
24
|
+
cache.set(k, v);
|
|
25
|
+
},
|
|
26
|
+
clear: () => cache.clear(),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare enum ResponseFormat {
|
|
2
|
+
MARKDOWN = "markdown",
|
|
3
|
+
JSON = "json"
|
|
4
|
+
}
|
|
5
|
+
export interface ToolTextResult {
|
|
6
|
+
content: {
|
|
7
|
+
type: "text";
|
|
8
|
+
text: string;
|
|
9
|
+
}[];
|
|
10
|
+
structuredContent?: Record<string, unknown>;
|
|
11
|
+
isError?: boolean;
|
|
12
|
+
[k: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
export declare function jsonBlock(data: unknown): string;
|
|
15
|
+
export declare function truncate(text: string, limit?: number): string;
|
|
16
|
+
export declare function toolResult(structured: Record<string, unknown>, text: string): ToolTextResult;
|
|
17
|
+
export declare function toolError(message: string, hint?: string, extra?: Record<string, unknown>): ToolTextResult;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { CHARACTER_LIMIT } from "../constants.js";
|
|
2
|
+
export var ResponseFormat;
|
|
3
|
+
(function (ResponseFormat) {
|
|
4
|
+
ResponseFormat["MARKDOWN"] = "markdown";
|
|
5
|
+
ResponseFormat["JSON"] = "json";
|
|
6
|
+
})(ResponseFormat || (ResponseFormat = {}));
|
|
7
|
+
export function jsonBlock(data) {
|
|
8
|
+
return JSON.stringify(data, null, 2);
|
|
9
|
+
}
|
|
10
|
+
export function truncate(text, limit = CHARACTER_LIMIT) {
|
|
11
|
+
if (text.length <= limit)
|
|
12
|
+
return text;
|
|
13
|
+
return `${text.slice(0, limit)}\n\n…[truncated — response exceeded ${limit} characters. Use filters or pagination to narrow results.]`;
|
|
14
|
+
}
|
|
15
|
+
export function toolResult(structured, text) {
|
|
16
|
+
return {
|
|
17
|
+
content: [{ type: "text", text: truncate(text) }],
|
|
18
|
+
structuredContent: structured,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function toolError(message, hint, extra) {
|
|
22
|
+
const payload = { error: true, message, hint, ...extra };
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: "text", text: jsonBlock(payload) }],
|
|
25
|
+
structuredContent: payload,
|
|
26
|
+
isError: true,
|
|
27
|
+
};
|
|
28
|
+
}
|