@mgcrea/mcp-x-api 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.
@@ -0,0 +1,836 @@
1
+ import { z } from "zod";
2
+ import { McpServer } from "@modelcontextprotocol/server";
3
+ //#region src/build-info.d.ts
4
+ type BuildInfo = {
5
+ name: string;
6
+ version: string;
7
+ gitCommit: string;
8
+ gitCommitDate: string;
9
+ };
10
+ declare const BUILD_INFO: BuildInfo;
11
+ //#endregion
12
+ //#region src/client/tokens.d.ts
13
+ type StoredTokens = {
14
+ version: number;
15
+ /** A changed client id invalidates the whole file: the tokens belong to that app. */
16
+ clientId: string;
17
+ scopes: string[];
18
+ accessToken: string;
19
+ refreshToken?: string;
20
+ /**
21
+ * One generation back. X invalidates the old refresh token the instant a
22
+ * refresh succeeds, so if we crash between receiving new tokens and writing
23
+ * them, the on-disk token is already dead. Keeping the previous one lets a
24
+ * failed refresh retry once instead of forcing the user through a browser.
25
+ */
26
+ previousRefreshToken?: string;
27
+ /** Milliseconds since epoch. */
28
+ expiresAt: number;
29
+ obtainedAt: number;
30
+ userId?: string;
31
+ username?: string;
32
+ };
33
+ type TokenStore = {
34
+ read(): StoredTokens | undefined;
35
+ write(tokens: StoredTokens): void;
36
+ clear(): void;
37
+ path: string;
38
+ };
39
+ //#endregion
40
+ //#region src/config.d.ts
41
+ declare const DEFAULT_ADS_BASE_URL = "https://ads-api.x.com";
42
+ /**
43
+ * The Ads API sandbox: free, isolated, and the only sane place to exercise the
44
+ * write tools. Note the host — X's own docs say `ads-api-sandbox.x.com`, which
45
+ * has no DNS record at all. `ads-api-sandbox.twitter.com` is the one that
46
+ * resolves, so this is not a typo waiting to be "fixed".
47
+ */
48
+ declare const SANDBOX_ADS_BASE_URL = "https://ads-api-sandbox.twitter.com";
49
+ /**
50
+ * X moved to pay-per-use on 2026-02-06; there is no free tier for new
51
+ * developers. These are list prices in USD, overridable via the config file's
52
+ * `pricing` key because a table baked into a schema with no escape hatch is
53
+ * wrong the day X changes it.
54
+ *
55
+ * The 24h dedup window is the load-bearing rule: within one UTC day, re-reading
56
+ * a resource id you already paid for is free. That is why the ledger keys on
57
+ * (kind, id, utcDay) rather than counting requests.
58
+ */
59
+ declare const DEFAULT_PRICING: {
60
+ postRead: number;
61
+ userRead: number;
62
+ /** Your own posts and profile — five times cheaper than reading someone else's. */
63
+ ownedRead: number;
64
+ postCreate: number;
65
+ /** A post containing a URL costs 40x a post read. Not a typo. */
66
+ postCreateWithUrl: number;
67
+ monthlyReadCap: number;
68
+ effectiveFrom: string;
69
+ };
70
+ declare const PricingSchema: z.ZodObject<{
71
+ postRead: z.ZodDefault<z.ZodNumber>;
72
+ userRead: z.ZodDefault<z.ZodNumber>;
73
+ ownedRead: z.ZodDefault<z.ZodNumber>;
74
+ postCreate: z.ZodDefault<z.ZodNumber>;
75
+ postCreateWithUrl: z.ZodDefault<z.ZodNumber>;
76
+ monthlyReadCap: z.ZodDefault<z.ZodNumber>;
77
+ effectiveFrom: z.ZodDefault<z.ZodString>;
78
+ }, z.core.$strict>;
79
+ type Pricing = z.infer<typeof PricingSchema>;
80
+ declare const ConfigSchema: z.ZodObject<{
81
+ bearerToken: z.ZodOptional<z.ZodString>;
82
+ clientId: z.ZodOptional<z.ZodString>;
83
+ clientSecret: z.ZodOptional<z.ZodString>;
84
+ redirectUri: z.ZodDefault<z.ZodString>;
85
+ scopes: z.ZodDefault<z.ZodArray<z.ZodString>>;
86
+ tokenFile: z.ZodString;
87
+ allowWrites: z.ZodDefault<z.ZodBoolean>;
88
+ writeBackend: z.ZodDefault<z.ZodEnum<{
89
+ api: "api";
90
+ intent: "intent";
91
+ }>>;
92
+ autoOpenBrowser: z.ZodDefault<z.ZodBoolean>;
93
+ enableFullArchive: z.ZodDefault<z.ZodBoolean>;
94
+ defaultMaxResults: z.ZodDefault<z.ZodNumber>;
95
+ monthlyBudgetUsd: z.ZodOptional<z.ZodNumber>;
96
+ cacheEnabled: z.ZodDefault<z.ZodBoolean>;
97
+ cacheMaxEntries: z.ZodDefault<z.ZodNumber>;
98
+ maxRetries: z.ZodDefault<z.ZodNumber>;
99
+ baseUrl: z.ZodDefault<z.ZodString>;
100
+ pricing: z.ZodDefault<z.ZodObject<{
101
+ postRead: z.ZodDefault<z.ZodNumber>;
102
+ userRead: z.ZodDefault<z.ZodNumber>;
103
+ ownedRead: z.ZodDefault<z.ZodNumber>;
104
+ postCreate: z.ZodDefault<z.ZodNumber>;
105
+ postCreateWithUrl: z.ZodDefault<z.ZodNumber>;
106
+ monthlyReadCap: z.ZodDefault<z.ZodNumber>;
107
+ effectiveFrom: z.ZodDefault<z.ZodString>;
108
+ }, z.core.$strict>>;
109
+ adsEnabled: z.ZodDefault<z.ZodBoolean>;
110
+ adsAllowWrites: z.ZodDefault<z.ZodBoolean>;
111
+ adsBaseUrl: z.ZodDefault<z.ZodString>;
112
+ adsAccountId: z.ZodOptional<z.ZodString>;
113
+ adsMaxDownloadBytes: z.ZodDefault<z.ZodNumber>;
114
+ }, z.core.$strict>;
115
+ type Config = z.infer<typeof ConfigSchema>;
116
+ /**
117
+ * Where the config file lives, most specific first: an explicit override, then
118
+ * the XDG location, then the conventional `~/.config`.
119
+ */
120
+ declare const resolveConfigPath: (env?: NodeJS.ProcessEnv) => string;
121
+ /**
122
+ * Environment first, config file second, **per field** — not whole-source.
123
+ * Docker and CI inject the environment and must keep working untouched, while a
124
+ * one-off `X_API_ALLOW_WRITES=0` still has to override a file that says `true`.
125
+ * Merging field by field is the only rule that gives both.
126
+ */
127
+ declare const loadConfig: (env?: NodeJS.ProcessEnv, configPath?: string) => Config;
128
+ /**
129
+ * Whether the ads tools should be registered. The Ads API rides the same OAuth
130
+ * 2.0 user token as bookmarks and the home timeline — the `ads.read` /
131
+ * `ads.write` scopes are what separate them — so a client id is the hard
132
+ * requirement, not a second set of credentials.
133
+ */
134
+ declare const hasAdsAccess: (config: Config) => boolean;
135
+ /**
136
+ * What to do when ads is enabled but the account cannot reach the Ads API.
137
+ * Surfaced by `x_auth_status`, because the two steps people miss are invisible
138
+ * from the error alone: the app needs the Ads Project attached, and any token
139
+ * minted *before* approval does not carry the entitlement.
140
+ */
141
+ declare const adsSetupInstructions: (config: Config) => string[];
142
+ /**
143
+ * The scopes actually requested at login. `tweet.write` is only asked for when
144
+ * the paid write backend is on, so a reader never holds a permission it cannot
145
+ * use — and the consent screen stays honest about what the server will do.
146
+ */
147
+ declare const effectiveScopes: (config: Config) => string[];
148
+ //#endregion
149
+ //#region src/client/auth.d.ts
150
+ type Logger = {
151
+ debug?: (message: string) => void;
152
+ warn?: (message: string) => void;
153
+ error?: (message: string) => void;
154
+ };
155
+ /**
156
+ * X has two credentials that are not interchangeable, so every request has to
157
+ * say which one it wants:
158
+ *
159
+ * - `"app"` — the app-only Bearer token. Reaches everything public: post
160
+ * lookup, search, user profiles, user timelines.
161
+ * - `"user"` — an OAuth2 access token for a logged-in account. The only way to
162
+ * reach bookmarks and the home timeline, and the only way to write.
163
+ *
164
+ * This is the one place the shape diverges from a single-credential API: the
165
+ * token provider is asked for a context rather than just "the token".
166
+ */
167
+ type AuthContext = "app" | "user";
168
+ type UserAuthStatus = {
169
+ authenticated: false;
170
+ reason: string;
171
+ } | {
172
+ authenticated: true;
173
+ username?: string;
174
+ userId?: string;
175
+ scopes: string[];
176
+ expiresAt: number;
177
+ };
178
+ type AuthStatus = {
179
+ app: boolean;
180
+ user: UserAuthStatus;
181
+ };
182
+ type TokenProvider = {
183
+ /** Bearer value for the requested context. Throws `UserContextRequiredError` if unavailable. */
184
+ getToken(context: AuthContext): Promise<string>;
185
+ /** Called on a 401 to force the next call to remint or refresh. */
186
+ invalidate(context: AuthContext): void;
187
+ /** Powers `x_auth_status` and the startup banner. */
188
+ describe(): AuthStatus;
189
+ };
190
+ /**
191
+ * The app-only Bearer token, copied from the developer portal. It never
192
+ * expires and cannot be reminted, so `invalidate` is a no-op: a 401 here means
193
+ * the token is wrong, and retrying with the same string would only burn the
194
+ * retry budget.
195
+ */
196
+ declare const bearerTokenProvider: (token: string) => TokenProvider;
197
+ /**
198
+ * Combine whichever providers are configured, dispatching by context. Either
199
+ * side may be absent — a Bearer-only install is the common case, and a
200
+ * user-only install is legitimate too (an OAuth2 access token can read
201
+ * everything an app-only token can).
202
+ */
203
+ declare const compositeTokenProvider: (parts: {
204
+ app?: TokenProvider;
205
+ user?: TokenProvider;
206
+ }) => TokenProvider;
207
+ /** The test double: one token, both contexts, no network. */
208
+ declare const staticTokenProvider: (token: string) => TokenProvider;
209
+ //#endregion
210
+ //#region src/client/http.d.ts
211
+ type QueryValue = string | number | boolean | string[] | undefined;
212
+ type Query = Record<string, QueryValue>;
213
+ /**
214
+ * What the last response told us about how much of an endpoint's budget is left.
215
+ *
216
+ * `scope` and `api` are optional because the v2 API reports exactly one family
217
+ * of rate-limit headers and has no need to distinguish them. The Ads API
218
+ * reports three (endpoint, account and cost), so it fills them in.
219
+ */
220
+ type RateLimitSnapshot = {
221
+ endpoint: string;
222
+ limit?: number;
223
+ remaining?: number;
224
+ /** Unix seconds, as X reports it. */
225
+ reset?: number;
226
+ resetAt?: string;
227
+ scope?: "endpoint" | "account" | "cost";
228
+ api?: "v2" | "ads";
229
+ };
230
+ //#endregion
231
+ //#region src/client/ads.d.ts
232
+ type AdsApiClientOptions = {
233
+ baseUrl?: string;
234
+ /**
235
+ * The same provider the v2 client uses. The Ads API is always asked for the
236
+ * `"user"` context: there is no app-only path to `/12/accounts`.
237
+ */
238
+ tokenProvider: TokenProvider;
239
+ maxRetries?: number;
240
+ maxDownloadBytes?: number;
241
+ fetch?: typeof fetch;
242
+ logger?: Logger;
243
+ userAgent?: string;
244
+ };
245
+ type CursorPage<T> = {
246
+ data: T[];
247
+ pages: number;
248
+ nextCursor?: string;
249
+ totalCount?: number;
250
+ };
251
+ /**
252
+ * Fetch-based client for the X Ads API v12. Deliberately not an `XApiClient`
253
+ * subclass: the two share transport concerns and nothing else. Ads paginates by
254
+ * cursor rather than `next_token`, answers errors in two envelopes neither of
255
+ * which is v2's problem-details, reports three families of rate-limit headers,
256
+ * and needs its own diagnostics — the v2 prose about the Pay-per-use package is
257
+ * actively misleading here. What genuinely is shared lives in `./http.js`.
258
+ *
259
+ * Writes send their parameters in the query string, never a JSON body: that is
260
+ * what the Ads API takes on POST and PUT, and what X's own SDKs send.
261
+ */
262
+ declare class AdsApiClient {
263
+ readonly sandbox: boolean;
264
+ readonly baseUrl: string;
265
+ private readonly tokenProvider;
266
+ private readonly maxRetries;
267
+ private readonly maxDownloadBytes;
268
+ private readonly fetchImpl;
269
+ private readonly logger;
270
+ private readonly userAgent;
271
+ private readonly rateLimits;
272
+ constructor(opts: AdsApiClientOptions);
273
+ rateLimitStatus(): RateLimitSnapshot[];
274
+ /**
275
+ * Ads reports three independent budgets, and a 429 can come from any of them.
276
+ * Recording only the endpoint family would leave the account-level limit —
277
+ * the one that actually bites during a bulk read — invisible.
278
+ */
279
+ private recordRateLimit;
280
+ request<T = unknown>(method: string, path: string, query?: Query): Promise<T>;
281
+ get<T = unknown>(path: string, query?: Query): Promise<T>;
282
+ post<T = unknown>(path: string, query?: Query): Promise<T>;
283
+ put<T = unknown>(path: string, query?: Query): Promise<T>;
284
+ del<T = unknown>(path: string, query?: Query): Promise<T>;
285
+ /**
286
+ * GET a collection, following `next_cursor` until the pages run out or a
287
+ * bound is hit.
288
+ *
289
+ * Unlike v2, the cursor is a top-level field rather than nested under `meta`,
290
+ * it is `null` (not absent) on the last page, and it goes back out as
291
+ * `cursor`. Everything else about the loop matches `XApiClient.paginate`,
292
+ * which is why this is a separate method rather than a shared one — the
293
+ * differences are exactly the parts that matter.
294
+ */
295
+ paginateCursor<T = unknown>(path: string, query: Query, opts: {
296
+ maxItems: number;
297
+ maxPages?: number;
298
+ }): Promise<CursorPage<T>>;
299
+ /**
300
+ * Fetch a finished analytics job's result file and decompress it.
301
+ *
302
+ * Three things here are load-bearing. The URL is a presigned object-store
303
+ * link on a different host, so it must go out with **no** Authorization
304
+ * header — signing it makes the store reject it. The host is checked first,
305
+ * because the URL came from a remote response. And both the compressed and
306
+ * decompressed sizes are capped: a 25 MB gzip of repetitive JSON expands to
307
+ * hundreds of megabytes, so an uncapped gunzip here is an OOM waiting for a
308
+ * big enough report.
309
+ */
310
+ downloadGzipped(url: string): Promise<{
311
+ text: string;
312
+ bytes: number;
313
+ }>;
314
+ private parseErrors;
315
+ /**
316
+ * Ads answers in two envelopes. The gateway rejects bad auth with the legacy
317
+ * v1.1 shape and a *numeric* code — and with HTTP 400, not 401. Past that,
318
+ * application errors use CAPS_CASE string codes. Both are quoted here, and
319
+ * each status gets the sentence that actually fixes it: a bare
320
+ * "UNAUTHORIZED_ACCESS" sends people to re-check a token that is usually fine.
321
+ */
322
+ private errorMessage;
323
+ }
324
+ //#endregion
325
+ //#region src/client/ads-shape.d.ts
326
+ /** X states every money field in millionths of a currency unit. */
327
+ declare const MICRO = 1000000;
328
+ declare const toMicro: (major: number) => number;
329
+ declare const fromMicro: (micro: number) => number;
330
+ /**
331
+ * Pair every `*_amount_local_micro` field with the value a human would say.
332
+ *
333
+ * This is not cosmetic. A model that reads `daily_budget_amount_local_micro:
334
+ * 50000000` and reasons about it concludes the budget is fifty million, and the
335
+ * next thing it proposes is scaled by a factor of a million. The write path
336
+ * guards against the same mistake by refusing micro inputs; this is the other
337
+ * half, and without it the guard only covers one direction.
338
+ *
339
+ * The micro field is kept rather than replaced, so the raw value X returned
340
+ * stays auditable.
341
+ */
342
+ declare const shapeMoney: <T>(value: T, currency?: string) => T;
343
+ //#endregion
344
+ //#region src/client/cache.d.ts
345
+ type ResourceKind = "post" | "user" | "owned";
346
+ type CacheStats = {
347
+ day: string;
348
+ entries: number;
349
+ hits: number;
350
+ misses: number;
351
+ hit_rate: number;
352
+ };
353
+ type DayCache = {
354
+ get(kind: ResourceKind, id: string): unknown | undefined;
355
+ set(kind: ResourceKind, id: string, value: unknown): void;
356
+ stats(): CacheStats;
357
+ };
358
+ /**
359
+ * The dedup window is the UTC calendar day, not a rolling 24 hours — so the
360
+ * whole cache turns over at once at UTC midnight rather than expiring entry by
361
+ * entry. Comparing a stored day string is both cheaper and more faithful than
362
+ * per-entry timestamps.
363
+ */
364
+ declare const utcDay: (now: number) => string;
365
+ declare const createDayCache: (opts: {
366
+ maxEntries: number;
367
+ enabled?: boolean;
368
+ now?: () => number;
369
+ }) => DayCache;
370
+ //#endregion
371
+ //#region src/client/cost.d.ts
372
+ /**
373
+ * Which resource ids we have already been billed for today.
374
+ *
375
+ * This is deliberately NOT the response cache. The cache stores payloads and is
376
+ * LRU-bounded; this stores only ids and is unbounded. If they were one
377
+ * structure, evicting a payload under memory pressure would make the estimator
378
+ * re-count an id X already charged us for, and over-report spend. Ids are ~20
379
+ * bytes, so keeping every one for a day costs kilobytes in practice.
380
+ */
381
+ type Ledger = {
382
+ /** Split ids into the ones today's read actually bills for and the ones already paid. */
383
+ record(kind: ResourceKind, ids: string[]): {
384
+ billable: string[];
385
+ free: string[];
386
+ };
387
+ /** Estimate a read before issuing it, for the budget guard. */
388
+ estimate(kind: ResourceKind, ids: string[]): number;
389
+ /** Estimate by count, for reads whose result ids are unknown in advance (searches). */
390
+ estimateCount(kind: ResourceKind, count: number): number;
391
+ recordCreate(hasUrl: boolean): void;
392
+ spentUsd(): number;
393
+ report(cache: CacheStats): UsageReport;
394
+ };
395
+ type CostNote = {
396
+ billable_post_reads?: number;
397
+ billable_user_reads?: number;
398
+ owned_reads?: number;
399
+ free_from_cache?: number;
400
+ estimated_usd: number;
401
+ note?: string;
402
+ };
403
+ type UsageReport = {
404
+ day: string;
405
+ since_process_start: {
406
+ billable_post_reads: number;
407
+ billable_user_reads: number;
408
+ owned_reads: number;
409
+ free_from_dedup: number;
410
+ posts_created: number;
411
+ estimated_usd: number;
412
+ };
413
+ read_cap: {
414
+ monthly_cap: number;
415
+ reads_this_session: number;
416
+ cap_used_pct: number;
417
+ };
418
+ budget?: {
419
+ limit_usd: number;
420
+ remaining_usd: number;
421
+ };
422
+ cache: CacheStats;
423
+ pricing: Pricing;
424
+ disclaimer: string;
425
+ };
426
+ declare const createLedger: (opts: {
427
+ pricing: Pricing;
428
+ budgetUsd?: number | undefined;
429
+ now?: () => number;
430
+ }) => Ledger;
431
+ //#endregion
432
+ //#region src/client/errors.d.ts
433
+ /**
434
+ * X answers errors in two shapes depending on the endpoint: a problem-details
435
+ * object (`{ title, detail, status, type }`) on v2, and a legacy
436
+ * `{ errors: [{ message, code }] }` on a few others. Both are modelled here so
437
+ * the client can quote whichever it got.
438
+ */
439
+ type XApiError = {
440
+ title?: string;
441
+ detail?: string;
442
+ type?: string;
443
+ status?: number;
444
+ message?: string;
445
+ code?: number | string;
446
+ /** Ads application errors carry the offending field here. */
447
+ details?: unknown;
448
+ /** Present on partial responses, e.g. one deleted post in a batch lookup. */
449
+ resource_type?: string;
450
+ parameter?: string;
451
+ value?: string;
452
+ };
453
+ declare class XApiRequestError extends Error {
454
+ readonly name = "XApiRequestError";
455
+ readonly status: number;
456
+ readonly errors: XApiError[] | unknown;
457
+ constructor(message: string, opts: {
458
+ status: number;
459
+ errors?: XApiError[] | unknown;
460
+ });
461
+ }
462
+ /**
463
+ * Thrown when a tool needs a logged-in user and only an app-only Bearer token
464
+ * is available. The message carries the fix, because "401 Unauthorized" tells
465
+ * you nothing about which of two credentials was missing.
466
+ */
467
+ declare class UserContextRequiredError extends Error {
468
+ readonly name = "UserContextRequiredError";
469
+ constructor(what: string, reason?: string);
470
+ }
471
+ /** Thrown when a write tool is reached while X_API_ALLOW_WRITES is off. */
472
+ declare class WritesDisabledError extends Error {
473
+ readonly name = "WritesDisabledError";
474
+ constructor(what: string);
475
+ }
476
+ /**
477
+ * Thrown when the Ads API is reachable but this account cannot use it — no Ads
478
+ * entitlement on the app, or no ads account behind the logged-in user. Separate
479
+ * from `UserContextRequiredError` because logging in again does not fix it: the
480
+ * missing piece is an approval, not a token.
481
+ */
482
+ declare class AdsAccessError extends Error {
483
+ readonly name = "AdsAccessError";
484
+ readonly details: Record<string, unknown>;
485
+ constructor(message: string, details?: Record<string, unknown>);
486
+ }
487
+ /**
488
+ * A local guard that fires *before* the request goes out, so an agent in a loop
489
+ * cannot spend past the ceiling. Carries the arithmetic so the number is
490
+ * auditable rather than mysterious.
491
+ */
492
+ declare class BudgetExceededError extends Error {
493
+ readonly name = "BudgetExceededError";
494
+ readonly details: Record<string, unknown>;
495
+ constructor(opts: {
496
+ estimateUsd: number;
497
+ spentUsd: number;
498
+ limitUsd: number;
499
+ what: string;
500
+ });
501
+ }
502
+ /**
503
+ * A local check that failed before we sent anything to X. Carries the state it
504
+ * read, so the caller sees why rather than just that something was wrong.
505
+ */
506
+ declare class PreconditionError extends Error {
507
+ readonly name = "PreconditionError";
508
+ readonly details: Record<string, unknown>;
509
+ constructor(message: string, details?: Record<string, unknown>);
510
+ }
511
+ //#endregion
512
+ //#region src/client/shape.d.ts
513
+ type Rec$1 = Record<string, unknown>;
514
+ /** Three lookup tables built once per response. */
515
+ type Includes = {
516
+ users: Map<string, Rec$1>;
517
+ tweets: Map<string, Rec$1>;
518
+ media: Map<string, Rec$1>;
519
+ };
520
+ /**
521
+ * Build the lookup tables from one `includes` block or several (paginated reads
522
+ * return one per page).
523
+ *
524
+ * These are `Map`s rather than an `Array.find` per lookup on purpose: a 100-post
525
+ * page with 100 distinct authors would otherwise be quadratic, and the whole
526
+ * point of this module is that it stays cheap on the largest responses.
527
+ */
528
+ declare const buildIncludesIndex: (includes: Rec$1 | Rec$1[] | undefined) => Includes;
529
+ /** A referenced post, resolved one level deep only. */
530
+ type ShapedRef = {
531
+ id: string;
532
+ author?: string;
533
+ text?: string;
534
+ created_at?: string;
535
+ };
536
+ type ShapedPost = {
537
+ id: string;
538
+ url: string;
539
+ author: string;
540
+ created_at?: string;
541
+ text: string;
542
+ lang?: string;
543
+ metrics?: {
544
+ likes?: number;
545
+ reposts?: number;
546
+ replies?: number;
547
+ quotes?: number;
548
+ views?: number;
549
+ };
550
+ quotes?: ShapedRef;
551
+ replies_to?: ShapedRef;
552
+ reposts?: ShapedRef;
553
+ media?: string[];
554
+ conversation_id?: string;
555
+ };
556
+ type ShapedUser = {
557
+ id: string;
558
+ username: string;
559
+ name?: string;
560
+ url: string;
561
+ description?: string;
562
+ verified?: boolean;
563
+ protected?: boolean;
564
+ location?: string;
565
+ created_at?: string;
566
+ metrics?: {
567
+ followers?: number;
568
+ following?: number;
569
+ posts?: number;
570
+ listed?: number;
571
+ };
572
+ };
573
+ declare const shapeUser: (raw: Rec$1) => ShapedUser;
574
+ type ShapedPosts = {
575
+ posts: ShapedPost[];
576
+ result_count?: number;
577
+ next_token?: string;
578
+ /** Ids X refused to return — deleted, protected, or suspended. */
579
+ not_found?: string[];
580
+ };
581
+ /** Flatten a list-of-posts response. Accepts a single- or multi-page envelope. */
582
+ declare const shapePostsResponse: (response: unknown) => ShapedPosts;
583
+ /** Flatten a single-post response. */
584
+ declare const shapePostResponse: (response: unknown) => ShapedPost | {
585
+ error: string;
586
+ };
587
+ type ShapedUsers = {
588
+ users: ShapedUser[];
589
+ not_found?: string[];
590
+ };
591
+ declare const shapeUsersResponse: (response: unknown) => ShapedUsers;
592
+ //#endregion
593
+ //#region src/client/x.d.ts
594
+ type RequestOptions = {
595
+ query?: Query;
596
+ body?: unknown;
597
+ /** Which credential to send. Defaults to the app-only Bearer token. */
598
+ auth?: AuthContext;
599
+ };
600
+ type XApiClientOptions = {
601
+ baseUrl?: string;
602
+ tokenProvider: TokenProvider;
603
+ maxRetries?: number;
604
+ fetch?: typeof fetch;
605
+ logger?: Logger;
606
+ userAgent?: string;
607
+ };
608
+ /**
609
+ * Minimal fetch-based client for the X API v2. Paths are absolute (`/2/tweets`).
610
+ * Retries a 401 (invalidating the token first) and 429/5xx with exponential
611
+ * backoff honoring `Retry-After`, and records every response's rate-limit
612
+ * headers so a 429 can say what it is waiting for.
613
+ */
614
+ declare class XApiClient {
615
+ private readonly baseUrl;
616
+ private readonly tokenProvider;
617
+ private readonly maxRetries;
618
+ private readonly fetchImpl;
619
+ private readonly logger;
620
+ private readonly userAgent;
621
+ private readonly rateLimits;
622
+ constructor(opts: XApiClientOptions);
623
+ /** Everything the last response said about each endpoint's remaining budget. */
624
+ rateLimitStatus(): RateLimitSnapshot[];
625
+ private recordRateLimit;
626
+ request<T = unknown>(method: string, path: string, opts?: RequestOptions): Promise<T>;
627
+ get<T = unknown>(path: string, query?: Query, auth?: AuthContext): Promise<T>;
628
+ post<T = unknown>(path: string, body?: unknown, auth?: AuthContext): Promise<T>;
629
+ del<T = unknown>(path: string, auth?: AuthContext): Promise<T>;
630
+ /**
631
+ * GET a collection, following `meta.next_token` until the pages run out or a
632
+ * bound is hit.
633
+ *
634
+ * Both bounds exist because unbounded pagination here spends real money: at
635
+ * $0.005 a post, walking a busy hashtag to the end is a three-figure mistake
636
+ * an agent can make in one call. `maxItems` is the one callers actually set.
637
+ *
638
+ * X carries the cursor in `meta.next_token` and expects it back as
639
+ * `pagination_token`, so — unlike a `links.next` API — the original query has
640
+ * to be re-sent on every page rather than replaced.
641
+ */
642
+ paginate<T = unknown>(path: string, query: Query, opts: {
643
+ maxItems: number;
644
+ maxPages?: number;
645
+ auth?: AuthContext;
646
+ }): Promise<{
647
+ data: T[];
648
+ pages: number;
649
+ nextToken?: string;
650
+ includes: Rec[];
651
+ }>;
652
+ private parseErrors;
653
+ /**
654
+ * The three statuses that actually happen get a sentence naming the fix. A
655
+ * bare "HTTP 403" sends people to the wrong place — usually to re-check a
656
+ * token that was fine, when the real answer is that their access tier does
657
+ * not include the endpoint.
658
+ */
659
+ private errorMessage;
660
+ private problemDetail;
661
+ }
662
+ type Rec = Record<string, unknown>;
663
+ //#endregion
664
+ //#region src/compose/intent.d.ts
665
+ /**
666
+ * X's web intent: a documented, credential-free URL that opens the composer
667
+ * pre-filled. Nothing is posted until a human clicks Post, which is why it
668
+ * needs no auth, consumes no API quota and costs nothing.
669
+ *
670
+ * The path is `/intent/tweet`, not `/intent/post`. X renamed Tweet to Post
671
+ * throughout its docs prose but never changed the URL, and `/intent/post` is
672
+ * undocumented with known edge-case bugs. `twitter.com` 301s to `x.com`, so
673
+ * there is no reason to emit the legacy domain.
674
+ */
675
+ declare const INTENT_BASE_URL = "https://x.com/intent/tweet";
676
+ type IntentInput = {
677
+ text: string;
678
+ /** Appended by the composer and counted against the 280. */
679
+ url?: string | undefined;
680
+ /** Without the leading '#'. */
681
+ hashtags?: string[] | undefined;
682
+ /** Handle without the leading '@'. */
683
+ via?: string | undefined;
684
+ /** The post id being replied to. */
685
+ inReplyTo?: string | undefined;
686
+ lang?: string | undefined;
687
+ };
688
+ /**
689
+ * What the composer will actually contain, in X's documented assembly order:
690
+ * text, then url, then hashtags, then "via @handle".
691
+ *
692
+ * Validating `text` alone and then handing back a URL the composer rejects is
693
+ * exactly the bug this module exists to prevent, so counting happens on this
694
+ * string rather than on the input.
695
+ */
696
+ declare const assembleComposerText: (input: IntentInput) => string;
697
+ declare const buildIntentUrl: (input: IntentInput) => string;
698
+ type IntentValidation = {
699
+ valid: boolean;
700
+ weighted: number;
701
+ remaining: number;
702
+ composed: string;
703
+ intent_url: string;
704
+ warnings: string[];
705
+ error?: string;
706
+ };
707
+ declare const validateIntent: (input: IntentInput) => IntentValidation;
708
+ //#endregion
709
+ //#region src/compose/weighted.d.ts
710
+ declare const MAX_WEIGHTED_LENGTH = 280;
711
+ /**
712
+ * Every URL costs the same whatever its real length: X rewrites it to t.co.
713
+ * There is a single value now — the old http/https split was deprecated once
714
+ * every t.co link became https.
715
+ */
716
+ declare const TCO_URL_LENGTH = 23;
717
+ type WeightedLength = {
718
+ /** 0-280+, in the units X shows the user. */
719
+ weighted: number;
720
+ remaining: number;
721
+ valid: boolean;
722
+ urls: {
723
+ url: string;
724
+ countedAs: number;
725
+ }[];
726
+ };
727
+ /**
728
+ * Count a draft the way X will.
729
+ *
730
+ * NFC normalization comes first because twitter-text normalizes first: without
731
+ * it a decomposed "café" counts 5 instead of 4, and the user is told they have
732
+ * one character less than they do.
733
+ */
734
+ declare const weightedLength: (text: string) => WeightedLength;
735
+ //#endregion
736
+ //#region src/server.d.ts
737
+ declare const SERVER_NAME: string;
738
+ declare const SERVER_VERSION: string;
739
+ type CreateServerOptions = {
740
+ config: Config;
741
+ fetch?: typeof fetch;
742
+ logger?: Logger;
743
+ /** Override the token provider (tests, and the OAuth user flow). */
744
+ tokenProvider?: TokenProvider;
745
+ now?: () => number;
746
+ };
747
+ type CreatedServer = {
748
+ server: McpServer;
749
+ client: XApiClient;
750
+ /** Present only when ads is configured. Exposed for tests and diagnostics. */
751
+ ads?: AdsApiClient | undefined;
752
+ tokenProvider: TokenProvider;
753
+ cache: DayCache;
754
+ ledger: Ledger;
755
+ store: TokenStore;
756
+ };
757
+ declare const createServer: (opts: CreateServerOptions) => CreatedServer;
758
+ //#endregion
759
+ //#region src/tools/index.d.ts
760
+ /**
761
+ * Everything the ads tools need, as one object rather than four independently
762
+ * optional fields: "the client exists exactly when ads is configured" is then
763
+ * an invariant the type enforces rather than one that can drift.
764
+ */
765
+ type AdsContext = {
766
+ client: AdsApiClient;
767
+ /** Register the campaign-mutating tools. Off by default — see X_ADS_ALLOW_WRITES. */
768
+ allowWrites: boolean;
769
+ /** Default ads account. Absent means "resolve it lazily from GET /12/accounts". */
770
+ accountId?: string | undefined;
771
+ /** True when pointed at the Ads sandbox, where nothing spends real money. */
772
+ sandbox: boolean;
773
+ baseUrl: string;
774
+ };
775
+ /**
776
+ * Threaded through every tool rather than a bare `allowWrites` boolean: the
777
+ * cache and ledger are needed by every read tool, so a boolean would have been
778
+ * widened on the first commit anyway.
779
+ */
780
+ type ToolContext = {
781
+ /** Register the paid write tools too. Off by default — see X_API_ALLOW_WRITES. */
782
+ allowWrites: boolean;
783
+ /** "intent" (free web-intent URLs, the default) or "api" (paid POST /2/tweets). */
784
+ writeBackend: "intent" | "api";
785
+ autoOpenBrowser: boolean;
786
+ /** Register x_search_all. Off by default — full-archive search needs a paid tier. */
787
+ enableFullArchive: boolean;
788
+ defaultMaxResults: number;
789
+ pricing: Pricing;
790
+ budgetUsd?: number | undefined;
791
+ cache: DayCache;
792
+ ledger: Ledger;
793
+ tokenProvider: TokenProvider;
794
+ /**
795
+ * False when neither a Bearer token nor an OAuth client id is configured. The
796
+ * server still starts and still serves the free local tools; the ones that
797
+ * would call the X API are simply not registered.
798
+ */
799
+ hasCredentials: boolean;
800
+ /** Setup guidance surfaced by x_auth_status when nothing is configured. */
801
+ setup?: string[] | undefined;
802
+ /** Where the OAuth tokens live, for x_auth_status. Absent when OAuth is unconfigured. */
803
+ tokenFile?: string | undefined;
804
+ /**
805
+ * The token file, so a user id discovered lazily can be written back and not
806
+ * re-fetched on every call. Absent when OAuth is unconfigured.
807
+ */
808
+ tokenStore?: TokenStore | undefined;
809
+ /** Present only when a client id is configured; its presence registers the login tools. */
810
+ login?: ((open: boolean) => Promise<LoginSummary>) | undefined;
811
+ logout?: (() => void) | undefined;
812
+ /** Present only when X_ADS_ENABLED is on and an OAuth client id is configured. */
813
+ ads?: AdsContext | undefined;
814
+ /** Ads setup guidance surfaced by x_auth_status when ads is not configured. */
815
+ adsSetup?: string[] | undefined;
816
+ };
817
+ type LoginSummary = {
818
+ username?: string | undefined;
819
+ userId?: string | undefined;
820
+ scopes: string[];
821
+ tokenFile: string;
822
+ };
823
+ /**
824
+ * Register the X API tools.
825
+ *
826
+ * Read tools and the free compose tools are always registered. The paid write
827
+ * tools appear only when `allowWrites` *and* `writeBackend === "api"`;
828
+ * `x_search_all` only when full-archive access is enabled; and the login tools
829
+ * and user-context timelines only when an OAuth client id is configured — so
830
+ * with the defaults those tools are not merely refused, they are invisible and
831
+ * cannot be called at all.
832
+ */
833
+ declare const registerTools: (server: McpServer, client: XApiClient, ctx: ToolContext) => void;
834
+ //#endregion
835
+ export { AdsAccessError, AdsApiClient, type AdsApiClientOptions, type AdsContext, type AuthContext, type AuthStatus, BUILD_INFO, BudgetExceededError, type BuildInfo, type Config, type CostNote, type CreateServerOptions, type CreatedServer, type CursorPage, DEFAULT_ADS_BASE_URL, DEFAULT_PRICING, type DayCache, INTENT_BASE_URL, type IntentInput, type Ledger, type Logger, MAX_WEIGHTED_LENGTH, MICRO, PreconditionError, type Pricing, type Query, type RateLimitSnapshot, type ResourceKind, SANDBOX_ADS_BASE_URL, SERVER_NAME, SERVER_VERSION, type ShapedPost, type ShapedUser, TCO_URL_LENGTH, type TokenProvider, type ToolContext, type UsageReport, UserContextRequiredError, WritesDisabledError, XApiClient, type XApiClientOptions, type XApiError, XApiRequestError, adsSetupInstructions, assembleComposerText, bearerTokenProvider, buildIncludesIndex, buildIntentUrl, compositeTokenProvider, createDayCache, createLedger, createServer, effectiveScopes, fromMicro, hasAdsAccess, loadConfig, registerTools, resolveConfigPath, shapeMoney, shapePostResponse, shapePostsResponse, shapeUser, shapeUsersResponse, staticTokenProvider, toMicro, utcDay, validateIntent, weightedLength };
836
+ //# sourceMappingURL=index.d.ts.map