← back to Affiliate Feeds Mcp
bin/server.js
79 lines
#!/usr/bin/env node
// affiliate-feeds MCP server.
// Exposes Amazon PA-API v5, Rakuten Advertising, and ShareASale product search as
// MCP tools by wrapping the credential-gated adapters copied from the
// interiordesignershowroom build. Each tool is safe to call with no credentials —
// it returns a clear "not configured" message instead of failing.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { createRequire } from 'module';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
// The adapters are CommonJS (node built-ins only); load them from ESM.
const require = createRequire(import.meta.url);
const adaptersDir = join(dirname(fileURLToPath(import.meta.url)), '..', 'adapters');
const amazon = require(join(adaptersDir, 'amazon.cjs'));
const rakuten = require(join(adaptersDir, 'rakuten.cjs'));
const shareasale = require(join(adaptersDir, 'shareasale.cjs'));
const NETWORKS = {
amazon: { adapter: amazon, env: ['AMAZON_ACCESS_KEY', 'AMAZON_SECRET_KEY', 'AMAZON_PARTNER_TAG'],
label: 'Amazon Associates (PA-API v5)' },
rakuten: { adapter: rakuten, env: ['RAKUTEN_CLIENT_ID', 'RAKUTEN_CLIENT_SECRET', 'RAKUTEN_SCOPE'],
label: 'Rakuten Advertising' },
shareasale: { adapter: shareasale, env: ['SHAREASALE_AFFILIATE_ID', 'SHAREASALE_API_TOKEN', 'SHAREASALE_API_SECRET'],
label: 'ShareASale' },
};
const server = new McpServer({ name: 'affiliate-feeds', version: '0.1.0' });
function registerSearch(key) {
const { adapter, env, label } = NETWORKS[key];
server.registerTool(
`${key}_search`,
{
title: `${label} product search`,
description: `Search ${label} for affiliate products by keyword. Returns normalized product objects `
+ `(title, brand, price, image_url, affiliate_url, advertiser). Requires the ${key.toUpperCase()} `
+ `credentials in the server env; returns a "not configured" note if absent.`,
inputSchema: {
keywords: z.string().optional().describe('Search terms, e.g. "velvet sofa" (defaults to home/furniture terms)'),
limit: z.number().int().positive().max(50).optional().describe('Max products to return (default 10)'),
},
},
async ({ keywords, limit }) => {
if (!adapter.enabled(process.env)) {
return { content: [{ type: 'text',
text: `${label} is not configured. Set these env vars on the affiliate-feeds MCP server: ${env.join(', ')}.` }] };
}
let products = [];
try {
products = await adapter.fetch(process.env, { keywords, limit: limit || 10 });
} catch (e) {
return { content: [{ type: 'text', text: `${label} search error: ${e && e.message}` }], isError: true };
}
return { content: [{ type: 'text',
text: JSON.stringify({ network: key, keywords: keywords || '(default)', count: products.length, products }, null, 2) }] };
}
);
}
Object.keys(NETWORKS).forEach(registerSearch);
// A tiny status tool so you can see which networks are wired without a live call.
server.registerTool('affiliate_status', {
title: 'Affiliate networks configuration status',
description: 'Reports which affiliate networks have credentials configured on this MCP server.',
inputSchema: {},
}, async () => {
const status = Object.fromEntries(Object.entries(NETWORKS).map(([k, v]) =>
[k, v.adapter.enabled(process.env) ? 'configured' : `missing: ${v.env.join(', ')}`]));
return { content: [{ type: 'text', text: JSON.stringify(status, null, 2) }] };
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('[affiliate-feeds] MCP server ready (stdio)');