feat(mcps): add wiki/packages/releases/projects to gitea + new umami & serpbear MCPs

This commit is contained in:
2026-03-05 10:52:05 +01:00
parent dca35a9900
commit 5c10eb0009
11 changed files with 985 additions and 8 deletions

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

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