facebook-mcp-server 1.6.6
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/.env.example +2 -0
- package/.github/dependabot.yml +50 -0
- package/.github/workflows/ci.yml +51 -0
- package/.github/workflows/release.yml +200 -0
- package/CONTRIBUTING.md +112 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/dist/client.d.ts +57 -0
- package/dist/client.js +140 -0
- package/dist/client.test.d.ts +9 -0
- package/dist/client.test.js +211 -0
- package/dist/create-post.d.ts +39 -0
- package/dist/create-post.js +85 -0
- package/dist/create-post.test.d.ts +11 -0
- package/dist/create-post.test.js +175 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +87 -0
- package/dist/errors.test.d.ts +9 -0
- package/dist/errors.test.js +162 -0
- package/dist/first-comment.test.d.ts +10 -0
- package/dist/first-comment.test.js +54 -0
- package/dist/handlers.test.d.ts +19 -0
- package/dist/handlers.test.js +333 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +374 -0
- package/dist/lib/index.d.ts +9 -0
- package/dist/lib/index.js +8 -0
- package/dist/lib/insights.d.ts +53 -0
- package/dist/lib/insights.js +47 -0
- package/dist/rate-limiter.d.ts +71 -0
- package/dist/rate-limiter.js +214 -0
- package/dist/rate-limiter.test.d.ts +1 -0
- package/dist/rate-limiter.test.js +154 -0
- package/dist/response.d.ts +24 -0
- package/dist/response.js +35 -0
- package/dist/response.test.d.ts +1 -0
- package/dist/response.test.js +71 -0
- package/dist/sanitize.d.ts +17 -0
- package/dist/sanitize.js +27 -0
- package/dist/sanitize.test.d.ts +1 -0
- package/dist/sanitize.test.js +43 -0
- package/dist/tools.test.d.ts +16 -0
- package/dist/tools.test.js +150 -0
- package/package.json +29 -0
- package/src/client.test.ts +284 -0
- package/src/client.ts +204 -0
- package/src/create-post.test.ts +196 -0
- package/src/create-post.ts +118 -0
- package/src/errors.test.ts +297 -0
- package/src/errors.ts +108 -0
- package/src/first-comment.test.ts +73 -0
- package/src/handlers.test.ts +431 -0
- package/src/index.ts +540 -0
- package/src/lib/index.ts +9 -0
- package/src/lib/insights.ts +150 -0
- package/src/rate-limiter.test.ts +186 -0
- package/src/rate-limiter.ts +252 -0
- package/src/response.test.ts +80 -0
- package/src/response.ts +43 -0
- package/src/sanitize.test.ts +52 -0
- package/src/sanitize.ts +35 -0
- package/src/tools.test.ts +195 -0
- package/tsconfig.json +15 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for tool-handler helpers.
|
|
3
|
+
*
|
|
4
|
+
* Tests the building blocks every tool uses:
|
|
5
|
+
* - safeHandler — wraps every tool, formats errors
|
|
6
|
+
* - resolveCredentials — env-vs-args precedence
|
|
7
|
+
*
|
|
8
|
+
* Full per-tool happy/error path tests would require spinning up the whole
|
|
9
|
+
* McpServer and sending JSON-RPC messages; instead we unit-test the shared
|
|
10
|
+
* helpers each tool depends on, which covers the surface area that actually
|
|
11
|
+
* breaks during Graph API changes.
|
|
12
|
+
*
|
|
13
|
+
* Note: Facebook publishes are synchronous (no container flow), so there is
|
|
14
|
+
* no pollContainerStatus helper to test.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
18
|
+
|
|
19
|
+
import { safeHandler, resolveCredentials } from "./index.js";
|
|
20
|
+
import { FacebookApiError } from "./client.js";
|
|
21
|
+
|
|
22
|
+
// Silence the console.error logging from safeHandler during tests
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
25
|
+
});
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
vi.unstubAllGlobals();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("resolveCredentials", () => {
|
|
32
|
+
// resolveCredentials reads DEFAULT_* constants captured at import time.
|
|
33
|
+
// We can't mutate them mid-test, but we can verify args-vs-env precedence
|
|
34
|
+
// by controlling args. The env-only path is covered by the "args missing"
|
|
35
|
+
// case returning whatever env happens to contain at import time.
|
|
36
|
+
|
|
37
|
+
it("returns null when both args and env are missing", () => {
|
|
38
|
+
// If env is set at import, this test is a no-op, so guard with a stub
|
|
39
|
+
if (!process.env.FACEBOOK_ACCESS_TOKEN || !process.env.FACEBOOK_PAGE_ID) {
|
|
40
|
+
expect(resolveCredentials({})).toBeNull();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns provided args when both are supplied", () => {
|
|
45
|
+
const r = resolveCredentials({
|
|
46
|
+
accessToken: "tok_from_args",
|
|
47
|
+
pageId: "page_from_args",
|
|
48
|
+
});
|
|
49
|
+
expect(r).toEqual({
|
|
50
|
+
accessToken: "tok_from_args",
|
|
51
|
+
pageId: "page_from_args",
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("args-provided values take precedence over env defaults", () => {
|
|
56
|
+
// Even if env vars are set (from .env or shell), explicit args win
|
|
57
|
+
const r = resolveCredentials({
|
|
58
|
+
accessToken: "override_token",
|
|
59
|
+
pageId: "override_account",
|
|
60
|
+
});
|
|
61
|
+
expect(r?.accessToken).toBe("override_token");
|
|
62
|
+
expect(r?.pageId).toBe("override_account");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("partial args fall through to env (or null if env missing)", () => {
|
|
66
|
+
// Only pageId supplied, accessToken must come from env
|
|
67
|
+
const r = resolveCredentials({ pageId: "page_only" });
|
|
68
|
+
if (process.env.FACEBOOK_ACCESS_TOKEN) {
|
|
69
|
+
expect(r?.pageId).toBe("page_only");
|
|
70
|
+
expect(r?.accessToken).toBe(process.env.FACEBOOK_ACCESS_TOKEN);
|
|
71
|
+
} else {
|
|
72
|
+
expect(r).toBeNull();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("safeHandler", () => {
|
|
78
|
+
it("passes through successful handler results", async () => {
|
|
79
|
+
const handler = safeHandler("test_tool", async () => ({
|
|
80
|
+
content: [{ type: "text" as const, text: "success" }],
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
const result = await handler({});
|
|
84
|
+
expect(result).toEqual({
|
|
85
|
+
content: [{ type: "text", text: "success" }],
|
|
86
|
+
});
|
|
87
|
+
expect((result as { isError?: boolean }).isError).toBeFalsy();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("catches thrown Error and returns errorResult", async () => {
|
|
91
|
+
const handler = safeHandler("test_tool", async () => {
|
|
92
|
+
throw new Error("something broke");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const result = (await handler({})) as {
|
|
96
|
+
isError: boolean;
|
|
97
|
+
content: Array<{ text: string }>;
|
|
98
|
+
};
|
|
99
|
+
expect(result.isError).toBe(true);
|
|
100
|
+
|
|
101
|
+
const body = JSON.parse(result.content[0].text);
|
|
102
|
+
expect(body.error).toBe("API error");
|
|
103
|
+
expect(body.message).toContain("test_tool failed");
|
|
104
|
+
expect(body.message).toContain("something broke");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("catches FacebookApiError with auth-failure detail and sets action=AUTH_FAILED", async () => {
|
|
108
|
+
const handler = safeHandler("fb_get_page_insights", async () => {
|
|
109
|
+
throw new FacebookApiError(400, {
|
|
110
|
+
message: "Invalid OAuth access token - Cannot parse access token",
|
|
111
|
+
type: "OAuthException",
|
|
112
|
+
code: 190,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const result = (await handler({})) as {
|
|
117
|
+
isError: boolean;
|
|
118
|
+
content: Array<{ text: string }>;
|
|
119
|
+
};
|
|
120
|
+
const body = JSON.parse(result.content[0].text);
|
|
121
|
+
expect(body.statusCode).toBe(400);
|
|
122
|
+
expect(body.action).toMatch(/^AUTH_FAILED:/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("catches metric-error (Bug #6 regression): does NOT set AUTH_FAILED", async () => {
|
|
126
|
+
const handler = safeHandler("fb_get_page_insights", async () => {
|
|
127
|
+
throw new FacebookApiError(400, {
|
|
128
|
+
message:
|
|
129
|
+
"(#100) metric[2] must be one of the following values: reach, follower_count",
|
|
130
|
+
type: "OAuthException",
|
|
131
|
+
code: 100,
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const result = (await handler({})) as {
|
|
136
|
+
isError: boolean;
|
|
137
|
+
content: Array<{ text: string }>;
|
|
138
|
+
};
|
|
139
|
+
const body = JSON.parse(result.content[0].text);
|
|
140
|
+
expect(body.action).not.toMatch(/^AUTH_FAILED:/);
|
|
141
|
+
expect(body.action).toMatch(/^INVALID_REQUEST:/);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("catches media-type error (Bug #9 regression): maps subcode 2207052 to INVALID_MEDIA", async () => {
|
|
145
|
+
const handler = safeHandler("fb_create_post", async () => {
|
|
146
|
+
throw new FacebookApiError(400, {
|
|
147
|
+
message: "Only photo or video can be accepted as media type.",
|
|
148
|
+
type: "OAuthException",
|
|
149
|
+
code: 100,
|
|
150
|
+
error_subcode: 2207052,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const result = (await handler({})) as {
|
|
155
|
+
isError: boolean;
|
|
156
|
+
content: Array<{ text: string }>;
|
|
157
|
+
};
|
|
158
|
+
const body = JSON.parse(result.content[0].text);
|
|
159
|
+
expect(body.action).toMatch(/^INVALID_MEDIA:/);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("catches non-Error throws (string) gracefully", async () => {
|
|
163
|
+
const handler = safeHandler("test_tool", async () => {
|
|
164
|
+
throw "string error";
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const result = (await handler({})) as {
|
|
168
|
+
isError: boolean;
|
|
169
|
+
content: Array<{ text: string }>;
|
|
170
|
+
};
|
|
171
|
+
expect(result.isError).toBe(true);
|
|
172
|
+
const body = JSON.parse(result.content[0].text);
|
|
173
|
+
expect(body.message).toContain("string error");
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("safeHandler — rate-limit error (429 long wait)", () => {
|
|
178
|
+
it("maps 429 to RATE_LIMITED action string", async () => {
|
|
179
|
+
const handler = safeHandler("fb_get_page_insights", async () => {
|
|
180
|
+
throw new FacebookApiError(
|
|
181
|
+
429,
|
|
182
|
+
{ message: "too fast", type: "OAuthException", code: 4 },
|
|
183
|
+
300,
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const result = (await handler({})) as {
|
|
188
|
+
isError: boolean;
|
|
189
|
+
content: Array<{ text: string }>;
|
|
190
|
+
};
|
|
191
|
+
const body = JSON.parse(result.content[0].text);
|
|
192
|
+
expect(body.statusCode).toBe(429);
|
|
193
|
+
expect(body.action).toMatch(/^RATE_LIMITED:/);
|
|
194
|
+
});
|
|
195
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"outDir": "dist",
|
|
10
|
+
"rootDir": "src",
|
|
11
|
+
"declaration": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "dist"]
|
|
15
|
+
}
|