diff --git a/src/clients/proxy-api-client.ts b/src/clients/proxy-api-client.ts new file mode 100644 index 0000000..ed88767 --- /dev/null +++ b/src/clients/proxy-api-client.ts @@ -0,0 +1,45 @@ +import axios from 'axios'; + +export type WhitelistedIp = { + id: number; + ip: string; + enabled: boolean; + created_at: string; +}; + +export class ProxyApiClient { + private apiKey: string; + private baseUrl = 'https://api.decodo.com/v2'; + + constructor({ apiKey }: { apiKey: string }) { + this.apiKey = apiKey; + } + + private get headers() { + return { + Authorization: this.apiKey, + 'Content-Type': 'application/json', + }; + } + + listWhitelistedIps = async (): Promise => { + const res = await axios.get(`${this.baseUrl}/whitelisted-ips`, { + headers: this.headers, + }); + return res.data; + }; + + addWhitelistedIps = async (ips: string[]): Promise => { + await axios.post( + `${this.baseUrl}/whitelisted-ips`, + { IPAddressList: ips }, + { headers: this.headers } + ); + }; + + removeWhitelistedIp = async (id: number): Promise => { + await axios.delete(`${this.baseUrl}/whitelisted-ips/${id}`, { + headers: this.headers, + }); + }; +} diff --git a/src/index.ts b/src/index.ts index ca59775..8c50792 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,10 +8,10 @@ if (process.env.ENABLE_MCPS_LOGGER) { import('mcps-logger/console'); } -const parseEnvsOrExit = (): Record => { - const envs = ['SCRAPER_API_USERNAME', 'SCRAPER_API_PASSWORD']; +const parseEnvsOrExit = (): { sapiUsername: string; sapiPassword: string; decodoApiKey?: string } => { + const requiredEnvs = ['SCRAPER_API_USERNAME', 'SCRAPER_API_PASSWORD']; - for (const envKey of envs) { + for (const envKey of requiredEnvs) { if (!process.env[envKey]) { exit(`env ${envKey} missing`); } @@ -20,6 +20,7 @@ const parseEnvsOrExit = (): Record => { return { sapiUsername: process.env['SCRAPER_API_USERNAME'] as string, sapiPassword: process.env['SCRAPER_API_PASSWORD'] as string, + decodoApiKey: process.env['DECODO_API_KEY'], }; }; @@ -27,11 +28,12 @@ async function main() { const transport = new StdioServerTransport(); // if there are no envs, some MCP clients will fail silently - const { sapiUsername, sapiPassword } = parseEnvsOrExit(); + const { sapiUsername, sapiPassword, decodoApiKey } = parseEnvsOrExit(); const sapiMcpServer = new ScraperAPIMCPServer({ sapiUsername, sapiPassword, + decodoApiKey, }); await sapiMcpServer.connect(transport); diff --git a/src/sapi-mcp-server.ts b/src/sapi-mcp-server.ts index e942ee5..35c8001 100644 --- a/src/sapi-mcp-server.ts +++ b/src/sapi-mcp-server.ts @@ -1,12 +1,16 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ScraperApiClient } from './clients/scraper-api-client'; +import { ProxyApiClient } from './clients/proxy-api-client'; import { AmazonSearchParsedTool, GoogleSearchParsedTool, RedditPostTool, RedditSubredditTool, ScrapeAsMarkdownTool, + ListWhitelistedIpsTool, + AddWhitelistedIpsTool, + RemoveWhitelistedIpTool, } from './tools'; export class ScraperAPIMCPServer { @@ -14,7 +18,17 @@ export class ScraperAPIMCPServer { sapiClient: ScraperApiClient; - constructor({ sapiUsername, sapiPassword }: { sapiUsername: string; sapiPassword: string }) { + proxyClient: ProxyApiClient | null; + + constructor({ + sapiUsername, + sapiPassword, + decodoApiKey, + }: { + sapiUsername: string; + sapiPassword: string; + decodoApiKey?: string; + }) { this.server = new McpServer({ name: 'decodo', version: '0.1.0', @@ -28,6 +42,7 @@ export class ScraperAPIMCPServer { const auth = Buffer.from(`${sapiUsername}:${sapiPassword}`).toString('base64'); this.sapiClient = new ScraperApiClient({ auth }); + this.proxyClient = decodoApiKey ? new ProxyApiClient({ apiKey: decodoApiKey }) : null; this.registerTools(); this.registerResources(); @@ -46,6 +61,13 @@ export class ScraperAPIMCPServer { AmazonSearchParsedTool.register({ server: this.server, sapiClient: this.sapiClient }); RedditPostTool.register({ server: this.server, sapiClient: this.sapiClient }); RedditSubredditTool.register({ server: this.server, sapiClient: this.sapiClient }); + + // proxy management (requires DECODO_API_KEY) + if (this.proxyClient) { + ListWhitelistedIpsTool.register({ server: this.server, proxyClient: this.proxyClient }); + AddWhitelistedIpsTool.register({ server: this.server, proxyClient: this.proxyClient }); + RemoveWhitelistedIpTool.register({ server: this.server, proxyClient: this.proxyClient }); + } } registerResources() { diff --git a/src/tools/add-whitelisted-ips-tool.ts b/src/tools/add-whitelisted-ips-tool.ts new file mode 100644 index 0000000..f3fac3a --- /dev/null +++ b/src/tools/add-whitelisted-ips-tool.ts @@ -0,0 +1,35 @@ +import z from 'zod'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ProxyApiClient } from '../clients/proxy-api-client'; + +export class AddWhitelistedIpsTool { + static register = ({ server, proxyClient }: { server: McpServer; proxyClient: ProxyApiClient }) => { + server.tool( + 'add_whitelisted_ips', + 'Add one or more IP addresses to the proxy authentication whitelist on your Decodo account. IPv4 only.', + { + ips: z + .array(z.string().ip({ version: 'v4' })) + .min(1) + .describe('List of IPv4 addresses to whitelist'), + }, + async ({ ips }: { ips: string[] }) => { + await proxyClient.addWhitelistedIps(ips); + const allIps = await proxyClient.listWhitelistedIps(); + + const formatted = allIps + .map(ip => `- **${ip.ip}** (id: ${ip.id}, enabled: ${ip.enabled})`) + .join('\n'); + + return { + content: [ + { + type: 'text', + text: `Successfully added ${ips.length} IP(s). Current whitelist (${allIps.length} total):\n\n${formatted}`, + }, + ], + }; + } + ); + }; +} diff --git a/src/tools/index.ts b/src/tools/index.ts index 066faab..d5a52ed 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -3,3 +3,6 @@ export * from './google-search-parsed-tool'; export * from './reddit-subreddit-tool'; export * from './reddit-post-tool'; export * from './scrape-as-markdown-tool'; +export * from './list-whitelisted-ips-tool'; +export * from './add-whitelisted-ips-tool'; +export * from './remove-whitelisted-ip-tool'; diff --git a/src/tools/list-whitelisted-ips-tool.ts b/src/tools/list-whitelisted-ips-tool.ts new file mode 100644 index 0000000..a6a1c69 --- /dev/null +++ b/src/tools/list-whitelisted-ips-tool.ts @@ -0,0 +1,31 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ProxyApiClient } from '../clients/proxy-api-client'; + +export class ListWhitelistedIpsTool { + static register = ({ server, proxyClient }: { server: McpServer; proxyClient: ProxyApiClient }) => { + server.tool( + 'list_whitelisted_ips', + 'List all IP addresses whitelisted for proxy authentication on your Decodo account', + {}, + async () => { + const ips = await proxyClient.listWhitelistedIps(); + + if (ips.length === 0) { + return { + content: [{ type: 'text', text: 'No whitelisted IPs found.' }], + }; + } + + const formatted = ips + .map(ip => `- **${ip.ip}** (id: ${ip.id}, enabled: ${ip.enabled}, added: ${ip.created_at})`) + .join('\n'); + + return { + content: [ + { type: 'text', text: `Found ${ips.length} whitelisted IP(s):\n\n${formatted}` }, + ], + }; + } + ); + }; +} diff --git a/src/tools/remove-whitelisted-ip-tool.ts b/src/tools/remove-whitelisted-ip-tool.ts new file mode 100644 index 0000000..dd4d755 --- /dev/null +++ b/src/tools/remove-whitelisted-ip-tool.ts @@ -0,0 +1,24 @@ +import z from 'zod'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { ProxyApiClient } from '../clients/proxy-api-client'; + +export class RemoveWhitelistedIpTool { + static register = ({ server, proxyClient }: { server: McpServer; proxyClient: ProxyApiClient }) => { + server.tool( + 'remove_whitelisted_ip', + 'Remove a whitelisted IP address from your Decodo account by its ID. Use list_whitelisted_ips to find the ID.', + { + id: z.number().describe('The ID of the whitelisted IP entry to remove (from list_whitelisted_ips)'), + }, + async ({ id }: { id: number }) => { + await proxyClient.removeWhitelistedIp(id); + + return { + content: [ + { type: 'text', text: `Successfully removed whitelisted IP entry with id: ${id}` }, + ], + }; + } + ); + }; +}