2753360787
Triple transport (STDIO + StreamableHTTP + SSE porta 3175). 24 modulos: agents, issues, approvals, routines, goals, projects, costs, activity, skills, secrets, plugins, assets, settings, access. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
201 lines
6.2 KiB
JavaScript
201 lines
6.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* MCP Paperclip - HTTP Server Mode (Streamable HTTP + SSE)
|
|
* Dual transport: StreamableHTTP (primary) + SSE (legacy compatibility)
|
|
*
|
|
* @author Descomplicar | @link descomplicar.pt | @copyright 2026
|
|
*/
|
|
|
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
import * as dotenv from 'dotenv';
|
|
import * as http from 'http';
|
|
import { randomUUID } from 'crypto';
|
|
|
|
import { createServer } from './server.js';
|
|
import { allTools } from './tools/index.js';
|
|
import { logger } from './utils/logger.js';
|
|
|
|
dotenv.config();
|
|
|
|
const PORT = parseInt(process.env.HTTP_PORT || '3175');
|
|
const HOST = process.env.HTTP_HOST || '127.0.0.1';
|
|
|
|
// Track active StreamableHTTP sessions
|
|
const sessions = new Map<string, { transport: StreamableHTTPServerTransport }>();
|
|
|
|
// Track SSE sessions
|
|
const sseSessions = new Map<string, SSEServerTransport>();
|
|
|
|
async function main() {
|
|
const httpServer = http.createServer(async (req, res) => {
|
|
// CORS headers on all responses
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id');
|
|
res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id');
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
res.writeHead(200);
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
const url = new URL(req.url || '/', `http://${HOST}:${PORT}`);
|
|
|
|
// Health check endpoint
|
|
if (url.pathname === '/health' && req.method === 'GET') {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(
|
|
JSON.stringify({
|
|
status: 'ok',
|
|
transport: 'http+sse',
|
|
sessions: sessions.size,
|
|
sse_sessions: sseSessions.size,
|
|
tools: allTools.length,
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
// StreamableHTTP endpoint — POST/DELETE
|
|
if (url.pathname === '/mcp') {
|
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
|
|
// Route to existing session
|
|
if (sessionId && sessions.has(sessionId)) {
|
|
const session = sessions.get(sessionId)!;
|
|
await session.transport.handleRequest(req, res);
|
|
return;
|
|
}
|
|
|
|
// DELETE — session termination
|
|
if (req.method === 'DELETE' && sessionId) {
|
|
const session = sessions.get(sessionId);
|
|
if (session) {
|
|
await session.transport.close();
|
|
sessions.delete(sessionId);
|
|
res.writeHead(200);
|
|
res.end();
|
|
} else {
|
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Session not found' }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// POST — new session
|
|
if (req.method === 'POST') {
|
|
const server = createServer(allTools);
|
|
|
|
const transport = new StreamableHTTPServerTransport({
|
|
sessionIdGenerator: () => randomUUID(),
|
|
enableJsonResponse: true,
|
|
onsessioninitialized: (newSessionId) => {
|
|
sessions.set(newSessionId, { transport });
|
|
logger.info(`HTTP session initialized: ${newSessionId}`);
|
|
},
|
|
});
|
|
|
|
transport.onclose = () => {
|
|
if (transport.sessionId) {
|
|
logger.info(`HTTP session closed: ${transport.sessionId}`);
|
|
sessions.delete(transport.sessionId);
|
|
}
|
|
};
|
|
|
|
await server.connect(transport);
|
|
await transport.handleRequest(req, res);
|
|
return;
|
|
}
|
|
|
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Method not allowed' }));
|
|
return;
|
|
}
|
|
|
|
// SSE endpoint — GET (legacy transport)
|
|
if (url.pathname === '/sse' && req.method === 'GET') {
|
|
const server = createServer(allTools);
|
|
const transport = new SSEServerTransport('/message', res);
|
|
|
|
sseSessions.set(transport.sessionId, transport);
|
|
logger.info(`SSE session started: ${transport.sessionId}`);
|
|
|
|
transport.onclose = () => {
|
|
logger.info(`SSE session closed: ${transport.sessionId}`);
|
|
sseSessions.delete(transport.sessionId);
|
|
};
|
|
|
|
await server.connect(transport);
|
|
return;
|
|
}
|
|
|
|
// SSE message endpoint — POST
|
|
if (url.pathname === '/message' && req.method === 'POST') {
|
|
const sessionId = url.searchParams.get('sessionId');
|
|
if (!sessionId) {
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Missing sessionId query param' }));
|
|
return;
|
|
}
|
|
|
|
const transport = sseSessions.get(sessionId);
|
|
if (!transport) {
|
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'SSE session not found' }));
|
|
return;
|
|
}
|
|
|
|
await transport.handlePostMessage(req, res);
|
|
return;
|
|
}
|
|
|
|
// 404 for all other paths
|
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Not found', hint: 'Use /mcp (StreamableHTTP) or /sse (SSE)' }));
|
|
});
|
|
|
|
httpServer.listen(PORT, HOST, () => {
|
|
logger.info('MCP Paperclip HTTP Server started', {
|
|
host: HOST,
|
|
port: PORT,
|
|
tools: allTools.length,
|
|
endpoints: {
|
|
health: `http://${HOST}:${PORT}/health`,
|
|
mcp: `http://${HOST}:${PORT}/mcp`,
|
|
sse: `http://${HOST}:${PORT}/sse`,
|
|
},
|
|
});
|
|
console.log(`MCP Paperclip HTTP Server running at http://${HOST}:${PORT}/mcp`);
|
|
});
|
|
|
|
// Graceful shutdown
|
|
const shutdown = (signal: string) => {
|
|
logger.info(`${signal} received, shutting down...`);
|
|
for (const session of sessions.values()) {
|
|
session.transport.close();
|
|
}
|
|
sessions.clear();
|
|
for (const transport of sseSessions.values()) {
|
|
transport.close();
|
|
}
|
|
sseSessions.clear();
|
|
httpServer.close(() => {
|
|
logger.info('HTTP server closed');
|
|
process.exit(0);
|
|
});
|
|
};
|
|
|
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
}
|
|
|
|
main().catch((error) => {
|
|
logger.error('Fatal error', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
process.exit(1);
|
|
});
|