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>
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
#!/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);
|
||||
});
|
||||
Reference in New Issue
Block a user