feat: v1.3.1 - Multi-transport + Production deployment
- Add HTTP transport (StreamableHTTPServerTransport) - Add shared server module (src/server/) - Configure production for hub.descomplicar.pt - Add SSH tunnel script (start-tunnel.sh) - Fix connection leak in pg-client.ts - Fix atomicity bug in comments deletion - Update docs with test plan for 164 tools Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
183
src/index-http.ts
Normal file
183
src/index-http.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* MCP Outline PostgreSQL - HTTP Server Mode
|
||||
* StreamableHTTP transport for web/remote access
|
||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||
*/
|
||||
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import * as http from 'http';
|
||||
import { URL } from 'url';
|
||||
import * as dotenv from 'dotenv';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
import { PgClient } from './pg-client.js';
|
||||
import { getDatabaseConfig } from './config/database.js';
|
||||
import { createMcpServer, allTools, getToolCounts } from './server/index.js';
|
||||
import { logger } from './utils/logger.js';
|
||||
import { startRateLimitCleanup, stopRateLimitCleanup } from './utils/security.js';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const PORT = parseInt(process.env.MCP_HTTP_PORT || '3200', 10);
|
||||
const HOST = process.env.MCP_HTTP_HOST || '127.0.0.1';
|
||||
const STATEFUL = process.env.MCP_STATEFUL !== 'false';
|
||||
|
||||
// Track active sessions (stateful mode)
|
||||
const sessions = new Map<string, { transport: StreamableHTTPServerTransport }>();
|
||||
|
||||
async function main() {
|
||||
// Get database configuration
|
||||
const config = getDatabaseConfig();
|
||||
|
||||
// Initialize PostgreSQL client
|
||||
const pgClient = new PgClient(config);
|
||||
|
||||
// Test database connection
|
||||
const isConnected = await pgClient.testConnection();
|
||||
if (!isConnected) {
|
||||
throw new Error('Failed to connect to PostgreSQL database');
|
||||
}
|
||||
|
||||
// Validate all tools have required properties
|
||||
const invalidTools = allTools.filter((tool) => !tool.name || !tool.handler);
|
||||
if (invalidTools.length > 0) {
|
||||
logger.error(`${invalidTools.length} invalid tools found`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create HTTP server
|
||||
const httpServer = http.createServer(async (req, res) => {
|
||||
// CORS headers for local access
|
||||
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');
|
||||
|
||||
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') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
status: 'ok',
|
||||
transport: 'streamable-http',
|
||||
version: '1.3.1',
|
||||
sessions: sessions.size,
|
||||
stateful: STATEFUL,
|
||||
tools: allTools.length
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Tool stats endpoint
|
||||
if (url.pathname === '/stats') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
totalTools: allTools.length,
|
||||
toolsByModule: getToolCounts(),
|
||||
activeSessions: sessions.size
|
||||
}, null, 2)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// MCP endpoint
|
||||
if (url.pathname === '/mcp') {
|
||||
try {
|
||||
// Create transport for this request
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: STATEFUL ? () => randomUUID() : undefined
|
||||
});
|
||||
|
||||
// Create MCP server
|
||||
const server = createMcpServer(pgClient.getPool(), {
|
||||
name: 'mcp-outline-http',
|
||||
version: '1.3.1'
|
||||
});
|
||||
|
||||
// Track session if stateful
|
||||
if (STATEFUL && transport.sessionId) {
|
||||
sessions.set(transport.sessionId, { transport });
|
||||
transport.onclose = () => {
|
||||
if (transport.sessionId) {
|
||||
sessions.delete(transport.sessionId);
|
||||
logger.debug(`Session closed: ${transport.sessionId}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Connect server to transport
|
||||
await server.connect(transport);
|
||||
|
||||
// Handle the request
|
||||
await transport.handleRequest(req, res);
|
||||
} catch (error) {
|
||||
logger.error('Error handling MCP request:', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Internal server error' }));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 404 for other paths
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Not found' }));
|
||||
});
|
||||
|
||||
// Start background tasks
|
||||
startRateLimitCleanup();
|
||||
|
||||
// Start HTTP server
|
||||
httpServer.listen(PORT, HOST, () => {
|
||||
logger.info('MCP Outline HTTP Server started', {
|
||||
host: HOST,
|
||||
port: PORT,
|
||||
stateful: STATEFUL,
|
||||
tools: allTools.length,
|
||||
endpoint: `http://${HOST}:${PORT}/mcp`
|
||||
});
|
||||
|
||||
// Console output for visibility
|
||||
console.log(`MCP Outline PostgreSQL HTTP Server v1.3.1`);
|
||||
console.log(` Endpoint: http://${HOST}:${PORT}/mcp`);
|
||||
console.log(` Health: http://${HOST}:${PORT}/health`);
|
||||
console.log(` Stats: http://${HOST}:${PORT}/stats`);
|
||||
console.log(` Mode: ${STATEFUL ? 'Stateful' : 'Stateless'}`);
|
||||
console.log(` Tools: ${allTools.length}`);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
const shutdown = async () => {
|
||||
logger.info('Shutting down HTTP server...');
|
||||
stopRateLimitCleanup();
|
||||
httpServer.close(() => {
|
||||
logger.info('HTTP server closed');
|
||||
});
|
||||
await pgClient.close();
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
235
src/index.ts
235
src/index.ts
@@ -1,127 +1,21 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* MCP Outline PostgreSQL - Main Server
|
||||
* MCP Outline PostgreSQL - Stdio Server
|
||||
* Standard stdio transport for CLI/local access
|
||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||
*/
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
ListToolsRequestSchema,
|
||||
CallToolRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ListPromptsRequestSchema
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
import { PgClient } from './pg-client.js';
|
||||
import { getDatabaseConfig } from './config/database.js';
|
||||
import { createMcpServer, allTools, getToolCounts } from './server/index.js';
|
||||
import { logger } from './utils/logger.js';
|
||||
import { checkRateLimit, startRateLimitCleanup, stopRateLimitCleanup } from './utils/security.js';
|
||||
import { BaseTool } from './types/tools.js';
|
||||
|
||||
// Import ALL tools
|
||||
import {
|
||||
documentsTools,
|
||||
collectionsTools,
|
||||
usersTools,
|
||||
groupsTools,
|
||||
commentsTools,
|
||||
sharesTools,
|
||||
revisionsTools,
|
||||
eventsTools,
|
||||
attachmentsTools,
|
||||
fileOperationsTools,
|
||||
oauthTools,
|
||||
authTools,
|
||||
starsTools,
|
||||
pinsTools,
|
||||
viewsTools,
|
||||
reactionsTools,
|
||||
apiKeysTools,
|
||||
webhooksTools,
|
||||
backlinksTools,
|
||||
searchQueriesTools,
|
||||
// New modules
|
||||
teamsTools,
|
||||
integrationsTools,
|
||||
notificationsTools,
|
||||
subscriptionsTools,
|
||||
templatesTools,
|
||||
importsTools,
|
||||
emojisTools,
|
||||
userPermissionsTools,
|
||||
bulkOperationsTools,
|
||||
advancedSearchTools,
|
||||
analyticsTools,
|
||||
exportImportTools,
|
||||
deskSyncTools
|
||||
} from './tools/index.js';
|
||||
import { startRateLimitCleanup, stopRateLimitCleanup } from './utils/security.js';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// Combine ALL tools into single array
|
||||
const allTools: BaseTool[] = [
|
||||
// Core functionality
|
||||
...documentsTools,
|
||||
...collectionsTools,
|
||||
...usersTools,
|
||||
...groupsTools,
|
||||
|
||||
// Collaboration
|
||||
...commentsTools,
|
||||
...sharesTools,
|
||||
...revisionsTools,
|
||||
|
||||
// System
|
||||
...eventsTools,
|
||||
...attachmentsTools,
|
||||
...fileOperationsTools,
|
||||
|
||||
// Authentication
|
||||
...oauthTools,
|
||||
...authTools,
|
||||
|
||||
// User engagement
|
||||
...starsTools,
|
||||
...pinsTools,
|
||||
...viewsTools,
|
||||
...reactionsTools,
|
||||
|
||||
// API & Integration
|
||||
...apiKeysTools,
|
||||
...webhooksTools,
|
||||
...integrationsTools,
|
||||
|
||||
// Analytics & Search
|
||||
...backlinksTools,
|
||||
...searchQueriesTools,
|
||||
...advancedSearchTools,
|
||||
...analyticsTools,
|
||||
|
||||
// Teams & Workspace
|
||||
...teamsTools,
|
||||
|
||||
// Notifications & Subscriptions
|
||||
...notificationsTools,
|
||||
...subscriptionsTools,
|
||||
|
||||
// Templates & Imports
|
||||
...templatesTools,
|
||||
...importsTools,
|
||||
|
||||
// Custom content
|
||||
...emojisTools,
|
||||
|
||||
// Permissions & Bulk operations
|
||||
...userPermissionsTools,
|
||||
...bulkOperationsTools,
|
||||
|
||||
// Export/Import & External Sync
|
||||
...exportImportTools,
|
||||
...deskSyncTools
|
||||
];
|
||||
|
||||
// Validate all tools have required properties
|
||||
const invalidTools = allTools.filter((tool) => !tool.name || !tool.handler);
|
||||
if (invalidTools.length > 0) {
|
||||
@@ -142,90 +36,16 @@ async function main() {
|
||||
throw new Error('Failed to connect to PostgreSQL database');
|
||||
}
|
||||
|
||||
// Initialize MCP server
|
||||
const server = new Server({
|
||||
name: 'mcp-outline',
|
||||
version: '1.0.0'
|
||||
// Create MCP server with shared configuration
|
||||
const server = createMcpServer(pgClient.getPool(), {
|
||||
name: 'mcp-outline-postgresql',
|
||||
version: '1.3.1'
|
||||
});
|
||||
|
||||
// Set capabilities (required for MCP v2.2+)
|
||||
(server as any)._capabilities = {
|
||||
tools: {},
|
||||
resources: {},
|
||||
prompts: {}
|
||||
};
|
||||
|
||||
// Connect transport BEFORE registering handlers
|
||||
// Connect stdio transport
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
|
||||
// Register tools list handler
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: allTools.map((tool) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema
|
||||
}))
|
||||
}));
|
||||
|
||||
// Register resources handler (required even if empty)
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||
logger.debug('Resources list requested');
|
||||
return { resources: [] };
|
||||
});
|
||||
|
||||
// Register prompts handler (required even if empty)
|
||||
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
||||
logger.debug('Prompts list requested');
|
||||
return { prompts: [] };
|
||||
});
|
||||
|
||||
// Register tool call handler
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
// Rate limiting (using 'default' as clientId for now)
|
||||
const clientId = process.env.CLIENT_ID || 'default';
|
||||
if (!checkRateLimit('api', clientId)) {
|
||||
return {
|
||||
content: [
|
||||
{ type: 'text', text: 'Too Many Requests: rate limit exceeded. Try again later.' }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Find the tool handler
|
||||
const tool = allTools.find((t) => t.name === name);
|
||||
|
||||
if (!tool) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Tool '${name}' not found`
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Pass the pool directly to tool handlers
|
||||
return await tool.handler(args as Record<string, unknown>, pgClient.getPool());
|
||||
} catch (error) {
|
||||
logger.error(`Error in tool ${name}:`, {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error in tool ${name}: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Start background tasks
|
||||
startRateLimitCleanup();
|
||||
|
||||
@@ -246,42 +66,9 @@ async function main() {
|
||||
|
||||
// Debug logging
|
||||
logger.debug('MCP Outline PostgreSQL Server running', {
|
||||
transport: 'stdio',
|
||||
totalTools: allTools.length,
|
||||
toolsByModule: {
|
||||
documents: documentsTools.length,
|
||||
collections: collectionsTools.length,
|
||||
users: usersTools.length,
|
||||
groups: groupsTools.length,
|
||||
comments: commentsTools.length,
|
||||
shares: sharesTools.length,
|
||||
revisions: revisionsTools.length,
|
||||
events: eventsTools.length,
|
||||
attachments: attachmentsTools.length,
|
||||
fileOperations: fileOperationsTools.length,
|
||||
oauth: oauthTools.length,
|
||||
auth: authTools.length,
|
||||
stars: starsTools.length,
|
||||
pins: pinsTools.length,
|
||||
views: viewsTools.length,
|
||||
reactions: reactionsTools.length,
|
||||
apiKeys: apiKeysTools.length,
|
||||
webhooks: webhooksTools.length,
|
||||
backlinks: backlinksTools.length,
|
||||
searchQueries: searchQueriesTools.length,
|
||||
teams: teamsTools.length,
|
||||
integrations: integrationsTools.length,
|
||||
notifications: notificationsTools.length,
|
||||
subscriptions: subscriptionsTools.length,
|
||||
templates: templatesTools.length,
|
||||
imports: importsTools.length,
|
||||
emojis: emojisTools.length,
|
||||
userPermissions: userPermissionsTools.length,
|
||||
bulkOperations: bulkOperationsTools.length,
|
||||
advancedSearch: advancedSearchTools.length,
|
||||
analytics: analyticsTools.length,
|
||||
exportImport: exportImportTools.length,
|
||||
deskSync: deskSyncTools.length
|
||||
}
|
||||
toolsByModule: getToolCounts()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -14,22 +14,22 @@ export class PgClient {
|
||||
constructor(config: DatabaseConfig) {
|
||||
const poolConfig: PoolConfig = config.connectionString
|
||||
? {
|
||||
connectionString: config.connectionString,
|
||||
max: config.max,
|
||||
idleTimeoutMillis: config.idleTimeoutMillis,
|
||||
connectionTimeoutMillis: config.connectionTimeoutMillis
|
||||
}
|
||||
connectionString: config.connectionString,
|
||||
max: config.max,
|
||||
idleTimeoutMillis: config.idleTimeoutMillis,
|
||||
connectionTimeoutMillis: config.connectionTimeoutMillis
|
||||
}
|
||||
: {
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
user: config.user,
|
||||
password: config.password,
|
||||
database: config.database,
|
||||
ssl: config.ssl ? { rejectUnauthorized: false } : false,
|
||||
max: config.max,
|
||||
idleTimeoutMillis: config.idleTimeoutMillis,
|
||||
connectionTimeoutMillis: config.connectionTimeoutMillis
|
||||
};
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
user: config.user,
|
||||
password: config.password,
|
||||
database: config.database,
|
||||
ssl: config.ssl ? { rejectUnauthorized: false } : false,
|
||||
max: config.max,
|
||||
idleTimeoutMillis: config.idleTimeoutMillis,
|
||||
connectionTimeoutMillis: config.connectionTimeoutMillis
|
||||
};
|
||||
|
||||
this.pool = new Pool(poolConfig);
|
||||
|
||||
@@ -50,10 +50,10 @@ export class PgClient {
|
||||
* Test database connection
|
||||
*/
|
||||
async testConnection(): Promise<boolean> {
|
||||
let client = null;
|
||||
try {
|
||||
const client = await this.pool.connect();
|
||||
client = await this.pool.connect();
|
||||
await client.query('SELECT 1');
|
||||
client.release();
|
||||
this.isConnected = true;
|
||||
logger.info('PostgreSQL connection successful');
|
||||
return true;
|
||||
@@ -63,6 +63,10 @@ export class PgClient {
|
||||
});
|
||||
this.isConnected = false;
|
||||
return false;
|
||||
} finally {
|
||||
if (client) {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +123,13 @@ export class PgClient {
|
||||
await client.query('COMMIT');
|
||||
return result;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
try {
|
||||
await client.query('ROLLBACK');
|
||||
} catch (rollbackError) {
|
||||
logger.error('Rollback failed', {
|
||||
error: rollbackError instanceof Error ? rollbackError.message : String(rollbackError),
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
|
||||
180
src/server/create-server.ts
Normal file
180
src/server/create-server.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* MCP Outline PostgreSQL - Server Factory
|
||||
* Creates configured MCP server instances for different transports
|
||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||
*/
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { Pool } from 'pg';
|
||||
import { registerHandlers } from './register-handlers.js';
|
||||
import { BaseTool } from '../types/tools.js';
|
||||
|
||||
// Import ALL tools
|
||||
import {
|
||||
documentsTools,
|
||||
collectionsTools,
|
||||
usersTools,
|
||||
groupsTools,
|
||||
commentsTools,
|
||||
sharesTools,
|
||||
revisionsTools,
|
||||
eventsTools,
|
||||
attachmentsTools,
|
||||
fileOperationsTools,
|
||||
oauthTools,
|
||||
authTools,
|
||||
starsTools,
|
||||
pinsTools,
|
||||
viewsTools,
|
||||
reactionsTools,
|
||||
apiKeysTools,
|
||||
webhooksTools,
|
||||
backlinksTools,
|
||||
searchQueriesTools,
|
||||
teamsTools,
|
||||
integrationsTools,
|
||||
notificationsTools,
|
||||
subscriptionsTools,
|
||||
templatesTools,
|
||||
importsTools,
|
||||
emojisTools,
|
||||
userPermissionsTools,
|
||||
bulkOperationsTools,
|
||||
advancedSearchTools,
|
||||
analyticsTools,
|
||||
exportImportTools,
|
||||
deskSyncTools
|
||||
} from '../tools/index.js';
|
||||
|
||||
export interface ServerConfig {
|
||||
name?: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
// Combine ALL tools into single array
|
||||
export const allTools: BaseTool[] = [
|
||||
// Core functionality
|
||||
...documentsTools,
|
||||
...collectionsTools,
|
||||
...usersTools,
|
||||
...groupsTools,
|
||||
|
||||
// Collaboration
|
||||
...commentsTools,
|
||||
...sharesTools,
|
||||
...revisionsTools,
|
||||
|
||||
// System
|
||||
...eventsTools,
|
||||
...attachmentsTools,
|
||||
...fileOperationsTools,
|
||||
|
||||
// Authentication
|
||||
...oauthTools,
|
||||
...authTools,
|
||||
|
||||
// User engagement
|
||||
...starsTools,
|
||||
...pinsTools,
|
||||
...viewsTools,
|
||||
...reactionsTools,
|
||||
|
||||
// API & Integration
|
||||
...apiKeysTools,
|
||||
...webhooksTools,
|
||||
...integrationsTools,
|
||||
|
||||
// Analytics & Search
|
||||
...backlinksTools,
|
||||
...searchQueriesTools,
|
||||
...advancedSearchTools,
|
||||
...analyticsTools,
|
||||
|
||||
// Teams & Workspace
|
||||
...teamsTools,
|
||||
|
||||
// Notifications & Subscriptions
|
||||
...notificationsTools,
|
||||
...subscriptionsTools,
|
||||
|
||||
// Templates & Imports
|
||||
...templatesTools,
|
||||
...importsTools,
|
||||
|
||||
// Custom content
|
||||
...emojisTools,
|
||||
|
||||
// Permissions & Bulk operations
|
||||
...userPermissionsTools,
|
||||
...bulkOperationsTools,
|
||||
|
||||
// Export/Import & External Sync
|
||||
...exportImportTools,
|
||||
...deskSyncTools
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a configured MCP server instance
|
||||
*/
|
||||
export function createMcpServer(
|
||||
pgPool: Pool,
|
||||
config: ServerConfig = {}
|
||||
): Server {
|
||||
const server = new Server({
|
||||
name: config.name || 'mcp-outline-postgresql',
|
||||
version: config.version || '1.3.1'
|
||||
});
|
||||
|
||||
// Set capabilities (required for MCP v2.2+)
|
||||
(server as any)._capabilities = {
|
||||
tools: {},
|
||||
resources: {},
|
||||
prompts: {}
|
||||
};
|
||||
|
||||
// Register all handlers
|
||||
registerHandlers(server, pgPool, allTools);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tool counts by module for debugging
|
||||
*/
|
||||
export function getToolCounts(): Record<string, number> {
|
||||
return {
|
||||
documents: documentsTools.length,
|
||||
collections: collectionsTools.length,
|
||||
users: usersTools.length,
|
||||
groups: groupsTools.length,
|
||||
comments: commentsTools.length,
|
||||
shares: sharesTools.length,
|
||||
revisions: revisionsTools.length,
|
||||
events: eventsTools.length,
|
||||
attachments: attachmentsTools.length,
|
||||
fileOperations: fileOperationsTools.length,
|
||||
oauth: oauthTools.length,
|
||||
auth: authTools.length,
|
||||
stars: starsTools.length,
|
||||
pins: pinsTools.length,
|
||||
views: viewsTools.length,
|
||||
reactions: reactionsTools.length,
|
||||
apiKeys: apiKeysTools.length,
|
||||
webhooks: webhooksTools.length,
|
||||
backlinks: backlinksTools.length,
|
||||
searchQueries: searchQueriesTools.length,
|
||||
teams: teamsTools.length,
|
||||
integrations: integrationsTools.length,
|
||||
notifications: notificationsTools.length,
|
||||
subscriptions: subscriptionsTools.length,
|
||||
templates: templatesTools.length,
|
||||
imports: importsTools.length,
|
||||
emojis: emojisTools.length,
|
||||
userPermissions: userPermissionsTools.length,
|
||||
bulkOperations: bulkOperationsTools.length,
|
||||
advancedSearch: advancedSearchTools.length,
|
||||
analytics: analyticsTools.length,
|
||||
exportImport: exportImportTools.length,
|
||||
deskSync: deskSyncTools.length
|
||||
};
|
||||
}
|
||||
7
src/server/index.ts
Normal file
7
src/server/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* MCP Outline PostgreSQL - Server Module
|
||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||
*/
|
||||
|
||||
export { createMcpServer, allTools, getToolCounts, type ServerConfig } from './create-server.js';
|
||||
export { registerHandlers } from './register-handlers.js';
|
||||
92
src/server/register-handlers.ts
Normal file
92
src/server/register-handlers.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* MCP Outline PostgreSQL - Register Handlers
|
||||
* Shared handler registration for all transport types
|
||||
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
||||
*/
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import {
|
||||
ListToolsRequestSchema,
|
||||
CallToolRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ListPromptsRequestSchema
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { Pool } from 'pg';
|
||||
import { BaseTool } from '../types/tools.js';
|
||||
import { checkRateLimit } from '../utils/security.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
/**
|
||||
* Register all MCP handlers on a server instance
|
||||
*/
|
||||
export function registerHandlers(
|
||||
server: Server,
|
||||
pgPool: Pool,
|
||||
tools: BaseTool[]
|
||||
): void {
|
||||
// Register tools list handler
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: tools.map((tool) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema
|
||||
}))
|
||||
}));
|
||||
|
||||
// Register resources handler (required even if empty)
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||
logger.debug('Resources list requested');
|
||||
return { resources: [] };
|
||||
});
|
||||
|
||||
// Register prompts handler (required even if empty)
|
||||
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
||||
logger.debug('Prompts list requested');
|
||||
return { prompts: [] };
|
||||
});
|
||||
|
||||
// Register tool call handler
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
// Rate limiting (using 'default' as clientId for now)
|
||||
const clientId = process.env.CLIENT_ID || 'default';
|
||||
if (!checkRateLimit('api', clientId)) {
|
||||
return {
|
||||
content: [
|
||||
{ type: 'text', text: 'Too Many Requests: rate limit exceeded. Try again later.' }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// Find the tool handler
|
||||
const tool = tools.find((t) => t.name === name);
|
||||
|
||||
if (!tool) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Tool '${name}' not found`
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return await tool.handler(args as Record<string, unknown>, pgPool);
|
||||
} catch (error) {
|
||||
logger.error(`Error in tool ${name}:`, {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error in tool ${name}: ${error instanceof Error ? error.message : String(error)}`
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -375,15 +375,23 @@ const deleteComment: BaseTool<GetCommentArgs> = {
|
||||
throw new Error('Invalid comment ID format');
|
||||
}
|
||||
|
||||
// Delete replies first
|
||||
await pgClient.query('DELETE FROM comments WHERE "parentCommentId" = $1', [args.id]);
|
||||
// Import transaction helper
|
||||
const { withTransactionNoRetry } = await import('../utils/transaction.js');
|
||||
|
||||
// Delete the comment
|
||||
const result = await pgClient.query('DELETE FROM comments WHERE id = $1 RETURNING id', [args.id]);
|
||||
// Use transaction to ensure atomicity
|
||||
const result = await withTransactionNoRetry(pgClient, async (client) => {
|
||||
// Delete replies first
|
||||
await client.query('DELETE FROM comments WHERE "parentCommentId" = $1', [args.id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
throw new Error('Comment not found');
|
||||
}
|
||||
// Delete the comment
|
||||
const deleteResult = await client.query('DELETE FROM comments WHERE id = $1 RETURNING id', [args.id]);
|
||||
|
||||
if (deleteResult.rows.length === 0) {
|
||||
throw new Error('Comment not found');
|
||||
}
|
||||
|
||||
return deleteResult.rows[0];
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
@@ -393,7 +401,7 @@ const deleteComment: BaseTool<GetCommentArgs> = {
|
||||
{
|
||||
success: true,
|
||||
message: 'Comment deleted successfully',
|
||||
id: result.rows[0].id,
|
||||
id: result.id,
|
||||
},
|
||||
null,
|
||||
2
|
||||
|
||||
Reference in New Issue
Block a user