@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.
Files changed (3) hide show
  1. package/README.md +28 -0
  2. package/build/gateway.js +171 -0
  3. 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.
@@ -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
+ }