@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 +198 -0
- package/embeddings/db.js +58 -0
- package/embeddings/vectorizer.js +25 -0
- package/mcp-mode.js +99 -0
- package/package.json +50 -0
- package/server.js +185 -0
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.
|
package/embeddings/db.js
ADDED
|
@@ -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
|
+
}
|