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:
@@ -12,10 +12,10 @@
|
||||
"@modelcontextprotocol/sdk": "^1.5.0",
|
||||
"axios": "^1.7.2",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1"
|
||||
"express": "^4.19.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.14.10",
|
||||
"typescript": "^5.5.3"
|
||||
}
|
||||
|
||||
@@ -26,8 +26,9 @@ if (!GITEA_ACCESS_TOKEN) {
|
||||
const giteaClient = axios.create({
|
||||
baseURL: `${GITEA_HOST.replace(/\/$/, "")}/api/v1`,
|
||||
headers: {
|
||||
Authorization: `token ${GITEA_ACCESS_TOKEN}`,
|
||||
Authorization: `token ${GITEA_ACCESS_TOKEN || ''}`,
|
||||
},
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
const LIST_PIPELINES_TOOL: Tool = {
|
||||
@@ -1275,6 +1276,18 @@ 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();
|
||||
});
|
||||
|
||||
// Middleware to log all requests for debugging
|
||||
app.use((req, _res, next) => {
|
||||
console.error(`${req.method} ${req.url}`);
|
||||
@@ -1282,34 +1295,45 @@ async function run() {
|
||||
});
|
||||
|
||||
app.get("/sse", async (req, res) => {
|
||||
const sessionId = crypto.randomUUID();
|
||||
console.error(`New SSE connection: ${sessionId}`);
|
||||
const transport = new SSEServerTransport(`/message/${sessionId}`, res);
|
||||
const transport = new SSEServerTransport("/message", res as any);
|
||||
const sessionId = transport.sessionId;
|
||||
transports.set(sessionId, transport);
|
||||
console.error(`New SSE connection: ${sessionId}`);
|
||||
|
||||
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);
|
||||
|
||||
// Keep the connection open indefinitely until client closes
|
||||
await new Promise((resolve) => {
|
||||
req.on("close", resolve);
|
||||
});
|
||||
});
|
||||
|
||||
app.post("/message/:sessionId", async (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
app.post("/message", express.json(), async (req, res) => {
|
||||
const sessionId = req.query.sessionId as string;
|
||||
const transport = transports.get(sessionId);
|
||||
|
||||
if (!transport) {
|
||||
console.error(`No transport found for session: ${sessionId}`);
|
||||
res.status(400).send("No active SSE connection for this session");
|
||||
res.status(404).send("No active SSE connection for this session");
|
||||
return;
|
||||
}
|
||||
|
||||
await transport.handlePostMessage(req, res);
|
||||
});
|
||||
|
||||
const PORT = process.env.GITEA_MCP_PORT || 3001;
|
||||
app.listen(PORT, () => {
|
||||
console.error(`Gitea MCP server running on http://localhost:${PORT}/sse`);
|
||||
const PORT = Number(process.env.GITEA_MCP_PORT) || 3001;
|
||||
app.listen(PORT, "0.0.0.0", () => {
|
||||
console.error(`Gitea MCP server running on http://0.0.0.0:${PORT}/sse`);
|
||||
});
|
||||
|
||||
// Start the background poller only in SSE mode or if specifically desired
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
"@modelcontextprotocol/sdk": "^1.5.0",
|
||||
"axios": "^1.7.2",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1"
|
||||
"express": "^4.19.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.14.10",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.5.3"
|
||||
|
||||
@@ -24,7 +24,25 @@ const httpsAgent = new https.Agent({
|
||||
const glitchtipClient = axios.create({
|
||||
baseURL: `${GLITCHTIP_BASE_URL}/api/0`,
|
||||
headers: { Authorization: `Bearer ${GLITCHTIP_API_KEY}` },
|
||||
httpsAgent
|
||||
httpsAgent,
|
||||
timeout: 10000 // Add timeout to prevent hangs
|
||||
});
|
||||
|
||||
// Logging middleware for GlitchTip client
|
||||
glitchtipClient.interceptors.request.use((config) => {
|
||||
console.error(`GlitchTip Request: ${config.method?.toUpperCase()} ${config.url}`);
|
||||
return config;
|
||||
}, (error) => {
|
||||
console.error(`GlitchTip Request Error: ${error.message}`);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
glitchtipClient.interceptors.response.use((response) => {
|
||||
return response;
|
||||
}, (error) => {
|
||||
const msg = error.response?.data?.detail || error.response?.data?.message || error.message;
|
||||
console.error(`GlitchTip API Error (${error.config?.url}): ${msg}`);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
const LIST_PROJECTS_TOOL: Tool = {
|
||||
@@ -88,48 +106,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name === "glitchtip_list_projects") {
|
||||
try {
|
||||
try {
|
||||
if (request.params.name === "glitchtip_list_projects") {
|
||||
const res = await glitchtipClient.get('/projects/');
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
} catch (e: any) {
|
||||
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
|
||||
}
|
||||
}
|
||||
|
||||
if (request.params.name === "glitchtip_list_issues") {
|
||||
const { organization_slug, project_slug, query, limit = 20 } = request.params.arguments as any;
|
||||
try {
|
||||
if (request.params.name === "glitchtip_list_issues") {
|
||||
const { organization_slug, project_slug, query, limit = 20 } = request.params.arguments as any;
|
||||
const res = await glitchtipClient.get(`/projects/${organization_slug}/${project_slug}/issues/`, {
|
||||
params: { query, limit }
|
||||
});
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
} catch (e: any) {
|
||||
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
|
||||
}
|
||||
}
|
||||
|
||||
if (request.params.name === "glitchtip_get_issue_details") {
|
||||
const { issue_id } = request.params.arguments as any;
|
||||
try {
|
||||
if (request.params.name === "glitchtip_get_issue_details") {
|
||||
const { issue_id } = request.params.arguments as any;
|
||||
const res = await glitchtipClient.get(`/issues/${issue_id}/`);
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
} catch (e: any) {
|
||||
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
|
||||
}
|
||||
}
|
||||
|
||||
if (request.params.name === "glitchtip_update_issue") {
|
||||
const { issue_id, status } = request.params.arguments as any;
|
||||
try {
|
||||
if (request.params.name === "glitchtip_update_issue") {
|
||||
const { issue_id, status } = request.params.arguments as any;
|
||||
const res = await glitchtipClient.put(`/issues/${issue_id}/`, { status });
|
||||
return { content: [{ type: "text", text: `Issue ${issue_id} status updated to ${status}.` }] };
|
||||
} 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?.detail || e.response?.data?.message || e.message;
|
||||
return { isError: true, content: [{ type: "text", text: `GlitchTip Service Error: ${msg}` }] };
|
||||
}
|
||||
});
|
||||
|
||||
async function run() {
|
||||
@@ -144,28 +151,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: Request, res: Response) => {
|
||||
const sessionId = req.params.sessionId;
|
||||
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}`);
|
||||
@@ -175,9 +205,9 @@ async function run() {
|
||||
await transport.handlePostMessage(req, res);
|
||||
});
|
||||
|
||||
const PORT = process.env.GLITCHTIP_MCP_PORT || 3005;
|
||||
app.listen(PORT, () => {
|
||||
console.error(`GlitchTip MCP server running on http://localhost:${PORT}/sse`);
|
||||
const PORT = Number(process.env.GLITCHTIP_MCP_PORT) || 3005;
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.error(`GlitchTip MCP server running on http://0.0.0.0:${PORT}/sse`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
"dev": "tsx watch src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"@modelcontextprotocol/sdk": "^1.5.0",
|
||||
"axios": "^1.7.2",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1"
|
||||
"express": "^4.19.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.14.10",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.5.3"
|
||||
|
||||
@@ -591,15 +591,27 @@ 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: Request, res: Response) => {
|
||||
const sessionId = crypto.randomUUID();
|
||||
app.get('/sse', async (req, res) => {
|
||||
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);
|
||||
|
||||
req.on('close', () => {
|
||||
@@ -608,11 +620,17 @@ async function run() {
|
||||
});
|
||||
|
||||
await server.connect(transport);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
req.on('close', resolve);
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/message/:sessionId', async (req: Request, res: Response) => {
|
||||
const sessionId = req.params.sessionId;
|
||||
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}`);
|
||||
@@ -622,9 +640,9 @@ async function run() {
|
||||
await transport.handlePostMessage(req, res);
|
||||
});
|
||||
|
||||
const PORT = process.env.KLZ_PAYLOAD_MCP_PORT || 3006;
|
||||
app.listen(PORT, () => {
|
||||
console.error(`KLZ Payload MCP server running on http://localhost:${PORT}/sse`);
|
||||
const PORT = Number(process.env.KLZ_PAYLOAD_MCP_PORT) || 3006;
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.error(`KLZ Payload MCP server running on http://0.0.0.0:${PORT}/sse`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
"@xenova/transformers": "^2.17.2",
|
||||
"onnxruntime-node": "^1.14.0",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"express": "^4.19.2",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.14.10",
|
||||
"tsx": "^4.19.1",
|
||||
"typescript": "^5.5.3",
|
||||
|
||||
@@ -80,28 +80,51 @@ async function main() {
|
||||
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}`);
|
||||
@@ -111,9 +134,9 @@ async function main() {
|
||||
await transport.handlePostMessage(req, res);
|
||||
});
|
||||
|
||||
const PORT = process.env.MEMORY_MCP_PORT || 3002;
|
||||
app.listen(PORT, async () => {
|
||||
console.error(`Memory MCP server running on http://localhost:${PORT}/sse`);
|
||||
const PORT = Number(process.env.MEMORY_MCP_PORT) || 3002;
|
||||
app.listen(PORT, '0.0.0.0', async () => {
|
||||
console.error(`Memory MCP server running on http://0.0.0.0:${PORT}/sse`);
|
||||
// Initialize dependencies in SSE mode on startup
|
||||
try {
|
||||
await qdrantService.initialize();
|
||||
|
||||
@@ -8,8 +8,10 @@ import path from "node:path";
|
||||
export const baseNextConfig = {
|
||||
output: "standalone",
|
||||
turbopack: {},
|
||||
serverActions: {
|
||||
allowedOrigins: ["*.klz-cables.com", "*.branch.klz-cables.com", "localhost:3000", "*.mintel.me"],
|
||||
experimental: {
|
||||
serverActions: {
|
||||
allowedOrigins: ["*.klz-cables.com", "*.branch.klz-cables.com", "localhost:3000", "*.mintel.me"],
|
||||
},
|
||||
},
|
||||
images: {
|
||||
dangerouslyAllowSVG: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mintel/payload-ai",
|
||||
"version": "1.9.17",
|
||||
"version": "1.9.18",
|
||||
"description": "Reusable Payload CMS AI Extensions",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
|
||||
import { tool, jsonSchema } from "ai";
|
||||
/**
|
||||
* Connects to an external MCP Server and maps its tools to Vercel AI SDK Tools.
|
||||
*/
|
||||
@@ -54,9 +52,7 @@ export async function createMcpTools(mcpConfig: {
|
||||
|
||||
aiSdkTools[`${mcpConfig.name}_${extTool.name}`] = tool({
|
||||
description: `[From ${mcpConfig.name}] ${extTool.description || extTool.name}`,
|
||||
parameters: z
|
||||
.any()
|
||||
.describe("JSON matching the original MCP input_schema"), // Simplify for prototype
|
||||
parameters: jsonSchema(toolSchema as any),
|
||||
// @ts-expect-error - AI strict mode overload bug with implicit zod inferences
|
||||
execute: async (args: any) => {
|
||||
const result = await client.callTool({
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
"@modelcontextprotocol/sdk": "^1.5.0",
|
||||
"axios": "^1.7.2",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1"
|
||||
"express": "^4.19.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.14.10",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.5.3"
|
||||
|
||||
@@ -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`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
"@modelcontextprotocol/sdk": "^1.5.0",
|
||||
"axios": "^1.7.2",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1"
|
||||
"express": "^4.19.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.14.10",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.5.3"
|
||||
|
||||
@@ -27,17 +27,28 @@ if (!UMAMI_USERNAME && !UMAMI_API_KEY) {
|
||||
let cachedToken: string | null = null;
|
||||
|
||||
async function getAuthHeaders(): Promise<Record<string, string>> {
|
||||
if (UMAMI_API_KEY) {
|
||||
return { 'x-umami-api-key': UMAMI_API_KEY };
|
||||
try {
|
||||
if (UMAMI_API_KEY) {
|
||||
return { 'x-umami-api-key': UMAMI_API_KEY };
|
||||
}
|
||||
if (!cachedToken) {
|
||||
console.error(`Attempting Umami login for ${UMAMI_USERNAME} at ${UMAMI_BASE_URL}`);
|
||||
const res = await axios.post(`${UMAMI_BASE_URL}/api/auth/login`, {
|
||||
username: UMAMI_USERNAME,
|
||||
password: UMAMI_PASSWORD,
|
||||
}, {
|
||||
httpsAgent,
|
||||
timeout: 5000 // Add timeout to prevent hangs
|
||||
});
|
||||
cachedToken = res.data.token;
|
||||
console.error("Umami login successful");
|
||||
}
|
||||
return { Authorization: `Bearer ${cachedToken}` };
|
||||
} catch (e: any) {
|
||||
const msg = e.response?.data?.message || e.message;
|
||||
console.error(`Umami Auth Error: ${msg}`);
|
||||
throw new Error(`Authentication failed: ${msg}`);
|
||||
}
|
||||
if (!cachedToken) {
|
||||
const res = await axios.post(`${UMAMI_BASE_URL}/api/auth/login`, {
|
||||
username: UMAMI_USERNAME,
|
||||
password: UMAMI_PASSWORD,
|
||||
}, { httpsAgent });
|
||||
cachedToken = res.data.token;
|
||||
}
|
||||
return { Authorization: `Bearer ${cachedToken}` };
|
||||
}
|
||||
|
||||
|
||||
@@ -153,92 +164,72 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
}));
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const headers = await getAuthHeaders();
|
||||
const api = axios.create({ baseURL: `${UMAMI_BASE_URL}/api`, headers, httpsAgent });
|
||||
|
||||
const now = Date.now();
|
||||
try {
|
||||
const headers = await getAuthHeaders();
|
||||
const api = axios.create({ baseURL: `${UMAMI_BASE_URL}/api`, headers, httpsAgent, timeout: 10000 });
|
||||
const now = Date.now();
|
||||
|
||||
if (request.params.name === "umami_list_websites") {
|
||||
try {
|
||||
const res = await api.get('/websites');
|
||||
const sites = (res.data.data || res.data || []).map((s: any) => ({
|
||||
id: s.id, name: s.name, domain: s.domain
|
||||
}));
|
||||
return { content: [{ type: "text", text: JSON.stringify(sites, null, 2) }] };
|
||||
} catch (e: any) {
|
||||
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
|
||||
}
|
||||
const res = await api.get('/websites');
|
||||
const sites = (res.data.data || res.data || []).map((s: any) => ({
|
||||
id: s.id, name: s.name, domain: s.domain
|
||||
}));
|
||||
return { content: [{ type: "text", text: JSON.stringify(sites, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "umami_get_website_stats") {
|
||||
const { website_id, start_at, end_at = now } = request.params.arguments as any;
|
||||
try {
|
||||
const res = await api.get(`/websites/${website_id}/stats`, { params: { startAt: start_at, endAt: end_at } });
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
} catch (e: any) {
|
||||
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
|
||||
}
|
||||
const res = await api.get(`/websites/${website_id}/stats`, { params: { startAt: start_at, endAt: end_at } });
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "umami_get_pageviews") {
|
||||
const { website_id, start_at, end_at = now, unit = 'day', timezone = 'Europe/Berlin' } = request.params.arguments as any;
|
||||
try {
|
||||
const res = await api.get(`/websites/${website_id}/pageviews`, {
|
||||
params: { startAt: start_at, endAt: end_at, unit, timezone }
|
||||
});
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
} catch (e: any) {
|
||||
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
|
||||
}
|
||||
const res = await api.get(`/websites/${website_id}/pageviews`, {
|
||||
params: { startAt: start_at, endAt: end_at, unit, timezone }
|
||||
});
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "umami_get_top_pages") {
|
||||
const { website_id, start_at, end_at = now, limit = 20 } = request.params.arguments as any;
|
||||
try {
|
||||
const res = await api.get(`/websites/${website_id}/metrics`, {
|
||||
params: { startAt: start_at, endAt: end_at, type: 'url', limit }
|
||||
});
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
} catch (e: any) {
|
||||
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
|
||||
}
|
||||
const res = await api.get(`/websites/${website_id}/metrics`, {
|
||||
params: { startAt: start_at, endAt: end_at, type: 'url', limit }
|
||||
});
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "umami_get_top_referrers") {
|
||||
const { website_id, start_at, end_at = now, limit = 10 } = request.params.arguments as any;
|
||||
try {
|
||||
const res = await api.get(`/websites/${website_id}/metrics`, {
|
||||
params: { startAt: start_at, endAt: end_at, type: 'referrer', limit }
|
||||
});
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
} catch (e: any) {
|
||||
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
|
||||
}
|
||||
const res = await api.get(`/websites/${website_id}/metrics`, {
|
||||
params: { startAt: start_at, endAt: end_at, type: 'referrer', limit }
|
||||
});
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "umami_get_country_stats") {
|
||||
const { website_id, start_at, end_at = now } = request.params.arguments as any;
|
||||
try {
|
||||
const res = await api.get(`/websites/${website_id}/metrics`, {
|
||||
params: { startAt: start_at, endAt: end_at, type: 'country' }
|
||||
});
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
} catch (e: any) {
|
||||
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
|
||||
}
|
||||
const res = await api.get(`/websites/${website_id}/metrics`, {
|
||||
params: { startAt: start_at, endAt: end_at, type: 'country' }
|
||||
});
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
if (request.params.name === "umami_get_active_visitors") {
|
||||
const { website_id } = request.params.arguments as any;
|
||||
try {
|
||||
const res = await api.get(`/websites/${website_id}/active`);
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
} catch (e: any) {
|
||||
return { isError: true, content: [{ type: "text", text: `Error: ${e.message}` }] };
|
||||
}
|
||||
const res = await api.get(`/websites/${website_id}/active`);
|
||||
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
||||
}
|
||||
|
||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||
|
||||
|
||||
} catch (e: any) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{ type: "text", text: `MCP Service Error: ${e.message}` }]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// --- Express / SSE Server ---
|
||||
@@ -254,28 +245,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}`);
|
||||
@@ -285,9 +299,9 @@ async function run() {
|
||||
await transport.handlePostMessage(req, res);
|
||||
});
|
||||
|
||||
const PORT = process.env.UMAMI_MCP_PORT || 3003;
|
||||
app.listen(PORT, () => {
|
||||
console.error(`Umami MCP server running on http://localhost:${PORT}/sse`);
|
||||
const PORT = Number(process.env.UMAMI_MCP_PORT) || 3003;
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.error(`Umami MCP server running on http://0.0.0.0:${PORT}/sse`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user