@natomalabs/natoma-mcp-gateway 1.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 +28 -0
- package/build/gateway.js +171 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Natoma's Server Gateway for all things MCP
|
|
2
|
+
|
|
3
|
+
This repo hosts the MCP Bridge proxy that enables the communication between an stdio-based Claude Desktop app and HTTP/SSE supported MCP servers.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This Bridge Proxy service enables interoperability by:
|
|
8
|
+
|
|
9
|
+
- Creating a unified communication layer between different MCP implementations
|
|
10
|
+
- Translating stdio protocol messages into HTTP/SSE format
|
|
11
|
+
- Converting server responses back into the expected stdio format
|
|
12
|
+
|
|
13
|
+
## What is MCP?
|
|
14
|
+
|
|
15
|
+
Reference: https://modelcontextprotocol.io/introduction
|
|
16
|
+
|
|
17
|
+
## How It Works
|
|
18
|
+
|
|
19
|
+
The acts as an intermediary that:
|
|
20
|
+
|
|
21
|
+
- Listens for incoming stdio messages from AI client hosts. Example: Claude Desktop
|
|
22
|
+
- Maintains persistent SSE connections with Natoma MCP Platform
|
|
23
|
+
- Handles bi-directional protocol conversion
|
|
24
|
+
- Ensures reliable message delivery and error recovery
|
|
25
|
+
|
|
26
|
+
This allows applications built for stdio-based MCP to work smoothly with the Natoma MCP Platform without requiring changes to either side.
|
|
27
|
+
|
|
28
|
+
Required Node.js Version: Node.js 18 or higher is required to run this project.
|
package/build/gateway.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import EventSource from "eventsource";
|
|
3
|
+
const NATOMA_MCP_SERVER_URL = "https://api.app.natoma.ai/api/mcp";
|
|
4
|
+
export class MCPGateway {
|
|
5
|
+
eventSource = null;
|
|
6
|
+
sessionId = null;
|
|
7
|
+
isReady = false;
|
|
8
|
+
messageQueue = [];
|
|
9
|
+
reconnectAttempts = 0;
|
|
10
|
+
baseUrl;
|
|
11
|
+
sseUrl;
|
|
12
|
+
messageUrl;
|
|
13
|
+
maxReconnectAttempts;
|
|
14
|
+
reconnectDelay;
|
|
15
|
+
apiKey; // Store the API key
|
|
16
|
+
constructor(config) {
|
|
17
|
+
const slug = config?.slug;
|
|
18
|
+
this.baseUrl = NATOMA_MCP_SERVER_URL;
|
|
19
|
+
this.sseUrl = slug ? `${this.baseUrl}/${slug}` : `${this.baseUrl}`;
|
|
20
|
+
this.messageUrl = slug
|
|
21
|
+
? `${this.baseUrl}/${slug}/message`
|
|
22
|
+
: `${this.baseUrl}/message`;
|
|
23
|
+
this.maxReconnectAttempts = config?.maxReconnectAttempts ?? 3;
|
|
24
|
+
this.reconnectDelay = config?.reconnectDelay ?? 1000;
|
|
25
|
+
this.apiKey = config?.apiKey;
|
|
26
|
+
}
|
|
27
|
+
async connect() {
|
|
28
|
+
if (this.eventSource) {
|
|
29
|
+
console.error("Closing existing connection");
|
|
30
|
+
this.eventSource.close();
|
|
31
|
+
}
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
// Include API key in headers if it exists
|
|
34
|
+
const headers = {
|
|
35
|
+
Accept: "text/event-stream",
|
|
36
|
+
};
|
|
37
|
+
// Add Authorization header if API key is provided
|
|
38
|
+
if (this.apiKey) {
|
|
39
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
40
|
+
}
|
|
41
|
+
this.eventSource = new EventSource(this.sseUrl, { headers });
|
|
42
|
+
this.eventSource.onopen = () => {
|
|
43
|
+
console.error("--- SSE backend connected");
|
|
44
|
+
this.reconnectAttempts = 0;
|
|
45
|
+
resolve();
|
|
46
|
+
};
|
|
47
|
+
this.eventSource.onerror = (error) => {
|
|
48
|
+
console.error(`--- SSE backend error: ${error?.message}`);
|
|
49
|
+
this.handleConnectionError(error);
|
|
50
|
+
reject(error);
|
|
51
|
+
};
|
|
52
|
+
this.setupEventListeners();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
setupEventListeners() {
|
|
56
|
+
if (!this.eventSource)
|
|
57
|
+
return;
|
|
58
|
+
this.eventSource.addEventListener("endpoint", (event) => {
|
|
59
|
+
const match = event.data.match(/sessionId=([^&]+)/);
|
|
60
|
+
if (match) {
|
|
61
|
+
this.sessionId = match[1];
|
|
62
|
+
this.isReady = true;
|
|
63
|
+
console.error(`Session established: ${this.sessionId}`);
|
|
64
|
+
this.processQueuedMessages();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
this.eventSource.addEventListener("message", (event) => {
|
|
68
|
+
try {
|
|
69
|
+
console.error(`<-- ${event.data}`);
|
|
70
|
+
console.log(event.data); // Forward to stdout
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
console.error(`Error handling message: ${error}`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async handleConnectionError(error) {
|
|
78
|
+
console.error(`Connection error: ${error.message}`);
|
|
79
|
+
if (this.eventSource?.readyState === EventSource.CLOSED) {
|
|
80
|
+
console.error("EventSource connection closed");
|
|
81
|
+
await this.reconnect();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async reconnect() {
|
|
85
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
86
|
+
console.error(`Max reconnection attempts (${this.maxReconnectAttempts}) reached, exiting...`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
this.reconnectAttempts++;
|
|
90
|
+
this.isReady = false;
|
|
91
|
+
try {
|
|
92
|
+
await new Promise((resolve) => setTimeout(resolve, this.reconnectDelay));
|
|
93
|
+
await this.connect();
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
console.error(`Reconnection failed: ${error}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async processMessage(input) {
|
|
100
|
+
if (!this.isReady || !this.sessionId) {
|
|
101
|
+
this.messageQueue.push(input);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const message = input.toString().trim();
|
|
105
|
+
console.error(`--> ${message}`);
|
|
106
|
+
try {
|
|
107
|
+
const url = `${this.messageUrl}?sessionId=${this.sessionId}`;
|
|
108
|
+
// Define headers with content type
|
|
109
|
+
const headers = {
|
|
110
|
+
"Content-Type": "application/json",
|
|
111
|
+
};
|
|
112
|
+
// Add Authorization header if API key is provided
|
|
113
|
+
if (this.apiKey) {
|
|
114
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
115
|
+
}
|
|
116
|
+
const response = await fetch(url, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers,
|
|
119
|
+
body: message,
|
|
120
|
+
});
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
if (response.status === 503) {
|
|
123
|
+
await this.reconnect();
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
console.error(`Error from server: ${response.status} ${response.statusText}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
console.error(`Request error: ${error}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async processQueuedMessages() {
|
|
135
|
+
while (this.messageQueue.length > 0) {
|
|
136
|
+
const message = this.messageQueue.shift();
|
|
137
|
+
if (message) {
|
|
138
|
+
await this.processMessage(message);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
cleanup() {
|
|
143
|
+
if (this.eventSource) {
|
|
144
|
+
this.eventSource.close();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Example usage
|
|
149
|
+
async function main() {
|
|
150
|
+
const gateway = new MCPGateway({
|
|
151
|
+
apiKey: process.env.NATOMA_MCP_API_KEY,
|
|
152
|
+
slug: process.env.NATOMA_MCP_SERVER_INSTALLATION_ID,
|
|
153
|
+
});
|
|
154
|
+
try {
|
|
155
|
+
await gateway.connect();
|
|
156
|
+
process.stdin.on("data", (data) => gateway.processMessage(data));
|
|
157
|
+
// Handle cleanup
|
|
158
|
+
const cleanup = () => {
|
|
159
|
+
console.error("Shutting down...");
|
|
160
|
+
gateway.cleanup();
|
|
161
|
+
process.exit(0);
|
|
162
|
+
};
|
|
163
|
+
process.on("SIGINT", cleanup);
|
|
164
|
+
process.on("SIGTERM", cleanup);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
console.error(`Fatal error:`, error);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@natomalabs/natoma-mcp-gateway",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Natoma MCP Gateway",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gateway": "./build/gateway.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"build"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"prepare": "npm run build",
|
|
15
|
+
"inspector": "node build/gateway.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.0.3",
|
|
19
|
+
"eventsource": "^2.0.2",
|
|
20
|
+
"express": "^4.21.1"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/eventsource": "^1.1.15",
|
|
24
|
+
"@types/node": "^20.11.0",
|
|
25
|
+
"@types/express": "^5.0.0",
|
|
26
|
+
"typescript": "^5.3.3"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
}
|