feat(mcps): add wiki/packages/releases/projects to gitea + new umami & serpbear MCPs
This commit is contained in:
225
packages/serpbear-mcp/src/index.ts
Normal file
225
packages/serpbear-mcp/src/index.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import express from 'express';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import axios from "axios";
|
||||
|
||||
const SERPBEAR_BASE_URL = process.env.SERPBEAR_BASE_URL || "https://serpbear.infra.mintel.me";
|
||||
const SERPBEAR_API_KEY = process.env.SERPBEAR_API_KEY;
|
||||
|
||||
if (!SERPBEAR_API_KEY) {
|
||||
console.error("Warning: SERPBEAR_API_KEY is not set. API calls will fail.");
|
||||
}
|
||||
|
||||
const serpbearClient = axios.create({
|
||||
baseURL: `${SERPBEAR_BASE_URL}/api`,
|
||||
headers: { apiKey: SERPBEAR_API_KEY },
|
||||
});
|
||||
|
||||
// --- Tool Definitions ---
|
||||
const LIST_DOMAINS_TOOL: Tool = {
|
||||
name: "serpbear_list_domains",
|
||||
description: "List all domains/projects tracked in SerpBear",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
};
|
||||
|
||||
const GET_KEYWORDS_TOOL: Tool = {
|
||||
name: "serpbear_get_keywords",
|
||||
description: "Get all tracked keywords for a domain, with their current ranking positions",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
domain_id: { type: "string", description: "Domain ID from serpbear_list_domains" },
|
||||
},
|
||||
required: ["domain_id"],
|
||||
},
|
||||
};
|
||||
|
||||
const ADD_KEYWORDS_TOOL: Tool = {
|
||||
name: "serpbear_add_keywords",
|
||||
description: "Add new keywords to track for a domain",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
domain_id: { type: "string", description: "Domain ID" },
|
||||
keywords: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "List of keywords to add (e.g., ['Webentwickler Frankfurt', 'Next.js Agentur'])"
|
||||
},
|
||||
country: { type: "string", description: "Country code for SERP tracking (e.g., 'de', 'us'). Default: 'de'" },
|
||||
device: { type: "string", description: "Device type: 'desktop' or 'mobile'. Default: 'desktop'" },
|
||||
},
|
||||
required: ["domain_id", "keywords"],
|
||||
},
|
||||
};
|
||||
|
||||
const DELETE_KEYWORDS_TOOL: Tool = {
|
||||
name: "serpbear_delete_keywords",
|
||||
description: "Remove keywords from tracking",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
keyword_ids: {
|
||||
type: "array",
|
||||
items: { type: "number" },
|
||||
description: "Array of keyword IDs to delete"
|
||||
},
|
||||
},
|
||||
required: ["keyword_ids"],
|
||||
},
|
||||
};
|
||||
|
||||
const REFRESH_KEYWORDS_TOOL: Tool = {
|
||||
name: "serpbear_refresh_keywords",
|
||||
description: "Trigger an immediate SERP position refresh for specific keywords",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
keyword_ids: {
|
||||
type: "array",
|
||||
items: { type: "number" },
|
||||
description: "List of keyword IDs to refresh"
|
||||
},
|
||||
},
|
||||
required: ["keyword_ids"],
|
||||
},
|
||||
};
|
||||
|
||||
const GET_KEYWORD_HISTORY_TOOL: Tool = {
|
||||
name: "serpbear_get_keyword_history",
|
||||
description: "Get the ranking history for a specific keyword over time",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
keyword_id: { type: "number", description: "Keyword ID from serpbear_get_keywords" },
|
||||
},
|
||||
required: ["keyword_id"],
|
||||
},
|
||||
};
|
||||
|
||||
// --- Server Setup ---
|
||||
const server = new Server(
|
||||
{ name: "serpbear-mcp", version: "1.0.0" },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
LIST_DOMAINS_TOOL,
|
||||
GET_KEYWORDS_TOOL,
|
||||
ADD_KEYWORDS_TOOL,
|
||||
DELETE_KEYWORDS_TOOL,
|
||||
REFRESH_KEYWORDS_TOOL,
|
||||
GET_KEYWORD_HISTORY_TOOL,
|
||||
],
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name === "serpbear_list_domains") {
|
||||
try {
|
||||
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 {
|
||||
const res = await serpbearClient.get('/keywords', { params: { domain: domain_id } });
|
||||
const keywords = (res.data.keywords || []).map((k: any) => ({
|
||||
id: k.id,
|
||||
keyword: k.keyword,
|
||||
position: k.position,
|
||||
lastUpdated: k.lastUpdated,
|
||||
country: k.country,
|
||||
device: k.device,
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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}`);
|
||||
});
|
||||
|
||||
// --- Express / SSE Server ---
|
||||
async function run() {
|
||||
const app = express();
|
||||
let transport: SSEServerTransport | null = null;
|
||||
|
||||
app.get('/sse', async (req, res) => {
|
||||
console.error('New SSE connection established');
|
||||
transport = new SSEServerTransport('/message', res);
|
||||
await server.connect(transport);
|
||||
});
|
||||
|
||||
app.post('/message', async (req, res) => {
|
||||
if (!transport) { res.status(400).send('No active SSE connection'); return; }
|
||||
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`);
|
||||
});
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.error("Fatal error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
13
packages/serpbear-mcp/src/start.ts
Normal file
13
packages/serpbear-mcp/src/start.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { config } from 'dotenv';
|
||||
import { resolve } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
config({ path: resolve(__dirname, '../../../.env.local') });
|
||||
config({ path: resolve(__dirname, '../../../.env') });
|
||||
|
||||
import('./index.js').catch(err => {
|
||||
console.error('Failed to start SerpBear MCP Server:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user