Skip to content

Commit ed3da8e

Browse files
Adapt to hono server
1 parent ed2bd7b commit ed3da8e

5 files changed

Lines changed: 328 additions & 19 deletions

File tree

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
"name": "neuro-mcp-relay-registry",
33
"version": "VSC-NeuroPilot",
44
"private": true,
5+
"dependencies": {
6+
"@modelcontextprotocol/sdk": "^1.0.0",
7+
"hono": "^4.6.12",
8+
"fetch-to-node": "^2.1.0"
9+
},
510
"devDependencies": {
611
"@changesets/cli": "^2.29.7",
712
"prettier": "^3.6.2",

pnpm-lock.yaml

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/routes/api/hello.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

server/routes/registry/index.ts

Whitespace-only changes.

server/server.ts

Lines changed: 308 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,319 @@
1-
import { Hono } from 'hono';
2-
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
3-
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4-
import { z } from 'zod';
1+
import {Hono} from 'hono';
2+
import {cors} from 'hono/cors';
3+
import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4+
import {toFetchResponse, toReqRes} from 'fetch-to-node';
5+
import {McpRelayServer} from './routes/mcp';
6+
import {PermissionMode} from "./routes/mcp/permissions";
57

6-
// #region MCP Server
78

8-
const mcp = new McpServer({
9-
name: 'neuro-mcp-relay-registry',
10-
version: '0.0.1-beta'
11-
})
9+
// Initialize the relay server when the module loads
10+
async function initializeRelayServer() {
11+
try {
12+
// Create relay server instance
13+
let relayServer = new McpRelayServer({
14+
name: "neuro-mcp-relay-registry",
15+
version: "0.0.1-beta",
16+
maxPending: 100,
17+
lockTimeout: 30000,
18+
toolSeparator: ":", // Use colon as separator for serverID:toolName
19+
defaultPermissionMode: 'copilot', // Default to copilot mode for security
20+
});
1221

13-
// #endregion
22+
// Initialize the relay server
23+
await relayServer.initialize();
24+
console.log(`[Server] Relay server initialized`);
1425

15-
// #region Hono app
26+
// Add any initial upstream servers here if needed
27+
// This could be configured via environment variables or a config file
28+
29+
console.log(`[Server] MCP Relay server initialized successfully`);
30+
return relayServer;
31+
} catch (error) {
32+
console.error('[Server] Failed to initialize relay server:', error);
33+
throw error;
34+
}
35+
}
36+
37+
const relayServer = await initializeRelayServer();
38+
const permissionManager = relayServer.getPermissionManager()
39+
const approvalQueue = permissionManager.getApprovalQueue()
1640

1741
const app = new Hono();
1842

19-
app.post('/mcp', (c) => {
20-
return c.text('Streamable HTTP MCP not yet implemented.')
21-
})
43+
// Add CORS middleware
44+
app.use('*', cors());
45+
46+
// ========== Relay server endpoints ==========
47+
48+
app.get('/health', async (c) => {
49+
try {
50+
const info = await relayServer.getInfo();
51+
const registry = relayServer.getRegistry();
52+
const stats = await registry.getStats();
53+
54+
return c.json({
55+
status: "ok",
56+
...info,
57+
servers: {
58+
total: stats.totalServers,
59+
connected: stats.connectedServers,
60+
disconnected: stats.disconnectedServers,
61+
errored: stats.erroredServers,
62+
},
63+
});
64+
} catch (error) {
65+
return c.json({error: String(error)}, 500);
66+
}
67+
});
68+
69+
app.get('/stats', async (c) => {
70+
try {
71+
const registry = relayServer.getRegistry();
72+
const stats = await registry.getStats();
73+
const servers = await registry.listServers();
74+
75+
const serverDetails = await Promise.all(
76+
servers.map(async (serverId) => {
77+
const wrapper = await registry.getServer(serverId);
78+
if (!wrapper) return null;
79+
80+
const info = wrapper.getInfo();
81+
return {
82+
serverId,
83+
name: info.name,
84+
state: info.state,
85+
connectedAt: info.connectedAt?.toISOString(),
86+
error: info.error,
87+
};
88+
})
89+
);
90+
91+
return c.json({
92+
stats,
93+
servers: serverDetails.filter((s) => s !== null),
94+
});
95+
} catch (error) {
96+
return c.json({error: String(error)}, 500);
97+
}
98+
});
99+
100+
app.post('/mcp', async (c) => {
101+
console.log(`[Server] Incoming MCP request from ${c.req.header('X-Forwarded-For') || c.req.header('CF-Connecting-IP') || c.req.header('X-Real-IP') || c.req.raw.headers.get('X-Real-IP') || 'unknown'}`);
102+
const {req, res} = toReqRes(c.req.raw);
103+
104+
try {
105+
// Get the MCP server instance
106+
const mcpServer = relayServer.getMcpServer();
107+
108+
// Create transport with stateless mode (no session management)
109+
const transport = new StreamableHTTPServerTransport({
110+
sessionIdGenerator: undefined, // Stateless mode
111+
});
112+
113+
console.log(`[Relay Server] Transport created, connecting server...`);
114+
115+
// Connect the server to the transport (sets up message handlers)
116+
await mcpServer.connect(transport);
117+
console.log(`[Relay Server] Server connected to transport`);
118+
119+
// Handle the actual HTTP request using the transport
120+
console.log(`[Relay Server] Processing request through transport...`);
121+
await transport.handleRequest(req, res, await c.req.json());
122+
console.log(`[Relay Server] Request processed successfully`);
123+
124+
res.on('close', () => {
125+
console.log('Request closed');
126+
transport.close();
127+
mcpServer.close();
128+
});
129+
130+
return toFetchResponse(res);
131+
132+
} catch (error) {
133+
console.error(`[Relay Server] Error handling MCP request:`, error);
134+
console.error(`[Relay Server] Error stack:`, String(error));
135+
return c.json({error: "Internal server error"});
136+
}
137+
});
138+
139+
140+
// ========== Permission Control Endpoints ==========
141+
142+
app.get('/api/permissions', async (c) => {
143+
try {
144+
const permissions = await permissionManager.getAllPermissions();
145+
const toolsWithPermissions = await permissionManager.getToolsWithPermissions();
146+
return c.json({
147+
configured: Array.from(permissions.entries()).map(([name, perm]) => ({
148+
toolName: name,
149+
mode: perm.mode,
150+
lastModified: perm.lastModified,
151+
})),
152+
allTools: toolsWithPermissions,
153+
});
154+
} catch (error) {
155+
return c.json({error: String(error)}, 500);
156+
}
157+
});
158+
159+
app.put('/api/permissions/:toolName', async (c) => {
160+
const toolName = c.req.param('toolName');
161+
const {mode} = await c.req.json();
162+
163+
if (!['auto', 'copilot', 'disabled'].includes(mode)) {
164+
return c.json({error: 'Invalid mode. Must be auto, copilot, or disabled'}, 400);
165+
}
166+
167+
try {
168+
await permissionManager.updatePermission(toolName, mode as PermissionMode);
169+
return c.json({success: true, toolName, mode});
170+
} catch (error) {
171+
return c.json({error: String(error)}, 500);
172+
}
173+
});
174+
175+
app.post('/api/permissions/batch', async (c) => {
176+
const {updates = {}} = await c.req.json();
177+
178+
try {
179+
// Validate and convert to Map<string, PermissionMode>
180+
const mapUpdates = new Map<string, PermissionMode>();
181+
for (const [toolName, mode] of Object.entries(updates)) {
182+
if (!['auto', 'copilot', 'disabled'].includes(mode as string)) {
183+
return c.json({error: `Invalid mode for ${toolName}: ${mode}`}, 400);
184+
}
185+
mapUpdates.set(toolName, mode as PermissionMode);
186+
}
187+
188+
await permissionManager.updatePermissions(mapUpdates);
189+
return c.json({success: true, count: mapUpdates.size});
190+
} catch (error) {
191+
return c.json({error: String(error)}, 500);
192+
}
193+
});
194+
195+
app.get('/api/permissions/stats', async (c) => {
196+
try {
197+
const stats = await permissionManager.getStats();
198+
return c.json(stats);
199+
} catch (error) {
200+
return c.json({error: String(error)}, 500);
201+
}
202+
});
203+
204+
app.get('/api/approvals/pending', async (c) => {
205+
try {
206+
const pending = approvalQueue.getPending();
207+
return c.json(pending);
208+
} catch (error) {
209+
return c.json({error: String(error)}, 500);
210+
}
211+
});
212+
213+
app.post('/api/approvals/:approvalId/approve', async (c) => {
214+
if (!approvalQueue) {
215+
return c.json({error: 'Approval queue not available'}, 500);
216+
}
217+
218+
const approvalId = c.req.param('approvalId');
219+
const {message} = await c.req.json();
220+
221+
try {
222+
approvalQueue.approve(approvalId, message);
223+
return c.json({success: true, approvalId});
224+
} catch (error) {
225+
return c.json({error: String(error)}, 400);
226+
}
227+
});
228+
229+
app.post('/api/approvals/:approvalId/reject', async (c) => {
230+
const approvalId = c.req.param('approvalId');
231+
const {message} = await c.req.json();
232+
233+
try {
234+
approvalQueue.reject(approvalId, message);
235+
return c.json({success: true, approvalId});
236+
} catch (error) {
237+
return c.json({error: String(error)}, 400);
238+
}
239+
});
240+
241+
app.get('/api/approvals/history', async (c) => {
242+
try {
243+
const history = approvalQueue.getHistory(100);
244+
return c.json(history);
245+
} catch (error) {
246+
return c.json({error: String(error)}, 500);
247+
}
248+
});
249+
250+
// ========== Upstream Server Management Endpoints ==========
251+
252+
app.post('/api/servers/register', async (c) => {
253+
const registry = relayServer.getRegistry();
254+
const {serverId, clientConfig} = await c.req.json();
255+
256+
if (!serverId || !clientConfig) {
257+
return c.json({error: 'Missing serverId or clientConfig'}, 400);
258+
}
259+
260+
if (!clientConfig.transport || !clientConfig.serverUrl || !clientConfig.name) {
261+
return c.json({error: 'clientConfig requires transport, serverUrl, and name'}, 400);
262+
}
263+
264+
try {
265+
const result = await registry.registerServer({
266+
serverId,
267+
clientConfig,
268+
autoConnect: true,
269+
});
270+
271+
if (result.success && result.serverInfo) {
272+
console.log(`[Server] ${serverId} connected successfully`);
273+
return c.json({
274+
success: true,
275+
serverId,
276+
serverInfo: result.serverInfo
277+
});
278+
} else {
279+
return c.json({
280+
success: false,
281+
error: result.error || 'Registration failed'
282+
}, 500);
283+
}
284+
} catch (error) {
285+
console.error(`[Server] Failed to register ${serverId}:`, error);
286+
return c.json({error: String(error)}, 500);
287+
}
288+
});
289+
290+
app.get('/api/servers', async (c) => {
291+
const registry = relayServer.getRegistry();
292+
293+
try {
294+
const serverIds = await registry.listServers();
295+
const serverDetails = await Promise.all(
296+
serverIds.map(async (serverId) => {
297+
const server = await registry.getServer(serverId);
298+
if (!server) return null;
22299

23-
app.post('/sse', (c) => {
24-
return c.text('Legacy SSE not implemented.')
25-
})
300+
const info = server.getInfo();
301+
return {
302+
serverId,
303+
name: info.name,
304+
state: info.state,
305+
connectedAt: info.connectedAt?.toISOString(),
306+
error: info.error,
307+
};
308+
})
309+
);
26310

27-
// #endregion
311+
return c.json({
312+
servers: serverDetails.filter((s) => s !== null),
313+
});
314+
} catch (error) {
315+
return c.json({error: String(error)}, 500);
316+
}
317+
});
28318

29319
export default app

0 commit comments

Comments
 (0)