xbird-mcp 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.
Potentially problematic release.
This version of xbird-mcp might be problematic. Click here for more details.
- package/docs/listing.md +42 -0
- package/docs/mcp-setup.md +146 -0
- package/package.json +50 -0
- package/src/cli/context.ts +80 -0
- package/src/cli/output.ts +33 -0
- package/src/cli/program.ts +63 -0
- package/src/cli.ts +8 -0
- package/src/commands/about.ts +30 -0
- package/src/commands/bookmark.ts +40 -0
- package/src/commands/bookmarks-list.ts +108 -0
- package/src/commands/check.ts +27 -0
- package/src/commands/engagement.ts +31 -0
- package/src/commands/follow.ts +40 -0
- package/src/commands/followers.ts +132 -0
- package/src/commands/home.ts +59 -0
- package/src/commands/likes.ts +73 -0
- package/src/commands/lists.ts +101 -0
- package/src/commands/mentions.ts +32 -0
- package/src/commands/news.ts +38 -0
- package/src/commands/read.ts +36 -0
- package/src/commands/replies.ts +59 -0
- package/src/commands/search.ts +60 -0
- package/src/commands/tweet.ts +128 -0
- package/src/commands/user-tweets.ts +73 -0
- package/src/commands/user.ts +20 -0
- package/src/commands/whoami.ts +26 -0
- package/src/formatters/common.ts +49 -0
- package/src/formatters/list.ts +22 -0
- package/src/formatters/news.ts +23 -0
- package/src/formatters/tweet.ts +38 -0
- package/src/formatters/user.ts +20 -0
- package/src/index.ts +26 -0
- package/src/lib/auth.ts +105 -0
- package/src/lib/client-bookmarks.ts +62 -0
- package/src/lib/client-engagement.ts +152 -0
- package/src/lib/client-follow.ts +90 -0
- package/src/lib/client-full.ts +48 -0
- package/src/lib/client-likes.ts +62 -0
- package/src/lib/client-lists.ts +227 -0
- package/src/lib/client-media.ts +162 -0
- package/src/lib/client-news.ts +185 -0
- package/src/lib/client-posting.ts +163 -0
- package/src/lib/client-reading.ts +452 -0
- package/src/lib/client-search.ts +156 -0
- package/src/lib/client-timeline.ts +98 -0
- package/src/lib/client-user.ts +518 -0
- package/src/lib/client.ts +134 -0
- package/src/lib/config.ts +22 -0
- package/src/lib/constants.ts +55 -0
- package/src/lib/cookies.ts +132 -0
- package/src/lib/features.ts +175 -0
- package/src/lib/headers.ts +28 -0
- package/src/lib/paginate.ts +39 -0
- package/src/lib/query-ids.ts +190 -0
- package/src/lib/types.ts +147 -0
- package/src/lib/utils.ts +176 -0
- package/src/mcp/executor.ts +178 -0
- package/src/mcp/payment.ts +38 -0
- package/src/mcp/server.ts +53 -0
- package/src/mcp/tools.ts +389 -0
- package/src/server/app.ts +117 -0
- package/src/server/config/accounts.ts +137 -0
- package/src/server/config/pricing.ts +217 -0
- package/src/server/erc8004/register.ts +77 -0
- package/src/server/middleware/account-pool.ts +101 -0
- package/src/server/middleware/error-handler.ts +27 -0
- package/src/server/middleware/payer-extract.ts +43 -0
- package/src/server/middleware/x402.ts +61 -0
- package/src/server/routes/accounts.ts +93 -0
- package/src/server/routes/authorize.ts +19 -0
- package/src/server/routes/bookmarks.ts +21 -0
- package/src/server/routes/engagement.ts +84 -0
- package/src/server/routes/follow.ts +32 -0
- package/src/server/routes/health.ts +14 -0
- package/src/server/routes/lists.ts +38 -0
- package/src/server/routes/media.ts +44 -0
- package/src/server/routes/mentions.ts +20 -0
- package/src/server/routes/news.ts +23 -0
- package/src/server/routes/search.ts +26 -0
- package/src/server/routes/timeline.ts +21 -0
- package/src/server/routes/tweets.ts +82 -0
- package/src/server/routes/users.ts +92 -0
- package/src/server/storage/accounts-db.ts +84 -0
- package/src/server.ts +34 -0
- package/tsconfig.json +29 -0
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
export interface Credentials {
|
|
2
|
+
authToken: string;
|
|
3
|
+
ct0: string;
|
|
4
|
+
source: "cli" | "env" | "browser";
|
|
5
|
+
browser?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface TwitterUser {
|
|
9
|
+
id: string;
|
|
10
|
+
screenName: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
followersCount: number;
|
|
14
|
+
followingCount: number;
|
|
15
|
+
tweetCount: number;
|
|
16
|
+
verified: boolean;
|
|
17
|
+
profileImageUrl: string;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
location?: string;
|
|
20
|
+
url?: string;
|
|
21
|
+
isBlueVerified: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Tweet {
|
|
25
|
+
id: string;
|
|
26
|
+
text: string;
|
|
27
|
+
authorId: string;
|
|
28
|
+
authorName: string;
|
|
29
|
+
authorHandle: string;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
likeCount: number;
|
|
32
|
+
retweetCount: number;
|
|
33
|
+
replyCount: number;
|
|
34
|
+
quoteCount: number;
|
|
35
|
+
bookmarkCount: number;
|
|
36
|
+
viewCount: number;
|
|
37
|
+
isRetweet: boolean;
|
|
38
|
+
isReply: boolean;
|
|
39
|
+
inReplyToId?: string;
|
|
40
|
+
conversationId?: string;
|
|
41
|
+
lang?: string;
|
|
42
|
+
media?: TweetMedia[];
|
|
43
|
+
urls?: TweetUrl[];
|
|
44
|
+
quotedTweet?: Tweet;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface TweetMedia {
|
|
48
|
+
type: "photo" | "video" | "animated_gif";
|
|
49
|
+
url: string;
|
|
50
|
+
width?: number;
|
|
51
|
+
height?: number;
|
|
52
|
+
videoUrl?: string;
|
|
53
|
+
durationMs?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TweetUrl {
|
|
57
|
+
url: string;
|
|
58
|
+
expandedUrl: string;
|
|
59
|
+
displayUrl: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SearchResult {
|
|
63
|
+
tweets: Tweet[];
|
|
64
|
+
cursor?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface TimelineResult {
|
|
68
|
+
tweets: Tweet[];
|
|
69
|
+
cursor?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ThreadResult {
|
|
73
|
+
mainTweet: Tweet;
|
|
74
|
+
replies: Tweet[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface QueryIdEntry {
|
|
78
|
+
queryId: string;
|
|
79
|
+
operationName: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface QueryIdCache {
|
|
83
|
+
version: number;
|
|
84
|
+
timestamp: number;
|
|
85
|
+
ids: Record<string, QueryIdEntry>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface GraphQLResponse<T = unknown> {
|
|
89
|
+
data?: T;
|
|
90
|
+
errors?: Array<{ message: string; code?: number }>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface TwitterList {
|
|
94
|
+
id: string;
|
|
95
|
+
name: string;
|
|
96
|
+
description: string;
|
|
97
|
+
memberCount: number;
|
|
98
|
+
subscriberCount: number;
|
|
99
|
+
isPrivate: boolean;
|
|
100
|
+
createdAt: string;
|
|
101
|
+
owner?: { id: string; screenName: string; name: string };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface NewsItem {
|
|
105
|
+
id: string;
|
|
106
|
+
headline: string;
|
|
107
|
+
category: string;
|
|
108
|
+
postCount?: number;
|
|
109
|
+
description?: string;
|
|
110
|
+
url?: string;
|
|
111
|
+
tweets?: Tweet[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface PaginatedResult<T> {
|
|
115
|
+
items: T[];
|
|
116
|
+
cursor?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface AboutInfo {
|
|
120
|
+
accountBasedIn?: string;
|
|
121
|
+
createdAt?: string;
|
|
122
|
+
source?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type OutputFormat = "human" | "json" | "json-full" | "plain";
|
|
126
|
+
|
|
127
|
+
export type ExtendedBrowserName = "chrome" | "edge" | "firefox" | "safari" | "arc" | "brave";
|
|
128
|
+
|
|
129
|
+
export interface XbirdConfig {
|
|
130
|
+
authToken?: string;
|
|
131
|
+
ct0?: string;
|
|
132
|
+
cookieSource?: ExtendedBrowserName;
|
|
133
|
+
format?: OutputFormat;
|
|
134
|
+
timeout?: number;
|
|
135
|
+
noColor?: boolean;
|
|
136
|
+
noEmoji?: boolean;
|
|
137
|
+
quoteDepth?: number;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface CLIContext {
|
|
141
|
+
credentials: Credentials;
|
|
142
|
+
format: OutputFormat;
|
|
143
|
+
noColor: boolean;
|
|
144
|
+
noEmoji: boolean;
|
|
145
|
+
quoteDepth: number;
|
|
146
|
+
timeoutMs: number;
|
|
147
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract tweet ID from a URL or return the ID directly.
|
|
3
|
+
* Supports: https://x.com/user/status/123, https://twitter.com/user/status/123, plain ID
|
|
4
|
+
*/
|
|
5
|
+
export function extractTweetId(input: string): string {
|
|
6
|
+
const trimmed = input.trim();
|
|
7
|
+
|
|
8
|
+
// Plain numeric ID
|
|
9
|
+
if (/^\d+$/.test(trimmed)) {
|
|
10
|
+
return trimmed;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// URL format
|
|
14
|
+
const match = trimmed.match(
|
|
15
|
+
/(?:twitter\.com|x\.com)\/\w+\/status\/(\d+)/
|
|
16
|
+
);
|
|
17
|
+
if (match?.[1]) {
|
|
18
|
+
return match[1];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
throw new Error(`Invalid tweet ID or URL: ${input}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Normalize a Twitter handle — strips leading @ and whitespace.
|
|
26
|
+
*/
|
|
27
|
+
export function normalizeHandle(handle: string): string {
|
|
28
|
+
return handle.trim().replace(/^@/, "");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build a tweet URL from handle and ID.
|
|
33
|
+
*/
|
|
34
|
+
export function tweetUrl(handle: string, id: string): string {
|
|
35
|
+
return `https://x.com/${normalizeHandle(handle)}/status/${id}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build a profile URL from handle.
|
|
40
|
+
*/
|
|
41
|
+
export function profileUrl(handle: string): string {
|
|
42
|
+
return `https://x.com/${normalizeHandle(handle)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Format a large number for human display (e.g. 1234 → "1.2K").
|
|
47
|
+
*/
|
|
48
|
+
export function formatCount(n: number): string {
|
|
49
|
+
if (n >= 1_000_000) {
|
|
50
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
51
|
+
}
|
|
52
|
+
if (n >= 1_000) {
|
|
53
|
+
return `${(n / 1_000).toFixed(1)}K`;
|
|
54
|
+
}
|
|
55
|
+
return String(n);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Format a date string to a relative or short format.
|
|
60
|
+
*/
|
|
61
|
+
export function formatDate(dateStr: string): string {
|
|
62
|
+
const date = new Date(dateStr);
|
|
63
|
+
const now = new Date();
|
|
64
|
+
const diffMs = now.getTime() - date.getTime();
|
|
65
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
66
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
67
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
68
|
+
const diffDay = Math.floor(diffHour / 24);
|
|
69
|
+
|
|
70
|
+
if (diffSec < 60) return `${diffSec}s`;
|
|
71
|
+
if (diffMin < 60) return `${diffMin}m`;
|
|
72
|
+
if (diffHour < 24) return `${diffHour}h`;
|
|
73
|
+
if (diffDay < 7) return `${diffDay}d`;
|
|
74
|
+
|
|
75
|
+
return date.toLocaleDateString("en-US", {
|
|
76
|
+
month: "short",
|
|
77
|
+
day: "numeric",
|
|
78
|
+
year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Truncate text to a max length with ellipsis.
|
|
84
|
+
*/
|
|
85
|
+
export function truncate(text: string, maxLen: number): string {
|
|
86
|
+
if (text.length <= maxLen) return text;
|
|
87
|
+
return text.slice(0, maxLen - 1) + "…";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Extract a cursor value from GraphQL timeline instructions.
|
|
92
|
+
* Looks for entries with entryId starting with `cursor-bottom-` or `cursor-top-`.
|
|
93
|
+
*/
|
|
94
|
+
export function extractCursorFromInstructions(
|
|
95
|
+
instructions: unknown[],
|
|
96
|
+
cursorType: "Bottom" | "Top" = "Bottom"
|
|
97
|
+
): string | undefined {
|
|
98
|
+
const prefix = `cursor-${cursorType.toLowerCase()}-`;
|
|
99
|
+
|
|
100
|
+
for (const inst of instructions ?? []) {
|
|
101
|
+
const instruction = inst as Record<string, unknown>;
|
|
102
|
+
|
|
103
|
+
// Check replaceEntry type (Twitter sometimes sends cursor updates this way)
|
|
104
|
+
if (instruction.type === "TimelineReplaceEntry") {
|
|
105
|
+
const entry = instruction.entry as Record<string, unknown> | undefined;
|
|
106
|
+
const entryId = entry?.entryId as string | undefined;
|
|
107
|
+
if (entryId?.startsWith(prefix)) {
|
|
108
|
+
const content = entry?.content as Record<string, unknown> | undefined;
|
|
109
|
+
return content?.value as string | undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const entries = (instruction.entries ?? []) as Array<Record<string, unknown>>;
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
const entryId = entry.entryId as string | undefined;
|
|
116
|
+
if (entryId?.startsWith(prefix)) {
|
|
117
|
+
const content = entry.content as Record<string, unknown> | undefined;
|
|
118
|
+
return content?.value as string | undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Parse user objects from GraphQL timeline instructions.
|
|
128
|
+
*/
|
|
129
|
+
export function parseUsersFromInstructions(instructions: unknown[]): import("./types.ts").TwitterUser[] {
|
|
130
|
+
const users: import("./types.ts").TwitterUser[] = [];
|
|
131
|
+
|
|
132
|
+
for (const inst of instructions ?? []) {
|
|
133
|
+
const instruction = inst as Record<string, unknown>;
|
|
134
|
+
const entries = (instruction.entries ?? []) as Array<Record<string, unknown>>;
|
|
135
|
+
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
const content = entry.content as Record<string, unknown> | undefined;
|
|
138
|
+
const itemContent = content?.itemContent as Record<string, unknown> | undefined;
|
|
139
|
+
const userResults = itemContent?.user_results as Record<string, unknown> | undefined;
|
|
140
|
+
const result = userResults?.result as Record<string, unknown> | undefined;
|
|
141
|
+
|
|
142
|
+
if (!result) continue;
|
|
143
|
+
|
|
144
|
+
const legacy = result.legacy as Record<string, unknown> | undefined;
|
|
145
|
+
const core = result.core as Record<string, unknown> | undefined;
|
|
146
|
+
const avatar = result.avatar as Record<string, unknown> | undefined;
|
|
147
|
+
const locationObj = result.location as Record<string, unknown> | undefined;
|
|
148
|
+
|
|
149
|
+
if (!legacy && !core) continue;
|
|
150
|
+
|
|
151
|
+
users.push({
|
|
152
|
+
id: String(result.rest_id ?? ""),
|
|
153
|
+
screenName: String(core?.screen_name ?? legacy?.screen_name ?? ""),
|
|
154
|
+
name: String(core?.name ?? legacy?.name ?? ""),
|
|
155
|
+
description: String(legacy?.description ?? ""),
|
|
156
|
+
followersCount: Number(legacy?.followers_count ?? 0),
|
|
157
|
+
followingCount: Number(legacy?.friends_count ?? 0),
|
|
158
|
+
tweetCount: Number(legacy?.statuses_count ?? 0),
|
|
159
|
+
verified: Boolean(legacy?.verified),
|
|
160
|
+
profileImageUrl: String(
|
|
161
|
+
avatar?.image_url ?? legacy?.profile_image_url_https ?? legacy?.profile_image_url ?? ""
|
|
162
|
+
),
|
|
163
|
+
createdAt: String(core?.created_at ?? legacy?.created_at ?? ""),
|
|
164
|
+
location: locationObj?.location
|
|
165
|
+
? String(locationObj.location)
|
|
166
|
+
: legacy?.location
|
|
167
|
+
? String(legacy.location)
|
|
168
|
+
: undefined,
|
|
169
|
+
url: legacy?.url ? String(legacy.url) : undefined,
|
|
170
|
+
isBlueVerified: Boolean(result.is_blue_verified),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return users;
|
|
176
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { XClient } from "../lib/client-full.ts";
|
|
2
|
+
import type { PaymentHelper } from "./payment.ts";
|
|
3
|
+
import type { PriceTier } from "../server/config/pricing.ts";
|
|
4
|
+
|
|
5
|
+
export class PaidExecutor {
|
|
6
|
+
constructor(
|
|
7
|
+
private client: XClient,
|
|
8
|
+
private pay: PaymentHelper
|
|
9
|
+
) {}
|
|
10
|
+
|
|
11
|
+
private async authorize(tier: PriceTier): Promise<void> {
|
|
12
|
+
await this.pay.authorize(tier);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ── Read tier ($0.001) ──────────────────────────────────
|
|
16
|
+
|
|
17
|
+
async getTweet(id: string) {
|
|
18
|
+
await this.authorize("read");
|
|
19
|
+
return this.client.getTweet(id);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async getThread(id: string) {
|
|
23
|
+
await this.authorize("read");
|
|
24
|
+
return this.client.getThread(id);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async getReplies(id: string, count?: number, cursor?: string) {
|
|
28
|
+
await this.authorize("read");
|
|
29
|
+
return this.client.getReplies(id, count, cursor);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async getUserByHandle(handle: string) {
|
|
33
|
+
await this.authorize("read");
|
|
34
|
+
return this.client.getUserByHandle(handle);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async getAbout(handle: string) {
|
|
38
|
+
await this.authorize("read");
|
|
39
|
+
return this.client.getAbout(handle);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async getCurrentUser() {
|
|
43
|
+
await this.authorize("read");
|
|
44
|
+
return this.client.getCurrentUser();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async home(count?: number, cursor?: string) {
|
|
48
|
+
await this.authorize("read");
|
|
49
|
+
return this.client.home(count, cursor);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async getNews(count?: number, opts?: { tab?: string; aiOnly?: boolean }) {
|
|
53
|
+
await this.authorize("read");
|
|
54
|
+
return this.client.getNews(count, opts);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async getOwnedLists() {
|
|
58
|
+
await this.authorize("read");
|
|
59
|
+
return this.client.getOwnedLists();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async getListTimeline(id: string, count?: number, cursor?: string) {
|
|
63
|
+
await this.authorize("read");
|
|
64
|
+
return this.client.getListTimeline(id, count, cursor);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Search tier ($0.005) ────────────────────────────────
|
|
68
|
+
|
|
69
|
+
async search(query: string, count?: number, cursor?: string) {
|
|
70
|
+
await this.authorize("search");
|
|
71
|
+
return this.client.search(query, count, cursor);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async mentions(handle: string, count?: number) {
|
|
75
|
+
await this.authorize("search");
|
|
76
|
+
return this.client.mentions(handle, count);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Bulk tier ($0.01) ───────────────────────────────────
|
|
80
|
+
|
|
81
|
+
async getUserTweets(userId: string, count?: number, cursor?: string) {
|
|
82
|
+
await this.authorize("bulk");
|
|
83
|
+
return this.client.getUserTweets(userId, count, cursor);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async getFollowers(userId: string, count?: number, cursor?: string) {
|
|
87
|
+
await this.authorize("bulk");
|
|
88
|
+
return this.client.getFollowers(userId, count, cursor);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async getFollowing(userId: string, count?: number, cursor?: string) {
|
|
92
|
+
await this.authorize("bulk");
|
|
93
|
+
return this.client.getFollowing(userId, count, cursor);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async getLikes(userId: string, count?: number, cursor?: string) {
|
|
97
|
+
await this.authorize("bulk");
|
|
98
|
+
return this.client.getLikes(userId, count, cursor);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async getBookmarks(count?: number, cursor?: string, folderId?: string) {
|
|
102
|
+
await this.authorize("bulk");
|
|
103
|
+
return this.client.getBookmarks(count, cursor, folderId);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async getListMemberships() {
|
|
107
|
+
await this.authorize("bulk");
|
|
108
|
+
return this.client.getListMemberships();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Write tier ($0.01) ──────────────────────────────────
|
|
112
|
+
|
|
113
|
+
async tweet(text: string, mediaIds?: string[]) {
|
|
114
|
+
await this.authorize("write");
|
|
115
|
+
return this.client.tweet(text, mediaIds);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async reply(text: string, replyToId: string, mediaIds?: string[]) {
|
|
119
|
+
await this.authorize("write");
|
|
120
|
+
return this.client.reply(text, replyToId, mediaIds);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async thread(texts: string[]) {
|
|
124
|
+
await this.authorize("write");
|
|
125
|
+
return this.client.thread(texts);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async like(tweetId: string) {
|
|
129
|
+
await this.authorize("write");
|
|
130
|
+
return this.client.like(tweetId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async unlike(tweetId: string) {
|
|
134
|
+
await this.authorize("write");
|
|
135
|
+
return this.client.unlike(tweetId);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async retweet(tweetId: string) {
|
|
139
|
+
await this.authorize("write");
|
|
140
|
+
return this.client.retweet(tweetId);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async unretweet(tweetId: string) {
|
|
144
|
+
await this.authorize("write");
|
|
145
|
+
return this.client.unretweet(tweetId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async bookmark(tweetId: string) {
|
|
149
|
+
await this.authorize("write");
|
|
150
|
+
return this.client.bookmark(tweetId);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async unbookmark(tweetId: string) {
|
|
154
|
+
await this.authorize("write");
|
|
155
|
+
return this.client.unbookmark(tweetId);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async follow(handle: string) {
|
|
159
|
+
await this.authorize("write");
|
|
160
|
+
return this.client.follow(handle);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async unfollow(handle: string) {
|
|
164
|
+
await this.authorize("write");
|
|
165
|
+
return this.client.unfollow(handle);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Media tier ($0.05) ──────────────────────────────────
|
|
169
|
+
|
|
170
|
+
async uploadMedia(opts: {
|
|
171
|
+
data: Uint8Array | Buffer;
|
|
172
|
+
mimeType: string;
|
|
173
|
+
alt?: string;
|
|
174
|
+
}) {
|
|
175
|
+
await this.authorize("media");
|
|
176
|
+
return this.client.uploadMedia(opts);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { wrapFetchWithPayment, x402Client } from "@x402/fetch";
|
|
2
|
+
import { registerExactEvmScheme } from "@x402/evm/exact/client";
|
|
3
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
4
|
+
import type { PriceTier } from "../server/config/pricing.ts";
|
|
5
|
+
|
|
6
|
+
export interface PaymentHelper {
|
|
7
|
+
authorize(tier: PriceTier): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createPaymentHelper(
|
|
11
|
+
privateKey: `0x${string}`,
|
|
12
|
+
serverUrl: string
|
|
13
|
+
): PaymentHelper {
|
|
14
|
+
const account = privateKeyToAccount(privateKey);
|
|
15
|
+
const client = new x402Client();
|
|
16
|
+
registerExactEvmScheme(client, { signer: account });
|
|
17
|
+
const paymentFetch = wrapFetchWithPayment(fetch, client);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
async authorize(tier: PriceTier): Promise<void> {
|
|
21
|
+
const res = await paymentFetch(`${serverUrl}/api/authorize/${tier}`, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
const body = await res.text().catch(() => "");
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Payment failed for tier "${tier}": ${res.status} ${res.statusText} ${body}`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const data = (await res.json()) as { authorized?: boolean };
|
|
33
|
+
if (!data.authorized) {
|
|
34
|
+
throw new Error(`Authorization denied for tier "${tier}"`);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { XClient } from "../lib/client-full.ts";
|
|
5
|
+
import { createPaymentHelper } from "./payment.ts";
|
|
6
|
+
import { PaidExecutor } from "./executor.ts";
|
|
7
|
+
import { registerTools } from "./tools.ts";
|
|
8
|
+
|
|
9
|
+
const AUTH_TOKEN = process.env.XBIRD_AUTH_TOKEN;
|
|
10
|
+
const CT0 = process.env.XBIRD_CT0;
|
|
11
|
+
const PRIVATE_KEY = process.env.XBIRD_PRIVATE_KEY as `0x${string}` | undefined;
|
|
12
|
+
const SERVER_URL =
|
|
13
|
+
process.env.XBIRD_SERVER_URL ?? "https://xbirdapi.up.railway.app";
|
|
14
|
+
|
|
15
|
+
if (!AUTH_TOKEN || !CT0) {
|
|
16
|
+
console.error(
|
|
17
|
+
"Missing required env vars: XBIRD_AUTH_TOKEN and XBIRD_CT0 (Twitter credentials)"
|
|
18
|
+
);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!PRIVATE_KEY) {
|
|
23
|
+
console.error(
|
|
24
|
+
"Missing required env var: XBIRD_PRIVATE_KEY (wallet private key for x402 payments)"
|
|
25
|
+
);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Initialize Twitter client with local credentials
|
|
30
|
+
const client = new XClient({
|
|
31
|
+
authToken: AUTH_TOKEN,
|
|
32
|
+
ct0: CT0,
|
|
33
|
+
source: "env",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Initialize payment helper
|
|
37
|
+
const payment = createPaymentHelper(PRIVATE_KEY, SERVER_URL);
|
|
38
|
+
|
|
39
|
+
// Create executor (XClient + payment)
|
|
40
|
+
const executor = new PaidExecutor(client, payment);
|
|
41
|
+
|
|
42
|
+
// Create MCP server
|
|
43
|
+
const server = new McpServer({
|
|
44
|
+
name: "xbird",
|
|
45
|
+
version: "0.1.0",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Register all 30 tools
|
|
49
|
+
registerTools(server, executor);
|
|
50
|
+
|
|
51
|
+
// Start stdio transport
|
|
52
|
+
const transport = new StdioServerTransport();
|
|
53
|
+
await server.connect(transport);
|