feat(mcps): unify SSE/Stdio transport and fix handshake timeouts
This commit is contained in:
@@ -15,10 +15,10 @@ import { z } from "zod";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
const GITEA_HOST = process.env.GITEA_HOST || "https://git.infra.mintel.me";
|
const GITEA_HOST = process.env.GITEA_HOST || "https://git.infra.mintel.me";
|
||||||
const GITEA_ACCESS_TOKEN = process.env.GITEA_ACCESS_TOKEN;
|
const GITEA_ACCESS_TOKEN = process.env.GITEA_ACCESS_TOKEN || process.env.GITEA_TOKEN;
|
||||||
|
|
||||||
if (!GITEA_ACCESS_TOKEN) {
|
if (!GITEA_ACCESS_TOKEN) {
|
||||||
console.error("Warning: GITEA_ACCESS_TOKEN environment variable is missing. Pipeline tools will return unauthorized errors.");
|
console.error("Warning: Neither GITEA_ACCESS_TOKEN nor GITEA_TOKEN environment variable is set. Pipeline tools will return unauthorized errors.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const giteaClient = axios.create({
|
const giteaClient = axios.create({
|
||||||
@@ -829,32 +829,42 @@ async function pollSubscriptions() {
|
|||||||
|
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
const app = express();
|
const isStdio = process.argv.includes('--stdio');
|
||||||
let transport: SSEServerTransport | null = null;
|
|
||||||
|
|
||||||
app.get('/sse', async (req, res) => {
|
if (isStdio) {
|
||||||
console.error('New SSE connection established');
|
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
||||||
transport = new SSEServerTransport('/message', res);
|
const transport = new StdioServerTransport();
|
||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
});
|
console.error('Gitea MCP server is running on stdio');
|
||||||
|
} else {
|
||||||
|
const app = express();
|
||||||
|
let transport: SSEServerTransport | null = null;
|
||||||
|
|
||||||
app.post('/message', async (req, res) => {
|
app.get('/sse', async (req, res) => {
|
||||||
if (!transport) {
|
console.error('New SSE connection established');
|
||||||
res.status(400).send('No active SSE connection');
|
transport = new SSEServerTransport('/message', res);
|
||||||
return;
|
await server.connect(transport);
|
||||||
}
|
});
|
||||||
await transport.handlePostMessage(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
const PORT = process.env.GITEA_MCP_PORT || 3001;
|
app.post('/message', async (req, res) => {
|
||||||
app.listen(PORT, () => {
|
if (!transport) {
|
||||||
console.error(`Gitea MCP Native Server running on http://localhost:${PORT}/sse`);
|
res.status(400).send('No active SSE connection');
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
await transport.handlePostMessage(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
// Start the background poller
|
const PORT = process.env.GITEA_MCP_PORT || 3001;
|
||||||
pollSubscriptions();
|
app.listen(PORT, () => {
|
||||||
|
console.error(`Gitea MCP server running on http://localhost:${PORT}/sse`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the background poller only in SSE mode or if specifically desired
|
||||||
|
pollSubscriptions();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
run().catch((error) => {
|
run().catch((error) => {
|
||||||
console.error("Fatal error:", error);
|
console.error("Fatal error:", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -132,25 +132,39 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
const app = express();
|
const isStdio = process.argv.includes('--stdio');
|
||||||
let transport: SSEServerTransport | null = null;
|
|
||||||
|
|
||||||
app.get('/sse', async (req, res) => {
|
if (isStdio) {
|
||||||
transport = new SSEServerTransport('/message', res);
|
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
});
|
console.error('GlitchTip MCP server is running on stdio');
|
||||||
|
} else {
|
||||||
|
const app = express();
|
||||||
|
let transport: SSEServerTransport | null = null;
|
||||||
|
|
||||||
app.post('/message', async (req, res) => {
|
app.get('/sse', async (req, res) => {
|
||||||
if (!transport) { res.status(400).send('No active SSE connection'); return; }
|
console.error('New SSE connection established');
|
||||||
await transport.handlePostMessage(req, res);
|
transport = new SSEServerTransport('/message', res);
|
||||||
});
|
await server.connect(transport);
|
||||||
|
});
|
||||||
|
|
||||||
const PORT = process.env.GLITCHTIP_MCP_PORT || 3005;
|
app.post('/message', async (req, res) => {
|
||||||
app.listen(PORT, () => {
|
if (!transport) {
|
||||||
console.error(`GlitchTip MCP server running on http://localhost:${PORT}/sse`);
|
res.status(400).send('No active SSE connection');
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
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`);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
run().catch((err) => {
|
run().catch((err) => {
|
||||||
console.error("Fatal error:", err);
|
console.error("Fatal error:", err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -12,14 +12,6 @@ async function main() {
|
|||||||
|
|
||||||
const qdrantService = new QdrantMemoryService(process.env.QDRANT_URL || 'http://localhost:6333');
|
const qdrantService = new QdrantMemoryService(process.env.QDRANT_URL || 'http://localhost:6333');
|
||||||
|
|
||||||
// Initialize embedding model and Qdrant connection
|
|
||||||
try {
|
|
||||||
await qdrantService.initialize();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to initialize local dependencies. Exiting.');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
'store_memory',
|
'store_memory',
|
||||||
'Store a new piece of knowledge/memory into the vector database. Use this to remember architectural decisions, preferences, aliases, etc.',
|
'Store a new piece of knowledge/memory into the vector database. Use this to remember architectural decisions, preferences, aliases, etc.',
|
||||||
@@ -71,10 +63,18 @@ async function main() {
|
|||||||
const isStdio = process.argv.includes('--stdio');
|
const isStdio = process.argv.includes('--stdio');
|
||||||
|
|
||||||
if (isStdio) {
|
if (isStdio) {
|
||||||
|
// Connect Stdio FIRST to avoid handshake timeouts while loading model
|
||||||
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
||||||
const transport = new StdioServerTransport();
|
const transport = new StdioServerTransport();
|
||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
console.error('Memory MCP server is running on stdio');
|
console.error('Memory MCP server is running on stdio');
|
||||||
|
|
||||||
|
// Initialize dependency after connection
|
||||||
|
try {
|
||||||
|
await qdrantService.initialize();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to initialize local dependencies:', e);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const app = express();
|
const app = express();
|
||||||
let transport: SSEServerTransport | null = null;
|
let transport: SSEServerTransport | null = null;
|
||||||
@@ -94,13 +94,19 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const PORT = process.env.MEMORY_MCP_PORT || 3002;
|
const PORT = process.env.MEMORY_MCP_PORT || 3002;
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, async () => {
|
||||||
console.error(`Memory MCP server is running on http://localhost:${PORT}/sse`);
|
console.error(`Memory MCP server running on http://localhost:${PORT}/sse`);
|
||||||
|
// Initialize dependencies in SSE mode on startup
|
||||||
|
try {
|
||||||
|
await qdrantService.initialize();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to initialize local dependencies:', e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((error) => {
|
main().catch((error) => {
|
||||||
console.error('Fatal error in main():', error);
|
console.error('Fatal error:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Tool,
|
Tool,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import https from "https";
|
||||||
|
|
||||||
const SERPBEAR_BASE_URL = process.env.SERPBEAR_BASE_URL || "https://serpbear.infra.mintel.me";
|
const SERPBEAR_BASE_URL = process.env.SERPBEAR_BASE_URL || "https://serpbear.infra.mintel.me";
|
||||||
const SERPBEAR_API_KEY = process.env.SERPBEAR_API_KEY;
|
const SERPBEAR_API_KEY = process.env.SERPBEAR_API_KEY;
|
||||||
@@ -18,8 +19,12 @@ if (!SERPBEAR_API_KEY) {
|
|||||||
const serpbearClient = axios.create({
|
const serpbearClient = axios.create({
|
||||||
baseURL: `${SERPBEAR_BASE_URL}/api`,
|
baseURL: `${SERPBEAR_BASE_URL}/api`,
|
||||||
headers: { apiKey: SERPBEAR_API_KEY },
|
headers: { apiKey: SERPBEAR_API_KEY },
|
||||||
|
httpsAgent: new https.Agent({
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// --- Tool Definitions ---
|
// --- Tool Definitions ---
|
||||||
const LIST_DOMAINS_TOOL: Tool = {
|
const LIST_DOMAINS_TOOL: Tool = {
|
||||||
name: "serpbear_list_domains",
|
name: "serpbear_list_domains",
|
||||||
@@ -199,26 +204,39 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
|
|
||||||
// --- Express / SSE Server ---
|
// --- Express / SSE Server ---
|
||||||
async function run() {
|
async function run() {
|
||||||
const app = express();
|
const isStdio = process.argv.includes('--stdio');
|
||||||
let transport: SSEServerTransport | null = null;
|
|
||||||
|
|
||||||
app.get('/sse', async (req, res) => {
|
if (isStdio) {
|
||||||
console.error('New SSE connection established');
|
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
||||||
transport = new SSEServerTransport('/message', res);
|
const transport = new StdioServerTransport();
|
||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
});
|
console.error('SerpBear MCP server is running on stdio');
|
||||||
|
} else {
|
||||||
|
const app = express();
|
||||||
|
let transport: SSEServerTransport | null = null;
|
||||||
|
|
||||||
app.post('/message', async (req, res) => {
|
app.get('/sse', async (req, res) => {
|
||||||
if (!transport) { res.status(400).send('No active SSE connection'); return; }
|
console.error('New SSE connection established');
|
||||||
await transport.handlePostMessage(req, res);
|
transport = new SSEServerTransport('/message', res);
|
||||||
});
|
await server.connect(transport);
|
||||||
|
});
|
||||||
|
|
||||||
const PORT = process.env.SERPBEAR_MCP_PORT || 3004;
|
app.post('/message', async (req, res) => {
|
||||||
app.listen(PORT, () => {
|
if (!transport) {
|
||||||
console.error(`SerpBear MCP server running on http://localhost:${PORT}/sse`);
|
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) => {
|
run().catch((err) => {
|
||||||
console.error("Fatal error:", err);
|
console.error("Fatal error:", err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -7,12 +7,17 @@ import {
|
|||||||
Tool,
|
Tool,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import https from "https";
|
||||||
|
|
||||||
const UMAMI_BASE_URL = process.env.UMAMI_BASE_URL || "https://umami.infra.mintel.me";
|
const UMAMI_BASE_URL = process.env.UMAMI_BASE_URL || "https://umami.infra.mintel.me";
|
||||||
const UMAMI_USERNAME = process.env.UMAMI_USERNAME;
|
const UMAMI_USERNAME = process.env.UMAMI_USERNAME;
|
||||||
const UMAMI_PASSWORD = process.env.UMAMI_PASSWORD;
|
const UMAMI_PASSWORD = process.env.UMAMI_PASSWORD;
|
||||||
const UMAMI_API_KEY = process.env.UMAMI_API_KEY; // optional if using API key auth
|
const UMAMI_API_KEY = process.env.UMAMI_API_KEY; // optional if using API key auth
|
||||||
|
|
||||||
|
const httpsAgent = new https.Agent({
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
});
|
||||||
|
|
||||||
if (!UMAMI_USERNAME && !UMAMI_API_KEY) {
|
if (!UMAMI_USERNAME && !UMAMI_API_KEY) {
|
||||||
console.error("Warning: Neither UMAMI_USERNAME/PASSWORD nor UMAMI_API_KEY is set.");
|
console.error("Warning: Neither UMAMI_USERNAME/PASSWORD nor UMAMI_API_KEY is set.");
|
||||||
}
|
}
|
||||||
@@ -28,12 +33,13 @@ async function getAuthHeaders(): Promise<Record<string, string>> {
|
|||||||
const res = await axios.post(`${UMAMI_BASE_URL}/api/auth/login`, {
|
const res = await axios.post(`${UMAMI_BASE_URL}/api/auth/login`, {
|
||||||
username: UMAMI_USERNAME,
|
username: UMAMI_USERNAME,
|
||||||
password: UMAMI_PASSWORD,
|
password: UMAMI_PASSWORD,
|
||||||
});
|
}, { httpsAgent });
|
||||||
cachedToken = res.data.token;
|
cachedToken = res.data.token;
|
||||||
}
|
}
|
||||||
return { Authorization: `Bearer ${cachedToken}` };
|
return { Authorization: `Bearer ${cachedToken}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Tool Definitions ---
|
// --- Tool Definitions ---
|
||||||
const LIST_WEBSITES_TOOL: Tool = {
|
const LIST_WEBSITES_TOOL: Tool = {
|
||||||
name: "umami_list_websites",
|
name: "umami_list_websites",
|
||||||
@@ -147,7 +153,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|||||||
|
|
||||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
const headers = await getAuthHeaders();
|
const headers = await getAuthHeaders();
|
||||||
const api = axios.create({ baseURL: `${UMAMI_BASE_URL}/api`, headers });
|
const api = axios.create({ baseURL: `${UMAMI_BASE_URL}/api`, headers, httpsAgent });
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
@@ -236,24 +242,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
|
|
||||||
// --- Express / SSE Server ---
|
// --- Express / SSE Server ---
|
||||||
async function run() {
|
async function run() {
|
||||||
const app = express();
|
const isStdio = process.argv.includes('--stdio');
|
||||||
let transport: SSEServerTransport | null = null;
|
|
||||||
|
|
||||||
app.get('/sse', async (req, res) => {
|
if (isStdio) {
|
||||||
console.error('New SSE connection established');
|
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
||||||
transport = new SSEServerTransport('/message', res);
|
const transport = new StdioServerTransport();
|
||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
});
|
console.error('Umami MCP server is running on stdio');
|
||||||
|
} else {
|
||||||
|
const app = express();
|
||||||
|
let transport: SSEServerTransport | null = null;
|
||||||
|
|
||||||
app.post('/message', async (req, res) => {
|
app.get('/sse', async (req, res) => {
|
||||||
if (!transport) { res.status(400).send('No active SSE connection'); return; }
|
console.error('New SSE connection established');
|
||||||
await transport.handlePostMessage(req, res);
|
transport = new SSEServerTransport('/message', res);
|
||||||
});
|
await server.connect(transport);
|
||||||
|
});
|
||||||
|
|
||||||
const PORT = process.env.UMAMI_MCP_PORT || 3003;
|
app.post('/message', async (req, res) => {
|
||||||
app.listen(PORT, () => {
|
if (!transport) {
|
||||||
console.error(`Umami MCP server running on http://localhost:${PORT}/sse`);
|
res.status(400).send('No active SSE connection');
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
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`);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
run().catch((err) => {
|
run().catch((err) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user