@bycrawl/mcp 0.4.1 → 0.5.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.
Files changed (51) hide show
  1. package/README.md +20 -0
  2. package/build/client.js +5 -1
  3. package/build/client.js.map +1 -1
  4. package/build/oauth/callback.d.ts +6 -0
  5. package/build/oauth/callback.js +75 -0
  6. package/build/oauth/callback.js.map +1 -0
  7. package/build/oauth/crypto.d.ts +6 -0
  8. package/build/oauth/crypto.js +29 -0
  9. package/build/oauth/crypto.js.map +1 -0
  10. package/build/oauth/provider.d.ts +14 -0
  11. package/build/oauth/provider.js +125 -0
  12. package/build/oauth/provider.js.map +1 -0
  13. package/build/oauth/store.d.ts +43 -0
  14. package/build/oauth/store.js +66 -0
  15. package/build/oauth/store.js.map +1 -0
  16. package/build/oauth/user-keys.d.ts +6 -0
  17. package/build/oauth/user-keys.js +89 -0
  18. package/build/oauth/user-keys.js.map +1 -0
  19. package/build/server.js +2 -2
  20. package/build/server.js.map +1 -1
  21. package/build/tools/dcard.js +6 -14
  22. package/build/tools/dcard.js.map +1 -1
  23. package/build/tools/facebook.js +21 -8
  24. package/build/tools/facebook.js.map +1 -1
  25. package/build/tools/gmaps.d.ts +2 -0
  26. package/build/tools/gmaps.js +13 -0
  27. package/build/tools/gmaps.js.map +1 -0
  28. package/build/tools/instagram.js +5 -5
  29. package/build/tools/instagram.js.map +1 -1
  30. package/build/tools/job104.js +3 -3
  31. package/build/tools/job104.js.map +1 -1
  32. package/build/tools/linkedin.js +9 -9
  33. package/build/tools/linkedin.js.map +1 -1
  34. package/build/tools/reddit.js +16 -5
  35. package/build/tools/reddit.js.map +1 -1
  36. package/build/tools/system.js +2 -1
  37. package/build/tools/system.js.map +1 -1
  38. package/build/tools/threads.js +8 -8
  39. package/build/tools/threads.js.map +1 -1
  40. package/build/tools/tiktok.js +8 -8
  41. package/build/tools/tiktok.js.map +1 -1
  42. package/build/tools/x.js +5 -5
  43. package/build/tools/x.js.map +1 -1
  44. package/build/tools/youtube.js +5 -7
  45. package/build/tools/youtube.js.map +1 -1
  46. package/build/transport-http.js +146 -66
  47. package/build/transport-http.js.map +1 -1
  48. package/package.json +6 -6
  49. package/build/tools/xiaohongshu.d.ts +0 -2
  50. package/build/tools/xiaohongshu.js +0 -31
  51. package/build/tools/xiaohongshu.js.map +0 -1
package/README.md CHANGED
@@ -45,6 +45,26 @@ Add to your MCP config:
45
45
  | Job104 | 3 | Search jobs, get company/job details |
46
46
  | System | 1 | Health check |
47
47
 
48
+ ## Usage Examples
49
+
50
+ ### Example 1: Search Threads posts about a topic
51
+
52
+ > **Prompt:** "What are people saying about OpenAI on Threads?"
53
+
54
+ Claude will call `threads_search_posts` with `query: "OpenAI"` and return a summary of recent public posts, including text, like counts, and reply counts.
55
+
56
+ ### Example 2: Get LinkedIn company info
57
+
58
+ > **Prompt:** "Tell me about Anthropic's LinkedIn presence — what do they do, how big are they?"
59
+
60
+ Claude will call `linkedin_get_company` with `company_id: "anthropic"` and return the company's description, employee count, industry, headquarters, and recent activity.
61
+
62
+ ### Example 3: Fetch YouTube video comments
63
+
64
+ > **Prompt:** "Summarize the comments on this YouTube video: https://www.youtube.com/watch?v=dQw4w9WgXcQ"
65
+
66
+ Claude will call `youtube_get_video_comments` with `video_id: "dQw4w9WgXcQ"` and return the top comments with like counts, which it can then summarize for sentiment and key themes.
67
+
48
68
  ## API Key
49
69
 
50
70
  Get your API key at [bycrawl.com](https://bycrawl.com).
package/build/client.js CHANGED
@@ -9,15 +9,19 @@ export async function bycrawlGet(path, params) {
9
9
  url.searchParams.set(k, String(v));
10
10
  }
11
11
  }
12
+ const apiKey = getApiKey();
13
+ console.log(`[bycrawlGet] ${url.toString()} apiKey=${apiKey ? apiKey.slice(0, 12) + "..." : "(empty)"}`);
12
14
  const res = await fetch(url, {
13
- headers: { "x-api-key": getApiKey() },
15
+ headers: { "x-api-key": apiKey },
14
16
  signal: AbortSignal.timeout(120_000),
15
17
  });
16
18
  const body = await res.text();
19
+ console.log(`[bycrawlGet] status=${res.status} body=${body.slice(0, 200)}`);
17
20
  return { content: [{ type: "text", text: body }] };
18
21
  }
19
22
  catch (err) {
20
23
  const message = err instanceof Error ? err.message : String(err);
24
+ console.error(`[bycrawlGet] error:`, message);
21
25
  return { content: [{ type: "text", text: JSON.stringify({ error: message }) }] };
22
26
  }
23
27
  }
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,yBAAyB,CAAC;AAIzE,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAY,EACZ,MAAqE;IAErE,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACnC,IAAI,MAAM,EAAE,CAAC;YACX,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC5C,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE;oBAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;YAChE,CAAC;QACH,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC3B,OAAO,EAAE,EAAE,WAAW,EAAE,SAAS,EAAE,EAAE;YACrC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC;SACrC,CAAC,CAAC;QAEH,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC9B,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IACrD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;IACnF,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,yBAAyB,CAAC;AAIzE,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAY,EACZ,MAAqE;IAErE,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACnC,IAAI,MAAM,EAAE,CAAC;YACX,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC5C,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE;oBAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;YAChE,CAAC;QACH,CAAC;QAED,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;QAC3B,OAAO,CAAC,GAAG,CAAC,gBAAgB,GAAG,CAAC,QAAQ,EAAE,WAAW,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC;QAEzG,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC3B,OAAO,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE;YAChC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC;SACrC,CAAC,CAAC;QAEH,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC9B,OAAO,CAAC,GAAG,CAAC,uBAAuB,GAAG,CAAC,MAAM,SAAS,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAC5E,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IACrD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,OAAO,CAAC,KAAK,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAC;QAC9C,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;IACnF,CAAC;AACH,CAAC"}
@@ -0,0 +1,6 @@
1
+ import type { Request, Response } from "express";
2
+ /**
3
+ * GET /oauth/callback/:nonce
4
+ * Supabase Auth redirects here after Google login.
5
+ */
6
+ export declare function oauthCallbackHandler(req: Request, res: Response): Promise<void>;
@@ -0,0 +1,75 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { pendingAuthStore, authCodeStore } from "./store.js";
3
+ import { findUserByEmail, findOrCreateMcpKey } from "./user-keys.js";
4
+ const SUPABASE_URL = process.env.SUPABASE_URL;
5
+ const SUPABASE_PUBLISHABLE_KEY = process.env.SUPABASE_PUBLISHABLE_KEY;
6
+ /**
7
+ * GET /oauth/callback/:nonce
8
+ * Supabase Auth redirects here after Google login.
9
+ */
10
+ export async function oauthCallbackHandler(req, res) {
11
+ try {
12
+ const nonce = req.params.nonce;
13
+ const code = req.query.code;
14
+ if (!nonce || !code) {
15
+ res.status(400).json({ error: "Missing nonce or code" });
16
+ return;
17
+ }
18
+ // 1. Look up pending auth state
19
+ const pending = pendingAuthStore.getAndDelete(nonce);
20
+ if (!pending) {
21
+ res.status(400).json({ error: "Invalid or expired nonce" });
22
+ return;
23
+ }
24
+ // 2. Exchange Supabase auth code for session (PKCE flow)
25
+ const tokenRes = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=pkce`, {
26
+ method: "POST",
27
+ headers: {
28
+ apikey: SUPABASE_PUBLISHABLE_KEY,
29
+ "Content-Type": "application/json",
30
+ },
31
+ body: JSON.stringify({
32
+ auth_code: code,
33
+ code_verifier: pending.supabaseCodeVerifier,
34
+ }),
35
+ });
36
+ if (!tokenRes.ok) {
37
+ const err = await tokenRes.text();
38
+ res.status(502).json({ error: `Supabase token exchange failed: ${err}` });
39
+ return;
40
+ }
41
+ const session = (await tokenRes.json());
42
+ const email = session.user?.email;
43
+ if (!email) {
44
+ res.status(502).json({ error: "No email in Supabase session" });
45
+ return;
46
+ }
47
+ // 3. Find user in ByCrawl
48
+ const user = await findUserByEmail(email);
49
+ if (!user) {
50
+ const loginUrl = `https://bycrawl.com/login?reason=mcp_signup_required&email=${encodeURIComponent(email)}`;
51
+ res.redirect(302, loginUrl);
52
+ return;
53
+ }
54
+ // 4. Find or create MCP API key
55
+ const apiKey = await findOrCreateMcpKey(user.id);
56
+ // 5. Generate auth code and store it
57
+ const authCode = randomBytes(32).toString("hex");
58
+ authCodeStore.set(authCode, {
59
+ clientId: pending.clientId,
60
+ apiKey,
61
+ codeChallenge: pending.codeChallenge,
62
+ redirectUri: pending.redirectUri,
63
+ });
64
+ // 6. Redirect back to Claude with auth code
65
+ const redirectUrl = new URL(pending.redirectUri);
66
+ redirectUrl.searchParams.set("code", authCode);
67
+ redirectUrl.searchParams.set("state", pending.state);
68
+ res.redirect(302, redirectUrl.toString());
69
+ }
70
+ catch (err) {
71
+ console.error("OAuth callback error:", err);
72
+ res.status(500).json({ error: "Internal server error during OAuth callback" });
73
+ }
74
+ }
75
+ //# sourceMappingURL=callback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.js","sourceRoot":"","sources":["../../src/oauth/callback.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /** AES-256-GCM encrypt. Returns `iv:ciphertext:tag` in hex. */
2
+ export declare function encryptApiKey(plain: string): string;
3
+ /** Decrypt an `iv:ciphertext:tag` hex blob. */
4
+ export declare function decryptApiKey(blob: string): string;
5
+ /** SHA-256 hex hash — matches ByCrawl gateway's Python hashlib.sha256. */
6
+ export declare function hashApiKey(plain: string): string;
@@ -0,0 +1,29 @@
1
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
2
+ const KEY_HEX = process.env.KEY_ENCRYPTION_KEY ?? "";
3
+ function getKey() {
4
+ if (KEY_HEX.length !== 64)
5
+ throw new Error("KEY_ENCRYPTION_KEY must be 64 hex chars (32 bytes)");
6
+ return Buffer.from(KEY_HEX, "hex");
7
+ }
8
+ /** AES-256-GCM encrypt. Returns `iv:ciphertext:tag` in hex. */
9
+ export function encryptApiKey(plain) {
10
+ const key = getKey();
11
+ const iv = randomBytes(12);
12
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
13
+ const encrypted = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
14
+ const tag = cipher.getAuthTag();
15
+ return `${iv.toString("hex")}:${encrypted.toString("hex")}:${tag.toString("hex")}`;
16
+ }
17
+ /** Decrypt an `iv:ciphertext:tag` hex blob. */
18
+ export function decryptApiKey(blob) {
19
+ const key = getKey();
20
+ const [ivHex, ctHex, tagHex] = blob.split(":");
21
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivHex, "hex"));
22
+ decipher.setAuthTag(Buffer.from(tagHex, "hex"));
23
+ return decipher.update(ctHex, "hex", "utf8") + decipher.final("utf8");
24
+ }
25
+ /** SHA-256 hex hash — matches ByCrawl gateway's Python hashlib.sha256. */
26
+ export function hashApiKey(plain) {
27
+ return createHash("sha256").update(plain).digest("hex");
28
+ }
29
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/oauth/crypto.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAExF,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,EAAE,CAAC;AAErD,SAAS,MAAM;IACb,IAAI,OAAO,CAAC,MAAM,KAAK,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;IACjG,OAAO,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;AACrC,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC;IACrB,MAAM,EAAE,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAC3B,MAAM,MAAM,GAAG,cAAc,CAAC,aAAa,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IACtD,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAChF,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;IAChC,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;AACrF,CAAC;AAED,+CAA+C;AAC/C,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC;IACrB,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC/C,MAAM,QAAQ,GAAG,gBAAgB,CAAC,aAAa,EAAE,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;IACjF,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;IAChD,OAAO,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;AACxE,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { Response } from "express";
2
+ import type { OAuthServerProvider, AuthorizationParams } from "@modelcontextprotocol/sdk/server/auth/provider.js";
3
+ import type { OAuthRegisteredClientsStore } from "@modelcontextprotocol/sdk/server/auth/clients.js";
4
+ import type { OAuthClientInformationFull, OAuthTokens, OAuthTokenRevocationRequest } from "@modelcontextprotocol/sdk/shared/auth.js";
5
+ import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
6
+ export declare class ByCrawlOAuthProvider implements OAuthServerProvider {
7
+ get clientsStore(): OAuthRegisteredClientsStore;
8
+ authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise<void>;
9
+ challengeForAuthorizationCode(_client: OAuthClientInformationFull, authorizationCode: string): Promise<string>;
10
+ exchangeAuthorizationCode(client: OAuthClientInformationFull, authorizationCode: string): Promise<OAuthTokens>;
11
+ exchangeRefreshToken(client: OAuthClientInformationFull, refreshToken: string): Promise<OAuthTokens>;
12
+ verifyAccessToken(token: string): Promise<AuthInfo>;
13
+ revokeToken(_client: OAuthClientInformationFull, request: OAuthTokenRevocationRequest): Promise<void>;
14
+ }
@@ -0,0 +1,125 @@
1
+ import { randomBytes, createHash } from "node:crypto";
2
+ import { SignJWT, jwtVerify } from "jose";
3
+ import { pendingAuthStore, authCodeStore, refreshTokenStore, clientStore, } from "./store.js";
4
+ const SUPABASE_URL = process.env.SUPABASE_URL;
5
+ const EXTERNAL_URL = process.env.EXTERNAL_URL;
6
+ const JWT_SECRET = process.env.JWT_SECRET;
7
+ function getJwtKey() {
8
+ return new TextEncoder().encode(JWT_SECRET);
9
+ }
10
+ async function signJwt(apiKey, clientId) {
11
+ return new SignJWT({ api_key: apiKey, client_id: clientId })
12
+ .setProtectedHeader({ alg: "HS256" })
13
+ .setIssuedAt()
14
+ .setExpirationTime("1h")
15
+ .sign(getJwtKey());
16
+ }
17
+ export class ByCrawlOAuthProvider {
18
+ get clientsStore() {
19
+ return clientStore;
20
+ }
21
+ async authorize(client, params, res) {
22
+ const nonce = randomBytes(16).toString("hex");
23
+ // Generate PKCE pair for Supabase side
24
+ const supabaseCodeVerifier = randomBytes(32).toString("hex");
25
+ const supabaseCodeChallenge = createHash("sha256")
26
+ .update(supabaseCodeVerifier)
27
+ .digest("base64url");
28
+ pendingAuthStore.set(nonce, {
29
+ state: params.state ?? "",
30
+ clientId: client.client_id,
31
+ codeChallenge: params.codeChallenge,
32
+ redirectUri: params.redirectUri,
33
+ scopes: params.scopes ?? [],
34
+ supabaseCodeVerifier,
35
+ });
36
+ // Redirect to Supabase Auth (Google OAuth) with PKCE + nonce in path
37
+ const callbackUrl = `${EXTERNAL_URL}/oauth/callback/${nonce}`;
38
+ const supabaseAuthUrl = `${SUPABASE_URL}/auth/v1/authorize?provider=google&flow_type=pkce&code_challenge=${supabaseCodeChallenge}&code_challenge_method=S256&redirect_to=${encodeURIComponent(callbackUrl)}`;
39
+ res.redirect(302, supabaseAuthUrl);
40
+ }
41
+ async challengeForAuthorizationCode(_client, authorizationCode) {
42
+ const entry = authCodeStore.get(authorizationCode);
43
+ if (!entry)
44
+ throw new Error("Invalid authorization code");
45
+ return entry.codeChallenge;
46
+ }
47
+ async exchangeAuthorizationCode(client, authorizationCode) {
48
+ const entry = authCodeStore.getAndDelete(authorizationCode);
49
+ if (!entry)
50
+ throw new Error("Invalid or expired authorization code");
51
+ if (entry.clientId !== client.client_id)
52
+ throw new Error("Client ID mismatch");
53
+ const accessToken = await signJwt(entry.apiKey, client.client_id);
54
+ const refreshToken = randomBytes(32).toString("hex");
55
+ refreshTokenStore.set(refreshToken, {
56
+ clientId: client.client_id,
57
+ apiKey: entry.apiKey,
58
+ scopes: [],
59
+ });
60
+ return {
61
+ access_token: accessToken,
62
+ token_type: "bearer",
63
+ expires_in: 3600,
64
+ refresh_token: refreshToken,
65
+ };
66
+ }
67
+ async exchangeRefreshToken(client, refreshToken) {
68
+ const entry = refreshTokenStore.getAndDelete(refreshToken);
69
+ if (!entry)
70
+ throw new Error("Invalid or expired refresh token");
71
+ if (entry.clientId !== client.client_id)
72
+ throw new Error("Client ID mismatch");
73
+ const accessToken = await signJwt(entry.apiKey, client.client_id);
74
+ const newRefreshToken = randomBytes(32).toString("hex");
75
+ // Rotate: store new refresh token
76
+ refreshTokenStore.set(newRefreshToken, {
77
+ clientId: client.client_id,
78
+ apiKey: entry.apiKey,
79
+ scopes: entry.scopes,
80
+ });
81
+ return {
82
+ access_token: accessToken,
83
+ token_type: "bearer",
84
+ expires_in: 3600,
85
+ refresh_token: newRefreshToken,
86
+ };
87
+ }
88
+ async verifyAccessToken(token) {
89
+ console.log(`[verifyAccessToken] token prefix=${token.slice(0, 20)}...`);
90
+ // Backward compat: plain API keys bypass JWT verification
91
+ if (token.startsWith("sk_byc_")) {
92
+ console.log(`[verifyAccessToken] plain API key detected`);
93
+ return {
94
+ token,
95
+ clientId: "legacy",
96
+ scopes: [],
97
+ expiresAt: Math.floor(Date.now() / 1000) + 3600,
98
+ extra: { apiKey: token },
99
+ };
100
+ }
101
+ // JWT verification
102
+ try {
103
+ const { payload } = await jwtVerify(token, getJwtKey(), {
104
+ algorithms: ["HS256"],
105
+ });
106
+ const apiKey = payload.api_key;
107
+ console.log(`[verifyAccessToken] JWT valid, clientId=${payload.client_id}, apiKey prefix=${apiKey?.slice(0, 12)}`);
108
+ return {
109
+ token,
110
+ clientId: payload.client_id ?? "unknown",
111
+ scopes: [],
112
+ expiresAt: payload.exp,
113
+ extra: { apiKey },
114
+ };
115
+ }
116
+ catch (err) {
117
+ console.error(`[verifyAccessToken] JWT verification failed:`, err);
118
+ throw err;
119
+ }
120
+ }
121
+ async revokeToken(_client, request) {
122
+ refreshTokenStore.delete(request.token);
123
+ }
124
+ }
125
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/oauth/provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import type { OAuthRegisteredClientsStore } from "@modelcontextprotocol/sdk/server/auth/clients.js";
2
+ import type { OAuthClientInformationFull } from "@modelcontextprotocol/sdk/shared/auth.js";
3
+ declare class TtlMap<V> {
4
+ private ttlMs;
5
+ private map;
6
+ private timer;
7
+ constructor(ttlMs: number);
8
+ set(key: string, value: V): void;
9
+ get(key: string): V | undefined;
10
+ getAndDelete(key: string): V | undefined;
11
+ delete(key: string): void;
12
+ private cleanup;
13
+ destroy(): void;
14
+ }
15
+ export interface PendingAuth {
16
+ state: string;
17
+ clientId: string;
18
+ codeChallenge: string;
19
+ redirectUri: string;
20
+ scopes: string[];
21
+ supabaseCodeVerifier: string;
22
+ }
23
+ export declare const pendingAuthStore: TtlMap<PendingAuth>;
24
+ export interface AuthCode {
25
+ clientId: string;
26
+ apiKey: string;
27
+ codeChallenge: string;
28
+ redirectUri: string;
29
+ }
30
+ export declare const authCodeStore: TtlMap<AuthCode>;
31
+ export interface RefreshEntry {
32
+ clientId: string;
33
+ apiKey: string;
34
+ scopes: string[];
35
+ }
36
+ export declare const refreshTokenStore: TtlMap<RefreshEntry>;
37
+ declare class InMemoryClientStore implements OAuthRegisteredClientsStore {
38
+ private clients;
39
+ getClient(clientId: string): OAuthClientInformationFull | undefined;
40
+ registerClient(client: Omit<OAuthClientInformationFull, "client_id" | "client_id_issued_at">): OAuthClientInformationFull;
41
+ }
42
+ export declare const clientStore: InMemoryClientStore;
43
+ export {};
@@ -0,0 +1,66 @@
1
+ import { randomUUID } from "node:crypto";
2
+ // --- TTL Map utility ---
3
+ class TtlMap {
4
+ ttlMs;
5
+ map = new Map();
6
+ timer;
7
+ constructor(ttlMs) {
8
+ this.ttlMs = ttlMs;
9
+ this.timer = setInterval(() => this.cleanup(), 60_000);
10
+ this.timer.unref();
11
+ }
12
+ set(key, value) {
13
+ this.map.set(key, { value, expiresAt: Date.now() + this.ttlMs });
14
+ }
15
+ get(key) {
16
+ const entry = this.map.get(key);
17
+ if (!entry)
18
+ return undefined;
19
+ if (Date.now() > entry.expiresAt) {
20
+ this.map.delete(key);
21
+ return undefined;
22
+ }
23
+ return entry.value;
24
+ }
25
+ getAndDelete(key) {
26
+ const value = this.get(key);
27
+ if (value !== undefined)
28
+ this.map.delete(key);
29
+ return value;
30
+ }
31
+ delete(key) {
32
+ this.map.delete(key);
33
+ }
34
+ cleanup() {
35
+ const now = Date.now();
36
+ for (const [key, entry] of this.map) {
37
+ if (now > entry.expiresAt)
38
+ this.map.delete(key);
39
+ }
40
+ }
41
+ destroy() {
42
+ clearInterval(this.timer);
43
+ }
44
+ }
45
+ export const pendingAuthStore = new TtlMap(10 * 60 * 1000); // 10 min
46
+ export const authCodeStore = new TtlMap(5 * 60 * 1000); // 5 min
47
+ export const refreshTokenStore = new TtlMap(30 * 24 * 60 * 60 * 1000); // 30 days
48
+ // --- Client registration (in-memory, persists until restart) ---
49
+ class InMemoryClientStore {
50
+ clients = new Map();
51
+ getClient(clientId) {
52
+ return this.clients.get(clientId);
53
+ }
54
+ registerClient(client) {
55
+ const full = {
56
+ ...client,
57
+ client_id: randomUUID(),
58
+ client_id_issued_at: Math.floor(Date.now() / 1000),
59
+ client_secret_expires_at: 0, // never expires
60
+ };
61
+ this.clients.set(full.client_id, full);
62
+ return full;
63
+ }
64
+ }
65
+ export const clientStore = new InMemoryClientStore();
66
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/oauth/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /** Look up a user by email in user_profiles. */
2
+ export declare function findUserByEmail(email: string): Promise<{
3
+ id: string;
4
+ } | null>;
5
+ /** Find existing MCP key or create a new one. Returns the plain API key. */
6
+ export declare function findOrCreateMcpKey(userId: string): Promise<string>;
@@ -0,0 +1,89 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { encryptApiKey, hashApiKey } from "./crypto.js";
3
+ /** All read scopes needed by MCP tools — no write/admin scopes. */
4
+ const MCP_SCOPES = [
5
+ // Threads
6
+ "threads:read", "threads:feed",
7
+ // X
8
+ "x:profile", "x:timeline", "x:tweet", "x:search",
9
+ // Facebook
10
+ "fb:profile", "fb:posts", "fb:post", "fb:search", "fb:post_comments",
11
+ "fb:marketplace_browse", "fb:marketplace_search", "fb:marketplace_item",
12
+ // Reddit
13
+ "reddit:subreddit", "reddit:posts", "reddit:search", "reddit:post", "reddit:user",
14
+ // Instagram
15
+ "ig:tags", "ig:user", "ig:post", "ig:post_comments", "ig:user_posts",
16
+ // TikTok
17
+ "tiktok:video", "tiktok:user", "tiktok:videos", "tiktok:categories",
18
+ "tiktok:comments", "tiktok:search", "tiktok:subtitles",
19
+ // LinkedIn
20
+ "linkedin:company", "linkedin:jobs", "linkedin:job", "linkedin:post",
21
+ "linkedin:user", "linkedin:user-search",
22
+ // YouTube
23
+ "youtube:video", "youtube:channel", "youtube:search", "youtube:comments", "youtube:transcription",
24
+ // Dcard
25
+ "dcard:post", "dcard:comments", "dcard:forum", "dcard:forum_posts", "dcard:search", "dcard:persona",
26
+ // Job104
27
+ "job104:search", "job104:company", "job104:job",
28
+ // Google Maps
29
+ "gmaps:search", "gmaps:place",
30
+ ];
31
+ const SUPABASE_URL = process.env.SUPABASE_URL;
32
+ const SUPABASE_SECRET_KEY = process.env.SUPABASE_SECRET_KEY;
33
+ function supabaseHeaders() {
34
+ return {
35
+ apikey: SUPABASE_SECRET_KEY,
36
+ Authorization: `Bearer ${SUPABASE_SECRET_KEY}`,
37
+ "Content-Type": "application/json",
38
+ };
39
+ }
40
+ /** Look up a user by email in user_profiles. */
41
+ export async function findUserByEmail(email) {
42
+ const url = `${SUPABASE_URL}/rest/v1/user_profiles?email=eq.${encodeURIComponent(email)}&select=id`;
43
+ const res = await fetch(url, { headers: supabaseHeaders() });
44
+ if (!res.ok)
45
+ throw new Error(`Supabase user lookup failed: ${res.status}`);
46
+ const rows = (await res.json());
47
+ return rows.length > 0 ? rows[0] : null;
48
+ }
49
+ /** Find existing MCP key or create a new one. Returns the plain API key. */
50
+ export async function findOrCreateMcpKey(userId) {
51
+ // 1. Look for existing active MCP key with encrypted_key
52
+ const findUrl = `${SUPABASE_URL}/rest/v1/api_keys?user_id=eq.${userId}&name=eq.MCP Connector&is_active=eq.true&select=encrypted_key`;
53
+ const findRes = await fetch(findUrl, { headers: supabaseHeaders() });
54
+ if (!findRes.ok)
55
+ throw new Error(`Supabase key lookup failed: ${findRes.status}`);
56
+ const existing = (await findRes.json());
57
+ if (existing.length > 0 && existing[0].encrypted_key) {
58
+ const { decryptApiKey } = await import("./crypto.js");
59
+ return decryptApiKey(existing[0].encrypted_key);
60
+ }
61
+ // 2. Generate a new key
62
+ const plain = `sk_byc_${randomBytes(32).toString("hex")}`;
63
+ const keyHash = hashApiKey(plain);
64
+ const encrypted = encryptApiKey(plain);
65
+ const keyPrefix = plain.slice(0, 16); // matches dashboard crypto.ts
66
+ const insertUrl = `${SUPABASE_URL}/rest/v1/api_keys`;
67
+ const insertRes = await fetch(insertUrl, {
68
+ method: "POST",
69
+ headers: {
70
+ ...supabaseHeaders(),
71
+ Prefer: "return=minimal",
72
+ },
73
+ body: JSON.stringify({
74
+ user_id: userId,
75
+ name: "MCP Connector",
76
+ key_prefix: keyPrefix,
77
+ key_hash: keyHash,
78
+ encrypted_key: encrypted,
79
+ scopes: MCP_SCOPES,
80
+ is_active: true,
81
+ }),
82
+ });
83
+ if (!insertRes.ok) {
84
+ const body = await insertRes.text();
85
+ throw new Error(`Supabase key insert failed: ${insertRes.status} ${body}`);
86
+ }
87
+ return plain;
88
+ }
89
+ //# sourceMappingURL=user-keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-keys.js","sourceRoot":"","sources":["../../src/oauth/user-keys.ts"],"names":[],"mappings":"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"}
package/build/server.js CHANGED
@@ -4,13 +4,13 @@ import { registerXTools } from "./tools/x.js";
4
4
  import { registerFacebookTools } from "./tools/facebook.js";
5
5
  import { registerRedditTools } from "./tools/reddit.js";
6
6
  import { registerLinkedinTools } from "./tools/linkedin.js";
7
- import { registerXiaohongshuTools } from "./tools/xiaohongshu.js";
8
7
  import { registerTiktokTools } from "./tools/tiktok.js";
9
8
  import { registerInstagramTools } from "./tools/instagram.js";
10
9
  import { registerYoutubeTools } from "./tools/youtube.js";
11
10
  import { registerDcardTools } from "./tools/dcard.js";
12
11
  import { registerSystemTools } from "./tools/system.js";
13
12
  import { registerJob104Tools } from "./tools/job104.js";
13
+ import { registerGmapsTools } from "./tools/gmaps.js";
14
14
  export function createServer() {
15
15
  const server = new McpServer({
16
16
  name: "bycrawl",
@@ -21,12 +21,12 @@ export function createServer() {
21
21
  registerFacebookTools(server);
22
22
  registerRedditTools(server);
23
23
  registerLinkedinTools(server);
24
- registerXiaohongshuTools(server);
25
24
  registerTiktokTools(server);
26
25
  registerInstagramTools(server);
27
26
  registerYoutubeTools(server);
28
27
  registerDcardTools(server);
29
28
  registerJob104Tools(server);
29
+ registerGmapsTools(server);
30
30
  registerSystemTools(server);
31
31
  return server;
32
32
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAExD,MAAM,UAAU,YAAY;IAC1B,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IAEH,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,cAAc,CAAC,MAAM,CAAC,CAAC;IACvB,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC9B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC9B,wBAAwB,CAAC,MAAM,CAAC,CAAC;IACjC,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC/B,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC3B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAE5B,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtD,MAAM,UAAU,YAAY;IAC1B,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IAEH,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,cAAc,CAAC,MAAM,CAAC,CAAC;IACvB,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC9B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC9B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC/B,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC3B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC3B,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAE5B,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -1,25 +1,17 @@
1
1
  import { z } from "zod";
2
2
  import { bycrawlGet } from "../client.js";
3
3
  export function registerDcardTools(server) {
4
- server.tool("dcard_get_post", "Get a Dcard post by ID", { postId: z.string().describe("Dcard post ID") }, async ({ postId }) => bycrawlGet(`/dcard/posts/${postId}`));
5
- server.tool("dcard_get_post_comments", "Get comments for a Dcard post", {
6
- postId: z.string().describe("Dcard post ID"),
7
- sort: z
8
- .enum(["oldest", "newest", "popular"])
9
- .optional()
10
- .describe("Sort order"),
11
- }, async ({ postId, sort }) => bycrawlGet(`/dcard/posts/${postId}/comments`, { sort }));
12
- server.tool("dcard_get_forum", "Get Dcard forum info by alias", { alias: z.string().describe("Forum alias (e.g. trending, talk, mood)") }, async ({ alias }) => bycrawlGet(`/dcard/forums/${alias}`));
4
+ server.tool("dcard_get_forum", "Get Dcard forum info by alias", { alias: z.string().describe("Forum alias (e.g. trending, talk, mood)") }, { readOnlyHint: true }, async ({ alias }) => bycrawlGet(`/dcard/forums/${alias}`));
13
5
  server.tool("dcard_get_forum_posts", "Get posts from a Dcard forum", {
14
6
  alias: z.string().describe("Forum alias (e.g. trending, talk, mood)"),
15
- limit: z.number().optional().describe("Number of posts (1-30)"),
7
+ count: z.number().optional().describe("Number of posts (1-30)"),
16
8
  popular: z.boolean().optional().describe("Show popular posts only"),
17
- }, async ({ alias, limit, popular }) => bycrawlGet(`/dcard/forums/${alias}/posts`, { limit, popular }));
9
+ }, { readOnlyHint: true }, async ({ alias, count, popular }) => bycrawlGet(`/dcard/forums/${alias}/posts`, { count, popular }));
18
10
  server.tool("dcard_search_posts", "Search Dcard posts by keyword", {
19
11
  q: z.string().describe("Search query"),
20
- limit: z.number().optional().describe("Number of results (1-100)"),
12
+ count: z.number().optional().describe("Number of results (1-100)"),
21
13
  offset: z.number().optional().describe("Offset for pagination"),
22
- }, async ({ q, limit, offset }) => bycrawlGet("/dcard/search/posts", { q, limit, offset }));
23
- server.tool("dcard_get_persona", "Get a Dcard persona by UID", { uid: z.string().describe("Dcard persona UID") }, async ({ uid }) => bycrawlGet(`/dcard/personas/${uid}`));
14
+ }, { readOnlyHint: true }, async ({ q, count, offset }) => bycrawlGet("/dcard/posts/search", { q, count, offset }));
15
+ server.tool("dcard_get_persona", "Get a Dcard persona by username", { username: z.string().describe("Dcard username") }, { readOnlyHint: true }, async ({ username }) => bycrawlGet(`/dcard/personas/${username}`));
24
16
  }
25
17
  //# sourceMappingURL=dcard.js.map