fix(next-config): move serverActions to experimental for Next.js 15+ compatibility
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 18s
Monorepo Pipeline / 🧪 Test (push) Failing after 10s
Monorepo Pipeline / 🏗️ Build (push) Failing after 10s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 18s
Monorepo Pipeline / 🧪 Test (push) Failing after 10s
Monorepo Pipeline / 🏗️ Build (push) Failing after 10s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
This commit is contained in:
@@ -19,10 +19,28 @@ if (!SERPBEAR_API_KEY) {
|
||||
|
||||
const serpbearClient = axios.create({
|
||||
baseURL: `${SERPBEAR_BASE_URL}/api`,
|
||||
headers: { apiKey: SERPBEAR_API_KEY },
|
||||
headers: { 'x-api-key': SERPBEAR_API_KEY || '' },
|
||||
httpsAgent: new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
}),
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
// Logging middleware for SerpBear client
|
||||
serpbearClient.interceptors.request.use((config) => {
|
||||
console.error(`SerpBear Request: ${config.method?.toUpperCase()} ${config.url}`);
|
||||
return config;
|
||||
}, (error) => {
|
||||
console.error(`SerpBear Request Error: ${error.message}`);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
serpbearClient.interceptors.response.use((response) => {
|
||||
return response;
|
||||
}, (error) => {
|
||||
const msg = error.response?.data?.error || error.response?.data?.message || error.message;
|
||||
console.error(`SerpBear API Error (${error.config?.url}): ${msg}`);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
|
||||
@@ -126,21 +144,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name === "serpbear_list_domains") {
|
||||
try {
|
||||
try {
|
||||
if (request.params.name === "serpbear_list_domains") {
|
||||
const res = await serpbearClient.get('/domains');
|
||||
const domains = (res.data.domains || []).map((d: any) => ({
|
||||
id: d.id, domain: d.domain, keywords: d.keywordCount
|
||||
}));
|
||||
return { content: [{ type: "text", text: JSON.stringify(domains, null, 2) }] };
|
||||
} catch (e: any) {
|
||||
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
|
||||
}
|
||||
}
|
||||
|
||||
if (request.params.name === "serpbear_get_keywords") {
|
||||
const { domain_id } = request.params.arguments as any;
|
||||
try {
|
||||
if (request.params.name === "serpbear_get_keywords") {
|
||||
const { domain_id } = request.params.arguments as any;
|
||||
const res = await serpbearClient.get('/keywords', { params: { domain: domain_id } });
|
||||
const keywords = (res.data.keywords || []).map((k: any) => ({
|
||||
id: k.id,
|
||||
@@ -152,55 +166,40 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
change: k.position_change ?? null,
|
||||
}));
|
||||
return { content: [{ type: "text", text: JSON.stringify(keywords, null, 2) }] };
|
||||
} catch (e: any) {
|
||||
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
|
||||
}
|
||||
}
|
||||
|
||||
if (request.params.name === "serpbear_add_keywords") {
|
||||
const { domain_id, keywords, country = 'de', device = 'desktop' } = request.params.arguments as any;
|
||||
try {
|
||||
if (request.params.name === "serpbear_add_keywords") {
|
||||
const { domain_id, keywords, country = 'de', device = 'desktop' } = request.params.arguments as any;
|
||||
const res = await serpbearClient.post('/keywords', {
|
||||
domain: domain_id,
|
||||
keywords: keywords.map((kw: string) => ({ keyword: kw, country, device })),
|
||||
});
|
||||
return { content: [{ type: "text", text: `Added ${keywords.length} keywords. Result: ${JSON.stringify(res.data)}` }] };
|
||||
} catch (e: any) {
|
||||
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
|
||||
}
|
||||
}
|
||||
|
||||
if (request.params.name === "serpbear_delete_keywords") {
|
||||
const { keyword_ids } = request.params.arguments as any;
|
||||
try {
|
||||
if (request.params.name === "serpbear_delete_keywords") {
|
||||
const { keyword_ids } = request.params.arguments as any;
|
||||
await serpbearClient.delete('/keywords', { data: { ids: keyword_ids } });
|
||||
return { content: [{ type: "text", text: `Deleted ${keyword_ids.length} keywords.` }] };
|
||||
} catch (e: any) {
|
||||
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
|
||||
}
|
||||
}
|
||||
|
||||
if (request.params.name === "serpbear_refresh_keywords") {
|
||||
const { keyword_ids } = request.params.arguments as any;
|
||||
try {
|
||||
if (request.params.name === "serpbear_refresh_keywords") {
|
||||
const { keyword_ids } = request.params.arguments as any;
|
||||
await serpbearClient.post('/keywords/refresh', { ids: keyword_ids });
|
||||
return { content: [{ type: "text", text: `Triggered refresh for ${keyword_ids.length} keywords.` }] };
|
||||
} catch (e: any) {
|
||||
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
|
||||
}
|
||||
}
|
||||
|
||||
if (request.params.name === "serpbear_get_keyword_history") {
|
||||
const { keyword_id } = request.params.arguments as any;
|
||||
try {
|
||||
if (request.params.name === "serpbear_get_keyword_history") {
|
||||
const { keyword_id } = request.params.arguments as any;
|
||||
const res = await serpbearClient.get(`/keywords/${keyword_id}/history`);
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
} catch (e: any) {
|
||||
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||
} catch (e: any) {
|
||||
const msg = e.response?.data?.error || e.response?.data?.message || e.message;
|
||||
return { isError: true, content: [{ type: "text", text: `SerpBear Service Error: ${msg}` }] };
|
||||
}
|
||||
});
|
||||
|
||||
// --- Express / SSE Server ---
|
||||
@@ -216,28 +215,51 @@ async function run() {
|
||||
const app = express();
|
||||
const transports = new Map<string, SSEServerTransport>();
|
||||
|
||||
// Manual CORS middleware
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-mcp-protocol-version');
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).end();
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
app.use((req, _res, next) => {
|
||||
console.error(`${req.method} ${req.url}`);
|
||||
next();
|
||||
});
|
||||
|
||||
app.get('/sse', async (req, res) => {
|
||||
const sessionId = crypto.randomUUID();
|
||||
const transport = new SSEServerTransport('/message', res as any);
|
||||
const sessionId = transport.sessionId;
|
||||
console.error(`New SSE connection: ${sessionId}`);
|
||||
const transport = new SSEServerTransport(`/message/${sessionId}`, res);
|
||||
transports.set(sessionId, transport);
|
||||
|
||||
const heartbeatInterval = setInterval(() => {
|
||||
res.write(": heartbeat\n\n");
|
||||
}, 15000);
|
||||
|
||||
req.on('close', () => {
|
||||
console.error(`SSE connection closed: ${sessionId}`);
|
||||
clearInterval(heartbeatInterval);
|
||||
transports.delete(sessionId);
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
req.on('close', resolve);
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/message/:sessionId', async (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
const transport = transports.get(sessionId as string);
|
||||
app.post('/message', express.json(), async (req, res) => {
|
||||
const sessionId = req.query.sessionId as string;
|
||||
const transport = transports.get(sessionId);
|
||||
|
||||
console.error(`POST /message/${sessionId} - Active transports: ${transports.size}`);
|
||||
|
||||
if (!transport) {
|
||||
console.error(`No transport found for session: ${sessionId}`);
|
||||
@@ -247,9 +269,9 @@ async function run() {
|
||||
await transport.handlePostMessage(req, res);
|
||||
});
|
||||
|
||||
const PORT = process.env.SERPBEAR_MCP_PORT || 3004;
|
||||
app.listen(PORT, () => {
|
||||
console.error(`SerpBear MCP server running on http://localhost:${PORT}/sse`);
|
||||
const PORT = Number(process.env.SERPBEAR_MCP_PORT) || 3004;
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.error(`SerpBear MCP server running on http://0.0.0.0:${PORT}/sse`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user