Files
mcp-paperclip/src/index-http.ts
T
ealmeida 2753360787 feat: mcp-paperclip v1.0.0 — 165 tools para Paperclip AI
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>
2026-04-07 02:56:45 +01:00

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