diff --git a/package.json b/package.json index 0a6a1a2..16b296a 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "dev": "tsx src/index.ts", "test": "vitest run", "test:watch": "vitest", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "start:http": "MCP_TRANSPORT=http node dist/index.js" }, "keywords": [], "author": "", diff --git a/src/index.ts b/src/index.ts index aeb0eb1..75fd773 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,82 @@ +import { createServer as createHttpServer } from 'node:http'; +import type { IncomingMessage, ServerResponse } from 'node:http'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { loadConfig } from './config/index.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { loadConfig, getTransportConfig } from './config/index.js'; import { createServer } from './server.js'; +async function readRequestBody(req: IncomingMessage): Promise { + const chunks: Buffer[] = []; + for await (const chunk of req) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + const raw = Buffer.concat(chunks).toString('utf-8'); + if (!raw) return undefined; + return JSON.parse(raw); +} + async function main(): Promise { + const rawTransport = process.env['MCP_TRANSPORT']; + if (rawTransport && rawTransport !== 'stdio' && rawTransport !== 'http') { + console.error( + `FATAL: Invalid MCP_TRANSPORT '${rawTransport}': must be 'stdio' or 'http'`, + ); + process.exit(1); + } + const config = loadConfig(); - const server = createServer(config); - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error('Sage X3 MCP server started (stdio transport)'); + const { transport: transportType, httpPort } = getTransportConfig(); + + if (transportType === 'stdio') { + const server = createServer(config); + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error('Sage X3 MCP server started (stdio transport)'); + } else { + const httpServer = createHttpServer( + async (req: IncomingMessage, res: ServerResponse) => { + if (req.url !== '/mcp') { + res.writeHead(404); + res.end('Not found'); + return; + } + + try { + const body = + req.method === 'POST' ? await readRequestBody(req) : undefined; + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + const server = createServer(config); + await server.connect(transport); + await transport.handleRequest(req, res, body); + } catch (error) { + if (!res.headersSent) { + res.writeHead(500); + res.end('Internal server error'); + } + console.error( + 'Request error:', + error instanceof Error ? error.message : String(error), + ); + } + }, + ); + + httpServer.listen(httpPort, () => { + console.error( + `Sage X3 MCP server started (HTTP transport on port ${httpPort})`, + ); + }); + + const shutdown = (): void => { + console.error('Shutting down...'); + httpServer.close(); + process.exit(0); + }; + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); + } } main().catch((error: unknown) => { diff --git a/src/server.ts b/src/server.ts index 520110d..6f315ae 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,6 +3,14 @@ import type { SageConfig } from './types/index.js'; import { RestClient } from './clients/rest-client.js'; import { SoapClient } from './clients/soap-client.js'; import { registerHealthTool } from './tools/sage-health.js'; +import { registerQueryTool } from './tools/sage-query.js'; +import { registerReadTool } from './tools/sage-read.js'; +import { registerSearchTool } from './tools/sage-search.js'; +import { registerListEntitiesTool } from './tools/sage-list-entities.js'; +import { registerGetContextTool } from './tools/sage-get-context.js'; +import { registerSoapReadTool } from './tools/sage-soap-read.js'; +import { registerSoapQueryTool } from './tools/sage-soap-query.js'; +import { registerDescribeEntityTool } from './tools/sage-describe-entity.js'; export function createServer(config: SageConfig): McpServer { const server = new McpServer( @@ -14,6 +22,14 @@ export function createServer(config: SageConfig): McpServer { const soapClient = new SoapClient(config); registerHealthTool(server, restClient, soapClient, config); + registerQueryTool(server, restClient); + registerReadTool(server, restClient); + registerSearchTool(server, restClient); + registerListEntitiesTool(server, restClient); + registerGetContextTool(server, restClient); + registerSoapReadTool(server, soapClient); + registerSoapQueryTool(server, soapClient); + registerDescribeEntityTool(server, soapClient); return server; } diff --git a/src/tools/index.ts b/src/tools/index.ts new file mode 100644 index 0000000..19e2ce7 --- /dev/null +++ b/src/tools/index.ts @@ -0,0 +1,9 @@ +export { registerHealthTool } from './sage-health.js'; +export { registerQueryTool } from './sage-query.js'; +export { registerReadTool } from './sage-read.js'; +export { registerSearchTool } from './sage-search.js'; +export { registerListEntitiesTool } from './sage-list-entities.js'; +export { registerGetContextTool } from './sage-get-context.js'; +export { registerSoapReadTool } from './sage-soap-read.js'; +export { registerSoapQueryTool } from './sage-soap-query.js'; +export { registerDescribeEntityTool } from './sage-describe-entity.js'; diff --git a/vitest.config.ts b/vitest.config.ts index d374436..2a6530a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,5 +5,6 @@ export default defineConfig({ globals: true, environment: 'node', passWithNoTests: true, + exclude: ['**/node_modules/**', '**/dist/**'], }, });