Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/clients/proxy-api-client.ts
Original file line number Diff line number Diff line change
@@ -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<WhitelistedIp[]> => {
const res = await axios.get<WhitelistedIp[]>(`${this.baseUrl}/whitelisted-ips`, {
headers: this.headers,
});
return res.data;
};

addWhitelistedIps = async (ips: string[]): Promise<void> => {
await axios.post(
`${this.baseUrl}/whitelisted-ips`,
{ IPAddressList: ips },
{ headers: this.headers }
);
};

removeWhitelistedIp = async (id: number): Promise<void> => {
await axios.delete(`${this.baseUrl}/whitelisted-ips/${id}`, {
headers: this.headers,
});
};
}
10 changes: 6 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ if (process.env.ENABLE_MCPS_LOGGER) {
import('mcps-logger/console');
}

const parseEnvsOrExit = (): Record<string, string> => {
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`);
}
Expand All @@ -20,18 +20,20 @@ const parseEnvsOrExit = (): Record<string, string> => {
return {
sapiUsername: process.env['SCRAPER_API_USERNAME'] as string,
sapiPassword: process.env['SCRAPER_API_PASSWORD'] as string,
decodoApiKey: process.env['DECODO_API_KEY'],
};
};

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);

Expand Down
24 changes: 23 additions & 1 deletion src/sapi-mcp-server.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
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 {
server: McpServer;

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',
Expand All @@ -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();
Expand All @@ -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() {
Expand Down
35 changes: 35 additions & 0 deletions src/tools/add-whitelisted-ips-tool.ts
Original file line number Diff line number Diff line change
@@ -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}`,
},
],
};
}
);
};
}
3 changes: 3 additions & 0 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
31 changes: 31 additions & 0 deletions src/tools/list-whitelisted-ips-tool.ts
Original file line number Diff line number Diff line change
@@ -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}` },
],
};
}
);
};
}
24 changes: 24 additions & 0 deletions src/tools/remove-whitelisted-ip-tool.ts
Original file line number Diff line number Diff line change
@@ -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}` },
],
};
}
);
};
}