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.

Files changed (85) hide show
  1. package/docs/listing.md +42 -0
  2. package/docs/mcp-setup.md +146 -0
  3. package/package.json +50 -0
  4. package/src/cli/context.ts +80 -0
  5. package/src/cli/output.ts +33 -0
  6. package/src/cli/program.ts +63 -0
  7. package/src/cli.ts +8 -0
  8. package/src/commands/about.ts +30 -0
  9. package/src/commands/bookmark.ts +40 -0
  10. package/src/commands/bookmarks-list.ts +108 -0
  11. package/src/commands/check.ts +27 -0
  12. package/src/commands/engagement.ts +31 -0
  13. package/src/commands/follow.ts +40 -0
  14. package/src/commands/followers.ts +132 -0
  15. package/src/commands/home.ts +59 -0
  16. package/src/commands/likes.ts +73 -0
  17. package/src/commands/lists.ts +101 -0
  18. package/src/commands/mentions.ts +32 -0
  19. package/src/commands/news.ts +38 -0
  20. package/src/commands/read.ts +36 -0
  21. package/src/commands/replies.ts +59 -0
  22. package/src/commands/search.ts +60 -0
  23. package/src/commands/tweet.ts +128 -0
  24. package/src/commands/user-tweets.ts +73 -0
  25. package/src/commands/user.ts +20 -0
  26. package/src/commands/whoami.ts +26 -0
  27. package/src/formatters/common.ts +49 -0
  28. package/src/formatters/list.ts +22 -0
  29. package/src/formatters/news.ts +23 -0
  30. package/src/formatters/tweet.ts +38 -0
  31. package/src/formatters/user.ts +20 -0
  32. package/src/index.ts +26 -0
  33. package/src/lib/auth.ts +105 -0
  34. package/src/lib/client-bookmarks.ts +62 -0
  35. package/src/lib/client-engagement.ts +152 -0
  36. package/src/lib/client-follow.ts +90 -0
  37. package/src/lib/client-full.ts +48 -0
  38. package/src/lib/client-likes.ts +62 -0
  39. package/src/lib/client-lists.ts +227 -0
  40. package/src/lib/client-media.ts +162 -0
  41. package/src/lib/client-news.ts +185 -0
  42. package/src/lib/client-posting.ts +163 -0
  43. package/src/lib/client-reading.ts +452 -0
  44. package/src/lib/client-search.ts +156 -0
  45. package/src/lib/client-timeline.ts +98 -0
  46. package/src/lib/client-user.ts +518 -0
  47. package/src/lib/client.ts +134 -0
  48. package/src/lib/config.ts +22 -0
  49. package/src/lib/constants.ts +55 -0
  50. package/src/lib/cookies.ts +132 -0
  51. package/src/lib/features.ts +175 -0
  52. package/src/lib/headers.ts +28 -0
  53. package/src/lib/paginate.ts +39 -0
  54. package/src/lib/query-ids.ts +190 -0
  55. package/src/lib/types.ts +147 -0
  56. package/src/lib/utils.ts +176 -0
  57. package/src/mcp/executor.ts +178 -0
  58. package/src/mcp/payment.ts +38 -0
  59. package/src/mcp/server.ts +53 -0
  60. package/src/mcp/tools.ts +389 -0
  61. package/src/server/app.ts +117 -0
  62. package/src/server/config/accounts.ts +137 -0
  63. package/src/server/config/pricing.ts +217 -0
  64. package/src/server/erc8004/register.ts +77 -0
  65. package/src/server/middleware/account-pool.ts +101 -0
  66. package/src/server/middleware/error-handler.ts +27 -0
  67. package/src/server/middleware/payer-extract.ts +43 -0
  68. package/src/server/middleware/x402.ts +61 -0
  69. package/src/server/routes/accounts.ts +93 -0
  70. package/src/server/routes/authorize.ts +19 -0
  71. package/src/server/routes/bookmarks.ts +21 -0
  72. package/src/server/routes/engagement.ts +84 -0
  73. package/src/server/routes/follow.ts +32 -0
  74. package/src/server/routes/health.ts +14 -0
  75. package/src/server/routes/lists.ts +38 -0
  76. package/src/server/routes/media.ts +44 -0
  77. package/src/server/routes/mentions.ts +20 -0
  78. package/src/server/routes/news.ts +23 -0
  79. package/src/server/routes/search.ts +26 -0
  80. package/src/server/routes/timeline.ts +21 -0
  81. package/src/server/routes/tweets.ts +82 -0
  82. package/src/server/routes/users.ts +92 -0
  83. package/src/server/storage/accounts-db.ts +84 -0
  84. package/src/server.ts +34 -0
  85. package/tsconfig.json +29 -0
@@ -0,0 +1,152 @@
1
+ import { TwitterClient } from "./client.ts";
2
+
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ type Constructor<T = TwitterClient> = new (...args: any[]) => T;
5
+
6
+ export function withEngagement<T extends Constructor>(Base: T) {
7
+ return class extends Base {
8
+ /**
9
+ * Like a tweet.
10
+ */
11
+ async like(
12
+ tweetId: string
13
+ ): Promise<{ success: boolean; error?: string }> {
14
+ try {
15
+ const { errors } = await this.graphqlPost("FavoriteTweet", {
16
+ tweet_id: tweetId,
17
+ });
18
+
19
+ if (errors?.length) {
20
+ return { success: false, error: this.formatErrors(errors) };
21
+ }
22
+
23
+ return { success: true };
24
+ } catch (err) {
25
+ return {
26
+ success: false,
27
+ error: err instanceof Error ? err.message : String(err),
28
+ };
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Unlike a tweet.
34
+ */
35
+ async unlike(
36
+ tweetId: string
37
+ ): Promise<{ success: boolean; error?: string }> {
38
+ try {
39
+ const { errors } = await this.graphqlPost("UnfavoriteTweet", {
40
+ tweet_id: tweetId,
41
+ });
42
+
43
+ if (errors?.length) {
44
+ return { success: false, error: this.formatErrors(errors) };
45
+ }
46
+
47
+ return { success: true };
48
+ } catch (err) {
49
+ return {
50
+ success: false,
51
+ error: err instanceof Error ? err.message : String(err),
52
+ };
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Retweet a tweet.
58
+ */
59
+ async retweet(
60
+ tweetId: string
61
+ ): Promise<{ success: boolean; error?: string }> {
62
+ try {
63
+ const { errors } = await this.graphqlPost("CreateRetweet", {
64
+ tweet_id: tweetId,
65
+ });
66
+
67
+ if (errors?.length) {
68
+ return { success: false, error: this.formatErrors(errors) };
69
+ }
70
+
71
+ return { success: true };
72
+ } catch (err) {
73
+ return {
74
+ success: false,
75
+ error: err instanceof Error ? err.message : String(err),
76
+ };
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Undo a retweet.
82
+ */
83
+ async unretweet(
84
+ tweetId: string
85
+ ): Promise<{ success: boolean; error?: string }> {
86
+ try {
87
+ const { errors } = await this.graphqlPost("DeleteRetweet", {
88
+ source_tweet_id: tweetId,
89
+ });
90
+
91
+ if (errors?.length) {
92
+ return { success: false, error: this.formatErrors(errors) };
93
+ }
94
+
95
+ return { success: true };
96
+ } catch (err) {
97
+ return {
98
+ success: false,
99
+ error: err instanceof Error ? err.message : String(err),
100
+ };
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Bookmark a tweet.
106
+ */
107
+ async bookmark(
108
+ tweetId: string
109
+ ): Promise<{ success: boolean; error?: string }> {
110
+ try {
111
+ const { errors } = await this.graphqlPost("CreateBookmark", {
112
+ tweet_id: tweetId,
113
+ });
114
+
115
+ if (errors?.length) {
116
+ return { success: false, error: this.formatErrors(errors) };
117
+ }
118
+
119
+ return { success: true };
120
+ } catch (err) {
121
+ return {
122
+ success: false,
123
+ error: err instanceof Error ? err.message : String(err),
124
+ };
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Remove a tweet from bookmarks.
130
+ */
131
+ async unbookmark(
132
+ tweetId: string
133
+ ): Promise<{ success: boolean; error?: string }> {
134
+ try {
135
+ const { errors } = await this.graphqlPost("DeleteBookmark", {
136
+ tweet_id: tweetId,
137
+ });
138
+
139
+ if (errors?.length) {
140
+ return { success: false, error: this.formatErrors(errors) };
141
+ }
142
+
143
+ return { success: true };
144
+ } catch (err) {
145
+ return {
146
+ success: false,
147
+ error: err instanceof Error ? err.message : String(err),
148
+ };
149
+ }
150
+ }
151
+ };
152
+ }
@@ -0,0 +1,90 @@
1
+ import { TwitterClient } from "./client.ts";
2
+ import { TWITTER_V1_BASE } from "./constants.ts";
3
+ import { buildBaseHeaders } from "./headers.ts";
4
+
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ type Constructor<T = TwitterClient> = new (...args: any[]) => T;
7
+
8
+ export function withFollow<T extends Constructor>(Base: T) {
9
+ return class extends Base {
10
+ /**
11
+ * Follow a user by their screen name.
12
+ */
13
+ async follow(
14
+ handle: string
15
+ ): Promise<{ success: boolean; error?: string }> {
16
+ try {
17
+ const headers = buildBaseHeaders(this.credentials);
18
+ headers["content-type"] = "application/x-www-form-urlencoded";
19
+
20
+ const body = new URLSearchParams({
21
+ screen_name: handle,
22
+ });
23
+
24
+ const res = await this.fetchWithTimeout(
25
+ `${TWITTER_V1_BASE}/friendships/create.json`,
26
+ {
27
+ method: "POST",
28
+ headers,
29
+ body: body.toString(),
30
+ }
31
+ );
32
+
33
+ if (!res.ok) {
34
+ const text = await res.text();
35
+ return {
36
+ success: false,
37
+ error: `HTTP ${res.status}: ${text.slice(0, 200)}`,
38
+ };
39
+ }
40
+
41
+ return { success: true };
42
+ } catch (err) {
43
+ return {
44
+ success: false,
45
+ error: err instanceof Error ? err.message : String(err),
46
+ };
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Unfollow a user by their screen name.
52
+ */
53
+ async unfollow(
54
+ handle: string
55
+ ): Promise<{ success: boolean; error?: string }> {
56
+ try {
57
+ const headers = buildBaseHeaders(this.credentials);
58
+ headers["content-type"] = "application/x-www-form-urlencoded";
59
+
60
+ const body = new URLSearchParams({
61
+ screen_name: handle,
62
+ });
63
+
64
+ const res = await this.fetchWithTimeout(
65
+ `${TWITTER_V1_BASE}/friendships/destroy.json`,
66
+ {
67
+ method: "POST",
68
+ headers,
69
+ body: body.toString(),
70
+ }
71
+ );
72
+
73
+ if (!res.ok) {
74
+ const text = await res.text();
75
+ return {
76
+ success: false,
77
+ error: `HTTP ${res.status}: ${text.slice(0, 200)}`,
78
+ };
79
+ }
80
+
81
+ return { success: true };
82
+ } catch (err) {
83
+ return {
84
+ success: false,
85
+ error: err instanceof Error ? err.message : String(err),
86
+ };
87
+ }
88
+ }
89
+ };
90
+ }
@@ -0,0 +1,48 @@
1
+ import { TwitterClient } from "./client.ts";
2
+ import { withUser } from "./client-user.ts";
3
+ import { withReading } from "./client-reading.ts";
4
+ import { withPosting } from "./client-posting.ts";
5
+ import { withSearch } from "./client-search.ts";
6
+ import { withTimeline } from "./client-timeline.ts";
7
+ import { withEngagement } from "./client-engagement.ts";
8
+ import { withFollow } from "./client-follow.ts";
9
+ import { withLists } from "./client-lists.ts";
10
+ import { withNews } from "./client-news.ts";
11
+ import { withBookmarks } from "./client-bookmarks.ts";
12
+ import { withLikes } from "./client-likes.ts";
13
+ import { withMedia } from "./client-media.ts";
14
+
15
+ /**
16
+ * Full-featured Twitter/X client composed from all mixins.
17
+ *
18
+ * Composition order matters: withTimeline depends on withSearch (for mentions()),
19
+ * so withSearch must be applied before withTimeline.
20
+ */
21
+ const FullClient = withMedia(
22
+ withLikes(
23
+ withBookmarks(
24
+ withNews(
25
+ withLists(
26
+ withFollow(
27
+ withEngagement(
28
+ withTimeline(
29
+ withSearch(
30
+ withPosting(
31
+ withReading(
32
+ withUser(TwitterClient)
33
+ )
34
+ )
35
+ )
36
+ )
37
+ )
38
+ )
39
+ )
40
+ )
41
+ )
42
+ )
43
+ );
44
+
45
+ export class XClient extends FullClient {}
46
+
47
+ // Re-export the parsers for use elsewhere
48
+ export { parseTweetResult, parseTweetsFromInstructions } from "./client-reading.ts";
@@ -0,0 +1,62 @@
1
+ import { TwitterClient } from "./client.ts";
2
+ import { likesFeatures, fieldToggles } from "./features.ts";
3
+ import { parseTweetsFromInstructions } from "./client-reading.ts";
4
+ import { extractCursorFromInstructions } from "./utils.ts";
5
+ import type { Tweet } from "./types.ts";
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ type Constructor<T = TwitterClient> = new (...args: any[]) => T;
9
+
10
+ export function withLikes<T extends Constructor>(Base: T) {
11
+ return class extends Base {
12
+ async getLikes(
13
+ userId: string,
14
+ count?: number,
15
+ cursor?: string
16
+ ): Promise<{
17
+ success: boolean;
18
+ tweets: Tweet[];
19
+ cursor?: string;
20
+ error?: string;
21
+ }> {
22
+ try {
23
+ const variables: Record<string, unknown> = {
24
+ userId,
25
+ count: count ?? 20,
26
+ includePromotedContent: false,
27
+ };
28
+ if (cursor) variables.cursor = cursor;
29
+
30
+ const { data, errors } = await this.graphqlGet(
31
+ "Likes",
32
+ variables,
33
+ likesFeatures(),
34
+ fieldToggles()
35
+ );
36
+
37
+ if (errors?.length) {
38
+ return { success: false, tweets: [], error: this.formatErrors(errors) };
39
+ }
40
+
41
+ const userResult = (data as Record<string, unknown>)?.user as Record<string, unknown> | undefined;
42
+ const result = userResult?.result as Record<string, unknown> | undefined;
43
+ const timeline = result?.timeline_v2 as Record<string, unknown> | undefined
44
+ ?? result?.timeline as Record<string, unknown> | undefined;
45
+ const tl = timeline?.timeline as Record<string, unknown> | undefined;
46
+ const instructions = tl?.instructions as unknown[] | undefined;
47
+
48
+ if (!instructions) return { success: true, tweets: [] };
49
+
50
+ const tweets = parseTweetsFromInstructions(instructions);
51
+ const nextCursor = extractCursorFromInstructions(instructions);
52
+ return { success: true, tweets, cursor: nextCursor };
53
+ } catch (err) {
54
+ return {
55
+ success: false,
56
+ tweets: [],
57
+ error: err instanceof Error ? err.message : String(err),
58
+ };
59
+ }
60
+ }
61
+ };
62
+ }
@@ -0,0 +1,227 @@
1
+ import { TwitterClient } from "./client.ts";
2
+ import { listFeatures, fieldToggles } from "./features.ts";
3
+ import { parseTweetsFromInstructions } from "./client-reading.ts";
4
+ import { extractCursorFromInstructions } from "./utils.ts";
5
+ import type { Tweet, TwitterList, PaginatedResult } from "./types.ts";
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ type Constructor<T = TwitterClient> = new (...args: any[]) => T;
9
+
10
+ function parseListResult(result: unknown): TwitterList | null {
11
+ const raw = result as Record<string, unknown> | null | undefined;
12
+ if (!raw) return null;
13
+
14
+ const legacy = raw.legacy as Record<string, unknown> | undefined;
15
+ if (!legacy) return null;
16
+
17
+ const ownerResults = (raw.core ?? raw.user_results) as Record<string, unknown> | undefined;
18
+ const ownerResultA = (ownerResults?.user_results as Record<string, unknown> | undefined)?.result as Record<string, unknown> | undefined;
19
+ const ownerResult = ownerResultA ?? ownerResults?.result as Record<string, unknown> | undefined;
20
+ const ownerLegacy = ownerResult?.legacy as Record<string, unknown> | undefined;
21
+ const ownerCore = ownerResult?.core as Record<string, unknown> | undefined;
22
+
23
+ let owner: TwitterList["owner"];
24
+ if (ownerResult) {
25
+ owner = {
26
+ id: String(ownerResult.rest_id ?? ""),
27
+ screenName: String(ownerCore?.screen_name ?? ownerLegacy?.screen_name ?? ""),
28
+ name: String(ownerCore?.name ?? ownerLegacy?.name ?? ""),
29
+ };
30
+ }
31
+
32
+ return {
33
+ id: String(raw.id_str ?? raw.rest_id ?? legacy.id_str ?? ""),
34
+ name: String(legacy.name ?? raw.name ?? ""),
35
+ description: String(legacy.description ?? ""),
36
+ memberCount: Number(legacy.member_count ?? 0),
37
+ subscriberCount: Number(legacy.subscriber_count ?? 0),
38
+ isPrivate: String(legacy.mode ?? "public") === "private",
39
+ createdAt: String(legacy.created_at ?? ""),
40
+ owner,
41
+ };
42
+ }
43
+
44
+ function parseListsFromInstructions(instructions: unknown[]): TwitterList[] {
45
+ const lists: TwitterList[] = [];
46
+
47
+ for (const inst of instructions ?? []) {
48
+ const instruction = inst as Record<string, unknown>;
49
+ const entries = (instruction.entries ?? []) as Array<Record<string, unknown>>;
50
+
51
+ for (const entry of entries) {
52
+ const content = entry.content as Record<string, unknown> | undefined;
53
+ const itemContent = content?.itemContent as Record<string, unknown> | undefined;
54
+ const listResults = itemContent?.list_results as Record<string, unknown> | undefined;
55
+ const listResult = (itemContent?.list as Record<string, unknown> | undefined)
56
+ ?? (listResults?.result as Record<string, unknown> | undefined);
57
+
58
+ if (listResult) {
59
+ const list = parseListResult(listResult);
60
+ if (list) lists.push(list);
61
+ }
62
+ }
63
+ }
64
+
65
+ return lists;
66
+ }
67
+
68
+ export function withLists<T extends Constructor>(Base: T) {
69
+ return class extends Base {
70
+ async getOwnedLists(): Promise<{
71
+ success: boolean;
72
+ lists: TwitterList[];
73
+ error?: string;
74
+ }> {
75
+ try {
76
+ // First get current user ID
77
+ const self = this as unknown as {
78
+ getCurrentUser: () => Promise<{
79
+ success: boolean;
80
+ user?: { id: string };
81
+ }>;
82
+ };
83
+
84
+ let userId: string | undefined;
85
+ if (typeof self.getCurrentUser === "function") {
86
+ const current = await self.getCurrentUser();
87
+ userId = current.user?.id;
88
+ }
89
+ if (!userId) {
90
+ return { success: false, lists: [], error: "Could not determine user ID" };
91
+ }
92
+
93
+ const { data, errors } = await this.graphqlGet(
94
+ "ListOwnerships",
95
+ {
96
+ userId,
97
+ count: 100,
98
+ isListMemberTargetUserId: "0",
99
+ },
100
+ listFeatures()
101
+ );
102
+
103
+ if (errors?.length) {
104
+ return { success: false, lists: [], error: this.formatErrors(errors) };
105
+ }
106
+
107
+ const userResult = (data as Record<string, unknown>)?.user as Record<string, unknown> | undefined;
108
+ const result = userResult?.result as Record<string, unknown> | undefined;
109
+ const timeline = result?.timeline as Record<string, unknown> | undefined;
110
+ const tl = timeline?.timeline as Record<string, unknown> | undefined;
111
+ const instructions = tl?.instructions as unknown[] | undefined;
112
+
113
+ if (!instructions) return { success: true, lists: [] };
114
+
115
+ const lists = parseListsFromInstructions(instructions);
116
+ return { success: true, lists };
117
+ } catch (err) {
118
+ return {
119
+ success: false,
120
+ lists: [],
121
+ error: err instanceof Error ? err.message : String(err),
122
+ };
123
+ }
124
+ }
125
+
126
+ async getListMemberships(): Promise<{
127
+ success: boolean;
128
+ lists: TwitterList[];
129
+ error?: string;
130
+ }> {
131
+ try {
132
+ const self = this as unknown as {
133
+ getCurrentUser: () => Promise<{
134
+ success: boolean;
135
+ user?: { id: string };
136
+ }>;
137
+ };
138
+
139
+ let userId: string | undefined;
140
+ if (typeof self.getCurrentUser === "function") {
141
+ const current = await self.getCurrentUser();
142
+ userId = current.user?.id;
143
+ }
144
+ if (!userId) {
145
+ return { success: false, lists: [], error: "Could not determine user ID" };
146
+ }
147
+
148
+ const { data, errors } = await this.graphqlGet(
149
+ "ListMemberships",
150
+ {
151
+ userId,
152
+ count: 100,
153
+ },
154
+ listFeatures()
155
+ );
156
+
157
+ if (errors?.length) {
158
+ return { success: false, lists: [], error: this.formatErrors(errors) };
159
+ }
160
+
161
+ const userResult = (data as Record<string, unknown>)?.user as Record<string, unknown> | undefined;
162
+ const result = userResult?.result as Record<string, unknown> | undefined;
163
+ const timeline = result?.timeline as Record<string, unknown> | undefined;
164
+ const tl = timeline?.timeline as Record<string, unknown> | undefined;
165
+ const instructions = tl?.instructions as unknown[] | undefined;
166
+
167
+ if (!instructions) return { success: true, lists: [] };
168
+
169
+ const lists = parseListsFromInstructions(instructions);
170
+ return { success: true, lists };
171
+ } catch (err) {
172
+ return {
173
+ success: false,
174
+ lists: [],
175
+ error: err instanceof Error ? err.message : String(err),
176
+ };
177
+ }
178
+ }
179
+
180
+ async getListTimeline(
181
+ listId: string,
182
+ count?: number,
183
+ cursor?: string
184
+ ): Promise<{
185
+ success: boolean;
186
+ tweets: Tweet[];
187
+ cursor?: string;
188
+ error?: string;
189
+ }> {
190
+ try {
191
+ const variables: Record<string, unknown> = {
192
+ listId,
193
+ count: count ?? 20,
194
+ };
195
+ if (cursor) variables.cursor = cursor;
196
+
197
+ const { data, errors } = await this.graphqlGet(
198
+ "ListLatestTweetsTimeline",
199
+ variables,
200
+ listFeatures(),
201
+ fieldToggles()
202
+ );
203
+
204
+ if (errors?.length) {
205
+ return { success: false, tweets: [], error: this.formatErrors(errors) };
206
+ }
207
+
208
+ const list = (data as Record<string, unknown>)?.list as Record<string, unknown> | undefined;
209
+ const tweetsTimeline = list?.tweets_timeline as Record<string, unknown> | undefined;
210
+ const timeline = tweetsTimeline?.timeline as Record<string, unknown> | undefined;
211
+ const instructions = timeline?.instructions as unknown[] | undefined;
212
+
213
+ if (!instructions) return { success: true, tweets: [] };
214
+
215
+ const tweets = parseTweetsFromInstructions(instructions);
216
+ const nextCursor = extractCursorFromInstructions(instructions);
217
+ return { success: true, tweets, cursor: nextCursor };
218
+ } catch (err) {
219
+ return {
220
+ success: false,
221
+ tweets: [],
222
+ error: err instanceof Error ? err.message : String(err),
223
+ };
224
+ }
225
+ }
226
+ };
227
+ }