Skip to content

Commit 2f22573

Browse files
nearestnaborsclaude
andcommitted
Fix Biome lint errors for CI
- Use named imports instead of namespace imports (fs, path) - Remove unnecessary async from handlers without await - Convert forEach to for...of loops - Add biome-ignore for complex functions that handle auth flows - Move regex patterns to top level for performance Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c3118e2 commit 2f22573

6 files changed

Lines changed: 366 additions & 263 deletions

File tree

src/arcade/client.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import Arcade from "@arcadeai/arcadejs";
1717
import type { AuthorizationResponse } from "@arcadeai/arcadejs/resources";
1818
import { setUsername as setPersistentUsername } from "../state/manager.js";
1919

20+
// Regex patterns for parsing tweet responses (moved to top level for performance)
21+
const TWEET_URL_REGEX = /URL:\s*(https:\/\/[^\s]+)/i;
22+
const TWEET_STATUS_ID_REGEX = /status\/(\d+)/;
23+
const TWEET_ID_REGEX = /id\s+(\d+)/i;
24+
2025
// Types for X data
2126
export interface Mention {
2227
id: string;
@@ -578,25 +583,26 @@ const realArcadeClient: ArcadeClient = {
578583
// Handle string response: "Tweet with id XXXXX posted successfully. URL: https://x.com/..."
579584
if (typeof response === "string") {
580585
// Try to extract URL from the string
581-
const urlMatch = response.match(/URL:\s*(https:\/\/[^\s]+)/i);
586+
const urlMatch = response.match(TWEET_URL_REGEX);
582587
if (urlMatch) {
583588
tweetUrl = urlMatch[1];
584589
// Extract ID from URL
585-
const idMatch = tweetUrl.match(/status\/(\d+)/);
590+
const idMatch = tweetUrl.match(TWEET_STATUS_ID_REGEX);
586591
if (idMatch) {
587592
tweetId = idMatch[1];
588593
}
589594
}
590595
// Fallback: try to extract ID directly from message
591596
if (tweetId === "unknown") {
592-
const idMatch = response.match(/id\s+(\d+)/i);
597+
const idMatch = response.match(TWEET_ID_REGEX);
593598
if (idMatch) {
594599
tweetId = idMatch[1];
595600
}
596601
}
597602
} else if (response && typeof response === "object") {
598603
// Handle object response
599-
tweetId = response.id || response.data?.id || response.tweet_id || "unknown";
604+
tweetId =
605+
response.id || response.data?.id || response.tweet_id || "unknown";
600606
}
601607

602608
log("Extracted tweet ID:", tweetId);

src/server.ts

Lines changed: 96 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -7,45 +7,46 @@
77
* Platform: X via Arcade.dev
88
*/
99

10-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
10+
import { readFile } from "node:fs/promises";
11+
import { dirname, join } from "node:path";
12+
import { fileURLToPath } from "node:url";
13+
import { RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
14+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1115
import {
1216
CallToolRequestSchema,
13-
ListToolsRequestSchema,
17+
type CallToolResult,
1418
ListResourcesRequestSchema,
19+
ListToolsRequestSchema,
1520
ReadResourceRequestSchema,
1621
type Tool,
17-
type CallToolResult,
18-
} from '@modelcontextprotocol/sdk/types.js';
19-
import { RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
20-
import * as fs from 'fs/promises';
21-
import * as path from 'path';
22-
import { fileURLToPath } from 'url';
22+
} from "@modelcontextprotocol/sdk/types.js";
2323

2424
// X tools
25-
import { xAuthStatus } from './tools/auth-status.js';
26-
import { xDraftTweet } from './tools/draft-tweet.js';
27-
import { xPostTweet } from './tools/post-tweet.js';
28-
import { xConversations, xGetConversations } from './tools/conversations.js';
29-
import { xDismissConversation } from './tools/dismiss-conversation.js';
25+
import { xAuthStatus } from "./tools/auth-status.js";
26+
import { xConversations, xGetConversations } from "./tools/conversations.js";
27+
import { xDismissConversation } from "./tools/dismiss-conversation.js";
28+
import { xDraftTweet } from "./tools/draft-tweet.js";
29+
import { xPostTweet } from "./tools/post-tweet.js";
3030

3131
const __filename = fileURLToPath(import.meta.url);
32-
const __dirname = path.dirname(__filename);
32+
const __dirname = dirname(__filename);
3333

3434
// UI Resource URIs
3535
const UI_RESOURCES = {
36-
authButton: 'ui://assa/auth-button.html',
37-
tweetPreview: 'ui://assa/tweet-preview.html',
38-
conversationList: 'ui://assa/conversation-list.html',
36+
authButton: "ui://assa/auth-button.html",
37+
tweetPreview: "ui://assa/tweet-preview.html",
38+
conversationList: "ui://assa/conversation-list.html",
3939
};
4040

4141
// Tool definitions for MCP with UI metadata
4242
const TOOLS: Tool[] = [
4343
// === X Tools ===
4444
{
45-
name: 'x_auth_status',
46-
description: 'Check X authentication status. IMPORTANT: The UI handles everything. Your ONLY response should be one short sentence. Do NOT explain, offer help, or ask follow-up questions.',
45+
name: "x_auth_status",
46+
description:
47+
"Check X authentication status. IMPORTANT: The UI handles everything. Your ONLY response should be one short sentence. Do NOT explain, offer help, or ask follow-up questions.",
4748
inputSchema: {
48-
type: 'object',
49+
type: "object",
4950
properties: {},
5051
required: [],
5152
},
@@ -57,25 +58,26 @@ const TOOLS: Tool[] = [
5758
},
5859
},
5960
{
60-
name: 'x_draft_tweet',
61-
description: 'Create a draft tweet and show a preview UI for approval before posting.',
61+
name: "x_draft_tweet",
62+
description:
63+
"Create a draft tweet and show a preview UI for approval before posting.",
6264
inputSchema: {
63-
type: 'object',
65+
type: "object",
6466
properties: {
6567
text: {
66-
type: 'string',
67-
description: 'Tweet content (max 280 characters)',
68+
type: "string",
69+
description: "Tweet content (max 280 characters)",
6870
},
6971
reply_to_id: {
70-
type: 'string',
71-
description: 'Tweet ID to reply to (optional)',
72+
type: "string",
73+
description: "Tweet ID to reply to (optional)",
7274
},
7375
quote_tweet_id: {
74-
type: 'string',
75-
description: 'Tweet ID to quote (optional)',
76+
type: "string",
77+
description: "Tweet ID to quote (optional)",
7678
},
7779
},
78-
required: ['text'],
80+
required: ["text"],
7981
},
8082
// _meta.ui links tool to UI resource
8183
_meta: {
@@ -85,33 +87,35 @@ const TOOLS: Tool[] = [
8587
},
8688
},
8789
{
88-
name: 'x_post_tweet',
89-
description: 'Post a tweet to X. Usually called from the draft preview UI after user approval.',
90+
name: "x_post_tweet",
91+
description:
92+
"Post a tweet to X. Usually called from the draft preview UI after user approval.",
9093
inputSchema: {
91-
type: 'object',
94+
type: "object",
9295
properties: {
9396
text: {
94-
type: 'string',
95-
description: 'Tweet content (max 280 characters)',
97+
type: "string",
98+
description: "Tweet content (max 280 characters)",
9699
},
97100
reply_to_id: {
98-
type: 'string',
99-
description: 'Tweet ID to reply to (optional)',
101+
type: "string",
102+
description: "Tweet ID to reply to (optional)",
100103
},
101104
quote_tweet_id: {
102-
type: 'string',
103-
description: 'Tweet ID to quote (optional)',
105+
type: "string",
106+
description: "Tweet ID to quote (optional)",
104107
},
105108
},
106-
required: ['text'],
109+
required: ["text"],
107110
},
108111
// No UI - this is a data-only tool called from tweet-preview UI
109112
},
110113
{
111-
name: 'x_conversations',
112-
description: 'Show X conversations awaiting your reply. IMPORTANT: The UI shows everything the user needs. Your ONLY response should be a single short sentence like "Here are your conversations." Do NOT offer help, create todos, or ask follow-up questions.',
114+
name: "x_conversations",
115+
description:
116+
'Show X conversations awaiting your reply. IMPORTANT: The UI shows everything the user needs. Your ONLY response should be a single short sentence like "Here are your conversations." Do NOT offer help, create todos, or ask follow-up questions.',
113117
inputSchema: {
114-
type: 'object',
118+
type: "object",
115119
properties: {},
116120
required: [],
117121
},
@@ -123,45 +127,48 @@ const TOOLS: Tool[] = [
123127
},
124128
},
125129
{
126-
name: 'x_get_conversations',
127-
description: 'Internal tool to fetch conversation data for the UI with pagination.',
130+
name: "x_get_conversations",
131+
description:
132+
"Internal tool to fetch conversation data for the UI with pagination.",
128133
inputSchema: {
129-
type: 'object',
134+
type: "object",
130135
properties: {
131136
limit: {
132-
type: 'number',
133-
description: 'Maximum number of conversations to return (default: 10)',
137+
type: "number",
138+
description:
139+
"Maximum number of conversations to return (default: 10)",
134140
},
135141
offset: {
136-
type: 'number',
137-
description: 'Number of conversations to skip (for pagination)',
142+
type: "number",
143+
description: "Number of conversations to skip (for pagination)",
138144
},
139145
},
140146
required: [],
141147
},
142148
// Hidden from model - only callable by UI apps
143149
_meta: {
144150
ui: {
145-
visibility: ['app'],
151+
visibility: ["app"],
146152
},
147153
},
148154
},
149155
{
150-
name: 'x_dismiss_conversation',
151-
description: 'Dismiss a conversation from the list. It will reappear if there is new activity (new replies).',
156+
name: "x_dismiss_conversation",
157+
description:
158+
"Dismiss a conversation from the list. It will reappear if there is new activity (new replies).",
152159
inputSchema: {
153-
type: 'object',
160+
type: "object",
154161
properties: {
155162
tweet_id: {
156-
type: 'string',
157-
description: 'The tweet ID to dismiss',
163+
type: "string",
164+
description: "The tweet ID to dismiss",
158165
},
159166
reply_count: {
160-
type: 'number',
161-
description: 'Current reply count (used to detect new activity)',
167+
type: "number",
168+
description: "Current reply count (used to detect new activity)",
162169
},
163170
},
164-
required: ['tweet_id', 'reply_count'],
171+
required: ["tweet_id", "reply_count"],
165172
},
166173
// No UI - this is a data-only tool called from conversation-list UI
167174
},
@@ -180,23 +187,31 @@ const toolHandlers: Record<string, ToolHandler> = {
180187

181188
// Load bundled UI HTML from dist/ui/{name}/ui-apps/{name}.html
182189
async function loadUIResource(filename: string): Promise<string> {
183-
const baseName = filename.replace('.html', '');
190+
const baseName = filename.replace(".html", "");
184191
// Try from dist/ui/{name}/ui-apps/{name}.html (vite output location)
185-
const filePath = path.join(__dirname, 'ui', baseName, 'ui-apps', filename);
192+
const filePath = join(__dirname, "ui", baseName, "ui-apps", filename);
186193
try {
187-
return await fs.readFile(filePath, 'utf-8');
194+
return await readFile(filePath, "utf-8");
188195
} catch {
189196
// Fallback: try from project root dist/ui/{name}/ui-apps/{name}.html (for development)
190-
const altPath = path.join(__dirname, '..', 'dist', 'ui', baseName, 'ui-apps', filename);
191-
return await fs.readFile(altPath, 'utf-8');
197+
const altPath = join(
198+
__dirname,
199+
"..",
200+
"dist",
201+
"ui",
202+
baseName,
203+
"ui-apps",
204+
filename
205+
);
206+
return await readFile(altPath, "utf-8");
192207
}
193208
}
194209

195210
export function createServer(): Server {
196211
const server = new Server(
197212
{
198-
name: 'assa-mcp',
199-
version: '0.2.0',
213+
name: "assa-mcp",
214+
version: "0.2.0",
200215
},
201216
{
202217
capabilities: {
@@ -207,27 +222,27 @@ export function createServer(): Server {
207222
);
208223

209224
// Handle tool listing
210-
server.setRequestHandler(ListToolsRequestSchema, async () => {
225+
server.setRequestHandler(ListToolsRequestSchema, () => {
211226
return { tools: TOOLS };
212227
});
213228

214229
// Handle resource listing (for MCP Apps UI resources)
215-
server.setRequestHandler(ListResourcesRequestSchema, async () => {
230+
server.setRequestHandler(ListResourcesRequestSchema, () => {
216231
return {
217232
resources: [
218233
{
219234
uri: UI_RESOURCES.authButton,
220-
name: 'Auth Button UI',
235+
name: "Auth Button UI",
221236
mimeType: RESOURCE_MIME_TYPE,
222237
},
223238
{
224239
uri: UI_RESOURCES.tweetPreview,
225-
name: 'Tweet Preview UI',
240+
name: "Tweet Preview UI",
226241
mimeType: RESOURCE_MIME_TYPE,
227242
},
228243
{
229244
uri: UI_RESOURCES.conversationList,
230-
name: 'Conversation List UI',
245+
name: "Conversation List UI",
231246
mimeType: RESOURCE_MIME_TYPE,
232247
},
233248
],
@@ -240,9 +255,9 @@ export function createServer(): Server {
240255

241256
// Map URI to filename
242257
const uriToFile: Record<string, string> = {
243-
[UI_RESOURCES.authButton]: 'auth-button.html',
244-
[UI_RESOURCES.tweetPreview]: 'tweet-preview.html',
245-
[UI_RESOURCES.conversationList]: 'conversation-list.html',
258+
[UI_RESOURCES.authButton]: "auth-button.html",
259+
[UI_RESOURCES.tweetPreview]: "tweet-preview.html",
260+
[UI_RESOURCES.conversationList]: "conversation-list.html",
246261
};
247262

248263
const filename = uriToFile[uri];
@@ -266,11 +281,14 @@ export function createServer(): Server {
266281

267282
// Add CSP for UIs that load avatars from unavatar.io
268283
// auth-button also renders conversations after auth completes
269-
if (uri === UI_RESOURCES.conversationList || uri === UI_RESOURCES.authButton) {
284+
if (
285+
uri === UI_RESOURCES.conversationList ||
286+
uri === UI_RESOURCES.authButton
287+
) {
270288
resourceContent._meta = {
271289
ui: {
272290
csp: {
273-
resourceDomains: ['https://unavatar.io'],
291+
resourceDomains: ["https://unavatar.io"],
274292
},
275293
},
276294
};
@@ -294,11 +312,11 @@ export function createServer(): Server {
294312
const result = await handler(args ?? {});
295313
return result as CallToolResult;
296314
} catch (error) {
297-
const message = error instanceof Error ? error.message : 'Unknown error';
315+
const message = error instanceof Error ? error.message : "Unknown error";
298316
return {
299317
content: [
300318
{
301-
type: 'text',
319+
type: "text",
302320
text: `Error: ${message}`,
303321
},
304322
],

src/state/manager.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,9 @@ export function pruneStaleReplies(currentMentionIds: string[]): number {
198198
const initialLength = state.replied_to.length;
199199

200200
// Keep only IDs that are still in the current API results
201-
state.replied_to = state.replied_to.filter((tweetId) => currentSet.has(tweetId));
201+
state.replied_to = state.replied_to.filter((tweetId) =>
202+
currentSet.has(tweetId)
203+
);
202204

203205
const prunedCount = initialLength - state.replied_to.length;
204206
if (prunedCount > 0) {

0 commit comments

Comments
 (0)