@imenam/mcp-http-gateway 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # MCP HTTP Gateway
2
+
3
+ Serveur HTTP qui expose n'importe quel serveur MCP (Model Context Protocol) en tant qu'API REST, sans modifier les MCPs eux-mêmes. Conçu pour permettre à des agents ou applications ne supportant pas nativement le protocole MCP de communiquer avec l'écosystème MCP via de simples requêtes HTTP.
4
+
5
+ ## Fonctionnement
6
+
7
+ Chaque MCP tourne en tant que processus enfant (transport stdio). Le gateway s'y connecte via le SDK MCP officiel et expose ses outils sur des routes HTTP configurables.
8
+
9
+ ```
10
+ Agent / App externe
11
+ │
12
+ │ HTTP
13
+ ▼
14
+ MCP HTTP Gateway (port 3500)
15
+ │
16
+ │ stdio (SDK MCP)
17
+ ├──▶ mcp-documentor (/documentor)
18
+ ├──▶ mcp-raindrop (/raindrop)
19
+ └──▶ ...
20
+ ```
21
+
22
+ ## Installation
23
+
24
+ **En tant que CLI globale :**
25
+
26
+ ```bash
27
+ npm install -g @imenam/mcp-http-gateway
28
+ ```
29
+
30
+ **Via npx (sans installation) :**
31
+
32
+ ```bash
33
+ npx @imenam/mcp-http-gateway
34
+ ```
35
+
36
+ **En dépendance locale d'un projet :**
37
+
38
+ ```bash
39
+ npm install @imenam/mcp-http-gateway
40
+ ```
41
+
42
+ ## Configuration
43
+
44
+ ### `.env`
45
+
46
+ ```env
47
+ GATEWAY_PORT=3500
48
+ ```
49
+
50
+ ### `config/mcp-servers.json`
51
+
52
+ À créer dans le **répertoire courant** depuis lequel tu lances le gateway (pas dans le package lui-même). Un fichier `config/mcp-servers.example.json` est disponible comme point de départ.
53
+
54
+ Même format que la configuration `mcpServers` de Cursor. Ajouter `MCP_SERVER_ROUTE` dans `env` pour définir la route HTTP du MCP, et `description` pour la documentation.
55
+
56
+ ```json
57
+ {
58
+ "mcpServers": {
59
+ "mon-mcp": {
60
+ "command": "node",
61
+ "args": ["/chemin/vers/dist/index.js"],
62
+ "description": "Description courte du MCP",
63
+ "env": {
64
+ "MA_VARIABLE": "valeur",
65
+ "MCP_SERVER_ROUTE": "/ma-route"
66
+ }
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ > Un MCP sans `MCP_SERVER_ROUTE` est ignoré au démarrage.
73
+
74
+ ## Démarrage
75
+
76
+ ```bash
77
+ npm start
78
+ ```
79
+
80
+ Le gateway connecte chaque MCP configuré et démarre le serveur HTTP. Les MCPs en échec au démarrage sont ignorés sans bloquer les autres.
81
+
82
+ ## Routes
83
+
84
+ ### `GET /mcps`
85
+
86
+ Retourne la liste des MCPs connectés avec leur route, description et instructions d'utilisation (format Markdown).
87
+
88
+ ```bash
89
+ curl http://localhost:3500/mcps
90
+ ```
91
+
92
+ ---
93
+
94
+ ### `GET /{route}/tools`
95
+
96
+ Liste les outils disponibles d'un MCP avec leur nom, description et schéma d'arguments.
97
+
98
+ ```bash
99
+ curl http://localhost:3500/documentor/tools
100
+ ```
101
+
102
+ ---
103
+
104
+ ### `POST /{route}/tools/:toolName`
105
+
106
+ Appelle un outil du MCP. Les arguments sont passés dans le body JSON.
107
+
108
+ ```bash
109
+ curl -X POST http://localhost:3500/documentor/tools/list_projects -H "Content-Type: application/json" -d '{}'
110
+ ```
111
+
112
+ ```bash
113
+ curl -X POST http://localhost:3500/documentor/tools/get_doc -H "Content-Type: application/json" -d '{"id":"mon-doc-id"}'
114
+ ```
115
+
116
+ #### Réponse
117
+
118
+ La réponse est le format brut du protocole MCP — un tableau `content` de blocs typés :
119
+
120
+ ```json
121
+ {
122
+ "content": [
123
+ { "type": "text", "text": "Résultat de l'outil..." }
124
+ ]
125
+ }
126
+ ```
127
+
128
+ Pour extraire le texte directement avec `jq` :
129
+
130
+ ```bash
131
+ curl -s -X POST http://localhost:3500/documentor/tools/list_projects -H "Content-Type: application/json" -d '{}' | jq -r '.content[0].text'
132
+ ```
133
+
134
+ ## Mode MCP
135
+
136
+ Le gateway peut également démarrer en tant que **serveur MCP** (transport stdio). Dans ce mode, il se connecte à une instance du gateway HTTP et expose ses outils comme des outils MCP natifs.
137
+
138
+ ### Cas d'usage : agent dans un conteneur Docker
139
+
140
+ Le gateway HTTP tourne **hors du conteneur**, avec accès aux MCPs et leurs clés sensibles. L'agent dans le conteneur reçoit uniquement le gateway en mode MCP — aucune clé ne lui est injectée.
141
+
142
+ ```
143
+ [Agent dans conteneur]
144
+ │ stdio (MCP)
145
+ ▼
146
+ [mcp-http-gateway --mcp] ←── GATEWAY_URL
147
+ │ HTTP
148
+ ▼
149
+ [mcp-http-gateway hors conteneur]
150
+ │ stdio
151
+ ▼
152
+ [MCPs réels (clés sensibles)]
153
+ ```
154
+
155
+ ### Démarrage
156
+
157
+ ```bash
158
+ GATEWAY_URL=http://host.docker.internal:3500 npx @imenam/mcp-http-gateway --mcp
159
+ ```
160
+
161
+ ### Configuration dans le conteneur
162
+
163
+ ```json
164
+ {
165
+ "mcpServers": {
166
+ "gateway": {
167
+ "command": "npx",
168
+ "args": ["@imenam/mcp-http-gateway", "--mcp"],
169
+ "env": {
170
+ "GATEWAY_URL": "http://host.docker.internal:3500"
171
+ }
172
+ }
173
+ }
174
+ }
175
+ ```
176
+
177
+ > `host.docker.internal` est l'adresse de l'hôte depuis un conteneur Docker.
178
+
179
+ ### Outils exposés
180
+
181
+ Le mode MCP expose 3 outils qui reflètent directement les routes du gateway :
182
+
183
+ | Outil | Description |
184
+ |---|---|
185
+ | `list_mcps` | Liste les MCPs disponibles avec leur route et description |
186
+ | `list_tools` | Liste les outils d'un MCP donné (`route` requis) |
187
+ | `call_tool` | Appelle un outil (`route`, `tool_name`, `arguments`) |
188
+
189
+ L'agent découvre d'abord les MCPs disponibles, explore les outils d'un MCP si besoin, puis appelle l'outil voulu.
190
+
191
+ ---
192
+
193
+ ## Ajouter un MCP
194
+
195
+ 1. Ajouter l'entrée dans `config/mcp-servers.json` avec `MCP_SERVER_ROUTE` dans `env`
196
+ 2. Redémarrer le gateway
197
+
198
+ Aucune modification du MCP n'est nécessaire.
@@ -0,0 +1,58 @@
1
+ import crypto from 'crypto';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import Database from 'better-sqlite3';
5
+
6
+ let database;
7
+
8
+ export function initDb(dbPath) {
9
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
10
+ database = new Database(dbPath);
11
+ database.pragma('journal_mode = WAL');
12
+ database.exec(`
13
+ CREATE TABLE IF NOT EXISTS tool_embeddings (
14
+ cache_key TEXT PRIMARY KEY,
15
+ desc_hash TEXT NOT NULL,
16
+ mcp_name TEXT NOT NULL,
17
+ tool_name TEXT NOT NULL,
18
+ description TEXT NOT NULL,
19
+ embedding BLOB NOT NULL
20
+ );
21
+ `);
22
+ }
23
+
24
+ export async function getOrComputeToolEmbedding(mcpName, tool, getEmbeddingFn) {
25
+ const cacheKey = `${mcpName}::${tool.name}`;
26
+ const descHash = crypto.createHash('md5').update(tool.description).digest('hex');
27
+
28
+ const existing = database
29
+ .prepare('SELECT desc_hash FROM tool_embeddings WHERE cache_key = ?')
30
+ .get(cacheKey);
31
+
32
+ if (existing?.desc_hash === descHash) return;
33
+
34
+ const embedding = await getEmbeddingFn(tool.description);
35
+ database.prepare(`
36
+ INSERT INTO tool_embeddings (cache_key, desc_hash, mcp_name, tool_name, description, embedding)
37
+ VALUES (?, ?, ?, ?, ?, ?)
38
+ ON CONFLICT(cache_key) DO UPDATE SET
39
+ desc_hash = excluded.desc_hash,
40
+ description = excluded.description,
41
+ embedding = excluded.embedding
42
+ `).run(
43
+ cacheKey, descHash, mcpName, tool.name, tool.description,
44
+ Buffer.from(new Float32Array(embedding).buffer),
45
+ );
46
+ }
47
+
48
+ export function getAllToolEmbeddings() {
49
+ return database
50
+ .prepare('SELECT mcp_name, tool_name, description, embedding FROM tool_embeddings')
51
+ .all()
52
+ .map(row => ({
53
+ mcp_name: row.mcp_name,
54
+ tool_name: row.tool_name,
55
+ description: row.description,
56
+ embedding: Array.from(new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength / 4)),
57
+ }));
58
+ }
@@ -0,0 +1,25 @@
1
+ import OpenAI from 'openai';
2
+
3
+ let openaiClient;
4
+
5
+ function getClient() {
6
+ if (!openaiClient) openaiClient = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
7
+ return openaiClient;
8
+ }
9
+
10
+ export async function getEmbedding(text) {
11
+ const model = process.env.OPENAI_EMBEDDING_MODEL ?? 'text-embedding-3-small';
12
+ const response = await getClient().embeddings.create({ model, input: text });
13
+ return response.data[0].embedding;
14
+ }
15
+
16
+ export function cosineSimilarity(vecA, vecB) {
17
+ let dot = 0, normA = 0, normB = 0;
18
+ for (let i = 0; i < vecA.length; i++) {
19
+ dot += vecA[i] * vecB[i];
20
+ normA += vecA[i] ** 2;
21
+ normB += vecB[i] ** 2;
22
+ }
23
+ const mag = Math.sqrt(normA) * Math.sqrt(normB);
24
+ return mag === 0 ? 0 : dot / mag;
25
+ }
package/mcp-mode.js ADDED
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import {
6
+ ListToolsRequestSchema,
7
+ CallToolRequestSchema,
8
+ } from '@modelcontextprotocol/sdk/types.js';
9
+
10
+ const GATEWAY_URL = process.env.GATEWAY_URL?.replace(/\/$/, '');
11
+ if (!GATEWAY_URL) {
12
+ console.error('[mcp-mode] GATEWAY_URL environment variable is required');
13
+ process.exit(1);
14
+ }
15
+
16
+ // --- MCP Server ---
17
+
18
+ const server = new Server(
19
+ { name: 'mcp-http-gateway-mcp', version: '1.0.0' },
20
+ { capabilities: { tools: {} } }
21
+ );
22
+
23
+ const tools = [
24
+ {
25
+ name: 'list_mcps',
26
+ description: 'List all MCPs available on the gateway with their route and description.',
27
+ inputSchema: { type: 'object', properties: {} },
28
+ },
29
+ {
30
+ name: 'list_tools',
31
+ description: 'List all tools available for a given MCP route.',
32
+ inputSchema: {
33
+ type: 'object',
34
+ properties: {
35
+ route: { type: 'string', description: 'MCP route as returned by list_mcps (e.g. "/documentor")' },
36
+ },
37
+ required: ['route'],
38
+ },
39
+ },
40
+ {
41
+ name: 'call_tool',
42
+ description: 'Call a tool on a given MCP route.',
43
+ inputSchema: {
44
+ type: 'object',
45
+ properties: {
46
+ route: { type: 'string', description: 'MCP route (e.g. "/documentor")' },
47
+ tool_name: { type: 'string', description: 'Tool name to call' },
48
+ arguments: { type: 'object', description: 'Arguments to pass to the tool' },
49
+ },
50
+ required: ['route', 'tool_name'],
51
+ },
52
+ },
53
+ ];
54
+
55
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
56
+
57
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
58
+ const { name, arguments: args = {} } = request.params;
59
+
60
+ try {
61
+ if (name === 'list_mcps') {
62
+ const res = await fetch(`${GATEWAY_URL}/mcp-definitions`);
63
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
64
+ const definitions = await res.json();
65
+ const text = definitions
66
+ .map(m => `- ${m.id} (route: ${m.route})${m.description ? ` — ${m.description}` : ''}`)
67
+ .join('\n');
68
+ return { content: [{ type: 'text', text }] };
69
+ }
70
+
71
+ if (name === 'list_tools') {
72
+ const res = await fetch(`${GATEWAY_URL}${args.route}/tools`);
73
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
74
+ const toolsList = await res.json();
75
+ const text = JSON.stringify(toolsList, null, 2);
76
+ return { content: [{ type: 'text', text }] };
77
+ }
78
+
79
+ if (name === 'call_tool') {
80
+ const res = await fetch(`${GATEWAY_URL}${args.route}/tools/${args.tool_name}`, {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json' },
83
+ body: JSON.stringify(args.arguments ?? {}),
84
+ });
85
+ if (!res.ok) {
86
+ const text = await res.text();
87
+ return { content: [{ type: 'text', text: `Gateway error ${res.status}: ${text}` }], isError: true };
88
+ }
89
+ return await res.json();
90
+ }
91
+
92
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
93
+ } catch (err) {
94
+ return { content: [{ type: 'text', text: `Request failed: ${err.message}` }], isError: true };
95
+ }
96
+ });
97
+
98
+ const transport = new StdioServerTransport();
99
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@imenam/mcp-http-gateway",
3
+ "version": "2.0.0",
4
+ "type": "module",
5
+ "description": "HTTP gateway that exposes MCP servers as REST routes — bridge between MCP and any HTTP-capable agent",
6
+ "main": "server.js",
7
+ "bin": {
8
+ "mcp-http-gateway": "server.js"
9
+ },
10
+ "files": [
11
+ "server.js",
12
+ "mcp-mode.js",
13
+ "README.md",
14
+ "embeddings/"
15
+ ],
16
+ "scripts": {
17
+ "start": "node server.js",
18
+ "release": "npm version patch && npm publish --access public && git push origin master --follow-tags"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "gateway",
24
+ "http",
25
+ "ai",
26
+ "agent",
27
+ "llm",
28
+ "bridge"
29
+ ],
30
+ "author": "Matthieu Pesnot-Pin <matthieupin@gmail.com>",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/Matthieu-Pesnot-Pin/mcp-http-gateway.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/Matthieu-Pesnot-Pin/mcp-http-gateway/issues"
37
+ },
38
+ "homepage": "https://github.com/Matthieu-Pesnot-Pin/mcp-http-gateway#readme",
39
+ "license": "ISC",
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.27.1",
45
+ "better-sqlite3": "^12.8.0",
46
+ "dotenv": "^17.3.1",
47
+ "express": "^5.2.1",
48
+ "openai": "^6.31.0"
49
+ }
50
+ }
package/server.js ADDED
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+
4
+ if (process.argv.includes('--mcp')) {
5
+ await import('./mcp-mode.js');
6
+ // Process stays alive: the MCP transport keeps stdin open until the client disconnects
7
+ }
8
+
9
+ import { readFileSync } from 'fs';
10
+ import path from 'path';
11
+ import express from 'express';
12
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
13
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
14
+ import { initDb, getOrComputeToolEmbedding, getAllToolEmbeddings } from './embeddings/db.js';
15
+ import { getEmbedding, cosineSimilarity } from './embeddings/vectorizer.js';
16
+
17
+ // Normalise les différents formats de config rencontrés dans la nature :
18
+ // - `command` string + `args` array (format Cursor)
19
+ // - `command` array (format OpenCode/autres)
20
+ // - `env` ou `environment` (selon l'outil)
21
+ function normalizeCfg(cfg) {
22
+ const env = cfg.env ?? cfg.environment ?? {};
23
+ const command = Array.isArray(cfg.command) ? cfg.command[0] : cfg.command;
24
+ const args = Array.isArray(cfg.command) ? cfg.command.slice(1) : (cfg.args ?? []);
25
+ return { ...cfg, command, args, env };
26
+ }
27
+
28
+ // --- MCP client ---
29
+
30
+ async function connectMcp(name, cfg) {
31
+ const transport = new StdioClientTransport({
32
+ command: cfg.command,
33
+ args: cfg.args ?? [],
34
+ env: { ...process.env, ...(cfg.env ?? {}) },
35
+ });
36
+ const client = new Client({ name: 'mcp-gateway', version: '1.0.0' }, { capabilities: {} });
37
+ await client.connect(transport);
38
+ console.log(`[gateway] Connected: ${name} → ${cfg.env?.MCP_SERVER_ROUTE}`);
39
+ return client;
40
+ }
41
+
42
+ // --- Routes ---
43
+
44
+ function registerMcpRoutes(app, route, client, name) {
45
+ app.get(`${route}/tools`, async (req, res) => {
46
+ try {
47
+ const { tools } = await client.listTools();
48
+ res.json(tools);
49
+ } catch (err) {
50
+ console.error(`[${name}] listTools error:`, err.message);
51
+ res.status(500).json({ error: err.message });
52
+ }
53
+ });
54
+
55
+ app.post(`${route}/tools/:toolName`, async (req, res) => {
56
+ try {
57
+ const result = await client.callTool({
58
+ name: req.params.toolName,
59
+ arguments: req.body ?? {},
60
+ });
61
+ res.json(result);
62
+ } catch (err) {
63
+ console.error(`[${name}] callTool(${req.params.toolName}) error:`, err.message);
64
+ res.status(500).json({ error: err.message });
65
+ }
66
+ });
67
+ }
68
+
69
+ // --- Bootstrap ---
70
+
71
+ async function main() {
72
+ const GATEWAY_PORT = parseInt(process.env.GATEWAY_PORT ?? '3500', 10);
73
+
74
+ // GATEWAY_CONFIG overrides the default path (config/mcp-servers.json in cwd).
75
+ const configPath = process.env.GATEWAY_CONFIG
76
+ ? path.resolve(process.env.GATEWAY_CONFIG)
77
+ : path.join(process.cwd(), 'config', 'mcp-servers.json');
78
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
79
+ const servers = config.mcpServers ?? {};
80
+
81
+ const app = express();
82
+ app.use(express.json({ type: '*/*' }));
83
+
84
+ const dataDir = process.env.GATEWAY_DATA_DIR ?? path.join(process.cwd(), 'data');
85
+ const dbPath = path.join(dataDir, 'embeddings', 'embeddings.db');
86
+ initDb(dbPath);
87
+
88
+ const clients = [];
89
+ const registry = []; // { name, route, description, tools }
90
+
91
+ for (const [name, cfg] of Object.entries(servers)) {
92
+ const route = cfg.env?.MCP_SERVER_ROUTE;
93
+ if (!route) {
94
+ console.warn(`[gateway] Skipping "${name}": no MCP_SERVER_ROUTE defined`);
95
+ continue;
96
+ }
97
+
98
+ try {
99
+ const client = await connectMcp(name, cfg);
100
+ const { tools } = await client.listTools();
101
+ clients.push(client);
102
+ registry.push({ name, route, description: cfg.description ?? null, tools });
103
+ registerMcpRoutes(app, route, client, name);
104
+ for (const tool of tools) {
105
+ if (!tool.description) continue;
106
+ getOrComputeToolEmbedding(name, tool, getEmbedding).catch(err =>
107
+ console.error(`[gateway] Embedding error for ${name}::${tool.name}:`, err.message)
108
+ );
109
+ }
110
+ } catch (err) {
111
+ console.error(`[gateway] Failed to connect "${name}":`, err.message);
112
+ }
113
+ }
114
+
115
+ app.get('/tools/search', async (req, res) => {
116
+ const q = req.query.q?.trim();
117
+ const limit = Math.min(parseInt(req.query.limit ?? '5', 10), 20);
118
+ if (!q) return res.status(400).json({ error: 'Missing query parameter: q' });
119
+ try {
120
+ const queryEmbedding = await getEmbedding(q);
121
+ const results = getAllToolEmbeddings()
122
+ .map(row => ({ ...row, score: cosineSimilarity(queryEmbedding, row.embedding) }))
123
+ .sort((a, b) => b.score - a.score)
124
+ .slice(0, limit)
125
+ .map(({ mcp_name, tool_name, description, score }) => ({
126
+ mcp_name,
127
+ mcp_description: registry.find(r => r.name === mcp_name)?.description ?? null,
128
+ tool_name,
129
+ description,
130
+ score,
131
+ }));
132
+ res.json(results);
133
+ } catch (err) {
134
+ console.error('[gateway] /tools/search error:', err.message);
135
+ res.status(500).json({ error: err.message });
136
+ }
137
+ });
138
+
139
+ app.get('/mcp-definitions', (req, res) => {
140
+ res.json(
141
+ registry.map(({ name, route, description, tools }) => ({
142
+ id: name,
143
+ route,
144
+ description,
145
+ tools,
146
+ }))
147
+ );
148
+ });
149
+
150
+ app.get('/mcps', (req, res) => {
151
+ const lines = [
152
+ '# MCP Gateway — Serveurs disponibles',
153
+ '',
154
+ 'Les outils de chaque serveur sont accessibles via `POST /{route}/tools/{toolName}` avec les arguments dans le body JSON.',
155
+ 'Pour lister les outils disponibles d\'un serveur, appelez `GET /{route}/tools`.',
156
+ '',
157
+ '## Serveurs',
158
+ '',
159
+ ...registry.map(({ name, route, description }) =>
160
+ `### ${name}\n- **Route** : \`${route}\`\n- **Description** : ${description ?? 'Non renseignée'}`
161
+ ),
162
+ ];
163
+ res.type('text/plain').send(lines.join('\n'));
164
+ });
165
+
166
+ app.listen(GATEWAY_PORT, () => {
167
+ console.log(`[gateway] Listening on http://localhost:${GATEWAY_PORT}`);
168
+ });
169
+
170
+ const shutdown = async () => {
171
+ console.log('[gateway] Shutting down...');
172
+ await Promise.allSettled(clients.map(c => c.close()));
173
+ process.exit(0);
174
+ };
175
+
176
+ process.on('SIGTERM', shutdown);
177
+ process.on('SIGINT', shutdown);
178
+ }
179
+
180
+ if (!process.argv.includes('--mcp')) {
181
+ main().catch(err => {
182
+ console.error('[gateway] Fatal error:', err.message);
183
+ process.exit(1);
184
+ });
185
+ }