@nitrostack/core 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/LICENSE +201 -0
- package/README.md +80 -0
- package/dist/auth/api-key.d.ts +118 -0
- package/dist/auth/api-key.d.ts.map +1 -0
- package/dist/auth/api-key.js +168 -0
- package/dist/auth/api-key.js.map +1 -0
- package/dist/auth/client.d.ts +151 -0
- package/dist/auth/client.d.ts.map +1 -0
- package/dist/auth/client.js +330 -0
- package/dist/auth/client.js.map +1 -0
- package/dist/auth/index.d.ts +31 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +46 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/middleware.d.ts +95 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +260 -0
- package/dist/auth/middleware.js.map +1 -0
- package/dist/auth/pkce.d.ts +53 -0
- package/dist/auth/pkce.d.ts.map +1 -0
- package/dist/auth/pkce.js +105 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/auth/quick-setup.d.ts +94 -0
- package/dist/auth/quick-setup.d.ts.map +1 -0
- package/dist/auth/quick-setup.js +210 -0
- package/dist/auth/quick-setup.js.map +1 -0
- package/dist/auth/secure-secret.d.ts +136 -0
- package/dist/auth/secure-secret.d.ts.map +1 -0
- package/dist/auth/secure-secret.js +182 -0
- package/dist/auth/secure-secret.js.map +1 -0
- package/dist/auth/server-integration.d.ts +97 -0
- package/dist/auth/server-integration.d.ts.map +1 -0
- package/dist/auth/server-integration.js +182 -0
- package/dist/auth/server-integration.js.map +1 -0
- package/dist/auth/server-metadata.d.ts +51 -0
- package/dist/auth/server-metadata.d.ts.map +1 -0
- package/dist/auth/server-metadata.js +106 -0
- package/dist/auth/server-metadata.js.map +1 -0
- package/dist/auth/simple-jwt.d.ts +174 -0
- package/dist/auth/simple-jwt.d.ts.map +1 -0
- package/dist/auth/simple-jwt.js +162 -0
- package/dist/auth/simple-jwt.js.map +1 -0
- package/dist/auth/token-store.d.ts +104 -0
- package/dist/auth/token-store.d.ts.map +1 -0
- package/dist/auth/token-store.js +205 -0
- package/dist/auth/token-store.js.map +1 -0
- package/dist/auth/token-validation.d.ts +59 -0
- package/dist/auth/token-validation.d.ts.map +1 -0
- package/dist/auth/token-validation.js +241 -0
- package/dist/auth/token-validation.js.map +1 -0
- package/dist/auth/types.d.ts +215 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +6 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/core/apikey-module.d.ts +69 -0
- package/dist/core/apikey-module.d.ts.map +1 -0
- package/dist/core/apikey-module.js +114 -0
- package/dist/core/apikey-module.js.map +1 -0
- package/dist/core/app-decorator.d.ts +59 -0
- package/dist/core/app-decorator.d.ts.map +1 -0
- package/dist/core/app-decorator.js +322 -0
- package/dist/core/app-decorator.js.map +1 -0
- package/dist/core/builders.d.ts +50 -0
- package/dist/core/builders.d.ts.map +1 -0
- package/dist/core/builders.js +139 -0
- package/dist/core/builders.js.map +1 -0
- package/dist/core/component.d.ts +111 -0
- package/dist/core/component.d.ts.map +1 -0
- package/dist/core/component.js +228 -0
- package/dist/core/component.js.map +1 -0
- package/dist/core/config-module.d.ts +62 -0
- package/dist/core/config-module.d.ts.map +1 -0
- package/dist/core/config-module.js +94 -0
- package/dist/core/config-module.js.map +1 -0
- package/dist/core/decorators/cache.decorator.d.ts +61 -0
- package/dist/core/decorators/cache.decorator.d.ts.map +1 -0
- package/dist/core/decorators/cache.decorator.js +115 -0
- package/dist/core/decorators/cache.decorator.js.map +1 -0
- package/dist/core/decorators/health-check.decorator.d.ts +80 -0
- package/dist/core/decorators/health-check.decorator.d.ts.map +1 -0
- package/dist/core/decorators/health-check.decorator.js +153 -0
- package/dist/core/decorators/health-check.decorator.js.map +1 -0
- package/dist/core/decorators/rate-limit.decorator.d.ts +63 -0
- package/dist/core/decorators/rate-limit.decorator.d.ts.map +1 -0
- package/dist/core/decorators/rate-limit.decorator.js +129 -0
- package/dist/core/decorators/rate-limit.decorator.js.map +1 -0
- package/dist/core/decorators.d.ts +190 -0
- package/dist/core/decorators.d.ts.map +1 -0
- package/dist/core/decorators.js +170 -0
- package/dist/core/decorators.js.map +1 -0
- package/dist/core/di/container.d.ts +64 -0
- package/dist/core/di/container.d.ts.map +1 -0
- package/dist/core/di/container.js +105 -0
- package/dist/core/di/container.js.map +1 -0
- package/dist/core/di/injectable.decorator.d.ts +62 -0
- package/dist/core/di/injectable.decorator.d.ts.map +1 -0
- package/dist/core/di/injectable.decorator.js +66 -0
- package/dist/core/di/injectable.decorator.js.map +1 -0
- package/dist/core/errors.d.ts +54 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +87 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/events/event-emitter.d.ts +50 -0
- package/dist/core/events/event-emitter.d.ts.map +1 -0
- package/dist/core/events/event-emitter.js +94 -0
- package/dist/core/events/event-emitter.js.map +1 -0
- package/dist/core/events/event.decorator.d.ts +48 -0
- package/dist/core/events/event.decorator.d.ts.map +1 -0
- package/dist/core/events/event.decorator.js +72 -0
- package/dist/core/events/event.decorator.js.map +1 -0
- package/dist/core/events/log-emitter.d.ts +14 -0
- package/dist/core/events/log-emitter.d.ts.map +1 -0
- package/dist/core/events/log-emitter.js +20 -0
- package/dist/core/events/log-emitter.js.map +1 -0
- package/dist/core/filters/exception-filter.decorator.d.ts +40 -0
- package/dist/core/filters/exception-filter.decorator.d.ts.map +1 -0
- package/dist/core/filters/exception-filter.decorator.js +54 -0
- package/dist/core/filters/exception-filter.decorator.js.map +1 -0
- package/dist/core/filters/exception-filter.interface.d.ts +39 -0
- package/dist/core/filters/exception-filter.interface.d.ts.map +1 -0
- package/dist/core/filters/exception-filter.interface.js +2 -0
- package/dist/core/filters/exception-filter.interface.js.map +1 -0
- package/dist/core/guards/apikey.guard.d.ts +22 -0
- package/dist/core/guards/apikey.guard.d.ts.map +1 -0
- package/dist/core/guards/apikey.guard.js +11 -0
- package/dist/core/guards/apikey.guard.js.map +1 -0
- package/dist/core/guards/guard.interface.d.ts +18 -0
- package/dist/core/guards/guard.interface.d.ts.map +1 -0
- package/dist/core/guards/guard.interface.js +2 -0
- package/dist/core/guards/guard.interface.js.map +1 -0
- package/dist/core/guards/jwt.guard.d.ts +18 -0
- package/dist/core/guards/jwt.guard.d.ts.map +1 -0
- package/dist/core/guards/jwt.guard.js +2 -0
- package/dist/core/guards/jwt.guard.js.map +1 -0
- package/dist/core/guards/oauth.guard.d.ts +35 -0
- package/dist/core/guards/oauth.guard.d.ts.map +1 -0
- package/dist/core/guards/oauth.guard.js +2 -0
- package/dist/core/guards/oauth.guard.js.map +1 -0
- package/dist/core/guards/use-guards.decorator.d.ts +25 -0
- package/dist/core/guards/use-guards.decorator.d.ts.map +1 -0
- package/dist/core/guards/use-guards.decorator.js +32 -0
- package/dist/core/guards/use-guards.decorator.js.map +1 -0
- package/dist/core/health/health-checks.resource.d.ts +14 -0
- package/dist/core/health/health-checks.resource.d.ts.map +1 -0
- package/dist/core/health/health-checks.resource.js +29 -0
- package/dist/core/health/health-checks.resource.js.map +1 -0
- package/dist/core/index.d.ts +57 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +59 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/interceptors/interceptor.decorator.d.ts +37 -0
- package/dist/core/interceptors/interceptor.decorator.d.ts.map +1 -0
- package/dist/core/interceptors/interceptor.decorator.js +51 -0
- package/dist/core/interceptors/interceptor.decorator.js.map +1 -0
- package/dist/core/interceptors/interceptor.interface.d.ts +31 -0
- package/dist/core/interceptors/interceptor.interface.d.ts.map +1 -0
- package/dist/core/interceptors/interceptor.interface.js +2 -0
- package/dist/core/interceptors/interceptor.interface.js.map +1 -0
- package/dist/core/jwt-module.d.ts +51 -0
- package/dist/core/jwt-module.d.ts.map +1 -0
- package/dist/core/jwt-module.js +52 -0
- package/dist/core/jwt-module.js.map +1 -0
- package/dist/core/logger.d.ts +18 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +53 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/middleware/middleware.decorator.d.ts +39 -0
- package/dist/core/middleware/middleware.decorator.d.ts.map +1 -0
- package/dist/core/middleware/middleware.decorator.js +53 -0
- package/dist/core/middleware/middleware.decorator.js.map +1 -0
- package/dist/core/middleware/middleware.interface.d.ts +29 -0
- package/dist/core/middleware/middleware.interface.d.ts.map +1 -0
- package/dist/core/middleware/middleware.interface.js +2 -0
- package/dist/core/middleware/middleware.interface.js.map +1 -0
- package/dist/core/module.d.ts +93 -0
- package/dist/core/module.d.ts.map +1 -0
- package/dist/core/module.js +87 -0
- package/dist/core/module.js.map +1 -0
- package/dist/core/oauth-module.d.ts +123 -0
- package/dist/core/oauth-module.d.ts.map +1 -0
- package/dist/core/oauth-module.js +324 -0
- package/dist/core/oauth-module.js.map +1 -0
- package/dist/core/pipes/pipe.decorator.d.ts +64 -0
- package/dist/core/pipes/pipe.decorator.d.ts.map +1 -0
- package/dist/core/pipes/pipe.decorator.js +85 -0
- package/dist/core/pipes/pipe.decorator.js.map +1 -0
- package/dist/core/pipes/pipe.interface.d.ts +41 -0
- package/dist/core/pipes/pipe.interface.d.ts.map +1 -0
- package/dist/core/pipes/pipe.interface.js +2 -0
- package/dist/core/pipes/pipe.interface.js.map +1 -0
- package/dist/core/prompt.d.ts +46 -0
- package/dist/core/prompt.d.ts.map +1 -0
- package/dist/core/prompt.js +76 -0
- package/dist/core/prompt.js.map +1 -0
- package/dist/core/resource.d.ts +47 -0
- package/dist/core/resource.d.ts.map +1 -0
- package/dist/core/resource.js +90 -0
- package/dist/core/resource.js.map +1 -0
- package/dist/core/server.d.ts +129 -0
- package/dist/core/server.d.ts.map +1 -0
- package/dist/core/server.js +617 -0
- package/dist/core/server.js.map +1 -0
- package/dist/core/tool.d.ts +108 -0
- package/dist/core/tool.d.ts.map +1 -0
- package/dist/core/tool.js +241 -0
- package/dist/core/tool.js.map +1 -0
- package/dist/core/transports/discovery-http-server.d.ts +19 -0
- package/dist/core/transports/discovery-http-server.d.ts.map +1 -0
- package/dist/core/transports/discovery-http-server.js +54 -0
- package/dist/core/transports/discovery-http-server.js.map +1 -0
- package/dist/core/transports/http-server.d.ts +108 -0
- package/dist/core/transports/http-server.d.ts.map +1 -0
- package/dist/core/transports/http-server.js +293 -0
- package/dist/core/transports/http-server.js.map +1 -0
- package/dist/core/transports/streamable-http.d.ts +177 -0
- package/dist/core/transports/streamable-http.d.ts.map +1 -0
- package/dist/core/transports/streamable-http.js +1287 -0
- package/dist/core/transports/streamable-http.js.map +1 -0
- package/dist/core/types.d.ts +195 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/widgets/widget-examples.resource.d.ts +17 -0
- package/dist/core/widgets/widget-examples.resource.d.ts.map +1 -0
- package/dist/core/widgets/widget-examples.resource.js +28 -0
- package/dist/core/widgets/widget-examples.resource.js.map +1 -0
- package/dist/core/widgets/widget-registry.d.ts +56 -0
- package/dist/core/widgets/widget-registry.d.ts.map +1 -0
- package/dist/core/widgets/widget-registry.js +75 -0
- package/dist/core/widgets/widget-registry.js.map +1 -0
- package/dist/testing/index.d.ts +103 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +161 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/ui-next/index.d.ts +31 -0
- package/dist/ui-next/index.d.ts.map +1 -0
- package/dist/ui-next/index.js +687 -0
- package/dist/ui-next/index.js.map +1 -0
- package/package.json +89 -0
|
@@ -0,0 +1,1287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streamable HTTP Transport for MCP
|
|
3
|
+
*
|
|
4
|
+
* Implements the MCP Streamable HTTP transport specification (2025-06-18).
|
|
5
|
+
* https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Single MCP endpoint supporting both POST and GET
|
|
9
|
+
* - POST for sending messages to server
|
|
10
|
+
* - GET for SSE streams from server
|
|
11
|
+
* - Session management with Mcp-Session-Id header
|
|
12
|
+
* - Resumability support with Last-Event-ID
|
|
13
|
+
* - Multiple concurrent client connections
|
|
14
|
+
* - Protocol version header support
|
|
15
|
+
*/
|
|
16
|
+
import express from 'express';
|
|
17
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
18
|
+
import { readFileSync } from 'fs';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
import { dirname, join } from 'path';
|
|
21
|
+
/**
|
|
22
|
+
* Streamable HTTP Transport
|
|
23
|
+
* Implements MCP Streamable HTTP specification
|
|
24
|
+
*/
|
|
25
|
+
export class StreamableHttpTransport {
|
|
26
|
+
app;
|
|
27
|
+
server = null;
|
|
28
|
+
sessions = new Map();
|
|
29
|
+
activeStreams = new Map(); // For sessionless mode
|
|
30
|
+
messageHandler;
|
|
31
|
+
closeHandler;
|
|
32
|
+
errorHandler;
|
|
33
|
+
options;
|
|
34
|
+
sessionCleanupInterval;
|
|
35
|
+
getToolsCallback;
|
|
36
|
+
serverConfig;
|
|
37
|
+
logoBase64;
|
|
38
|
+
constructor(options = {}) {
|
|
39
|
+
this.options = {
|
|
40
|
+
port: options.port || 3000,
|
|
41
|
+
host: options.host || 'localhost',
|
|
42
|
+
endpoint: options.endpoint || '/mcp',
|
|
43
|
+
enableSessions: options.enableSessions === true, // Default to false for simpler clients
|
|
44
|
+
sessionTimeout: options.sessionTimeout || 30 * 60 * 1000, // 30 minutes
|
|
45
|
+
enableCors: options.enableCors !== false, // Default to true
|
|
46
|
+
};
|
|
47
|
+
this.app = options.app || express();
|
|
48
|
+
// CRITICAL: Disable Express's automatic OPTIONS handling
|
|
49
|
+
this.app.set('x-powered-by', false);
|
|
50
|
+
// Enable trust proxy to respect X-Forwarded-* headers from reverse proxies
|
|
51
|
+
// This is essential for HTTPS detection when behind a proxy
|
|
52
|
+
this.app.set('trust proxy', true);
|
|
53
|
+
// Load logo for documentation page
|
|
54
|
+
this.loadLogo();
|
|
55
|
+
this.setupMiddleware();
|
|
56
|
+
this.setupRoutes();
|
|
57
|
+
this.startSessionCleanup();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Load logo image as base64 for embedding in documentation page
|
|
61
|
+
*/
|
|
62
|
+
loadLogo() {
|
|
63
|
+
try {
|
|
64
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
65
|
+
const __dirname = dirname(__filename);
|
|
66
|
+
// Try multiple paths:
|
|
67
|
+
// 1. From dist/core/transports/streamable-http.js -> ../../../src/assets/nitrocloud.png (package source)
|
|
68
|
+
// 2. From dist/core/transports/streamable-http.js -> ../../../../src/assets/nitrocloud.png (if in nitrostack package)
|
|
69
|
+
// 3. From project root (user's project) -> src/assets/nitrocloud.png
|
|
70
|
+
const possiblePaths = [
|
|
71
|
+
join(__dirname, '../../../src/assets/nitrocloud.png'), // From dist/core/transports -> src/assets
|
|
72
|
+
join(__dirname, '../../../../src/assets/nitrocloud.png'), // From dist/core/transports -> src/assets (alternative)
|
|
73
|
+
join(process.cwd(), 'src/assets/nitrocloud.png'), // User's project
|
|
74
|
+
join(process.cwd(), 'node_modules/nitrostack/src/assets/nitrocloud.png'), // From node_modules
|
|
75
|
+
];
|
|
76
|
+
let logoPath = null;
|
|
77
|
+
for (const path of possiblePaths) {
|
|
78
|
+
try {
|
|
79
|
+
if (readFileSync(path, { flag: 'r' })) {
|
|
80
|
+
logoPath = path;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (logoPath) {
|
|
89
|
+
const logoBuffer = readFileSync(logoPath);
|
|
90
|
+
this.logoBase64 = logoBuffer.toString('base64');
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
this.logoBase64 = undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
// Logo is optional, continue without it
|
|
98
|
+
this.logoBase64 = undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Setup Express middleware
|
|
103
|
+
*/
|
|
104
|
+
setupMiddleware() {
|
|
105
|
+
// CORS (if enabled) - MUST be the very first middleware, handles ALL requests
|
|
106
|
+
if (this.options.enableCors) {
|
|
107
|
+
// Add CORS headers to ALL responses
|
|
108
|
+
this.app.use((req, res, next) => {
|
|
109
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
110
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
111
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Authorization, Mcp-Session-Id, MCP-Protocol-Version, Last-Event-ID');
|
|
112
|
+
res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
|
|
113
|
+
// Handle OPTIONS immediately
|
|
114
|
+
if (req.method === 'OPTIONS') {
|
|
115
|
+
res.status(200).end();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
next();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
// Security: Validate Origin header to prevent DNS rebinding attacks (skip if CORS enabled)
|
|
122
|
+
if (!this.options.enableCors) {
|
|
123
|
+
this.app.use((req, res, next) => {
|
|
124
|
+
const origin = req.get('Origin');
|
|
125
|
+
const host = req.get('Host');
|
|
126
|
+
if (origin && host) {
|
|
127
|
+
const originHost = new URL(origin).host;
|
|
128
|
+
if (originHost !== host && !this.isLocalhost(originHost)) {
|
|
129
|
+
res.status(403).json({ error: 'Invalid Origin header' });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
next();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// JSON parsing
|
|
137
|
+
this.app.use(express.json());
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Setup MCP endpoint routes
|
|
141
|
+
*/
|
|
142
|
+
setupRoutes() {
|
|
143
|
+
const endpoint = this.options.endpoint;
|
|
144
|
+
// IMPORTANT: Add OPTIONS handlers FIRST to override Express's auto-OPTIONS
|
|
145
|
+
if (this.options.enableCors) {
|
|
146
|
+
// Main endpoint OPTIONS
|
|
147
|
+
this.app.options(endpoint, (req, res) => {
|
|
148
|
+
res.sendStatus(200);
|
|
149
|
+
});
|
|
150
|
+
// SSE endpoint OPTIONS
|
|
151
|
+
this.app.options(`${endpoint}/sse`, (req, res) => {
|
|
152
|
+
res.sendStatus(200);
|
|
153
|
+
});
|
|
154
|
+
// Message endpoint OPTIONS
|
|
155
|
+
this.app.options(`${endpoint}/message`, (req, res) => {
|
|
156
|
+
res.sendStatus(200);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// MCP Endpoint - POST for sending messages to server (main endpoint)
|
|
160
|
+
this.app.post(endpoint, async (req, res) => {
|
|
161
|
+
await this.handlePost(req, res);
|
|
162
|
+
});
|
|
163
|
+
// Legacy message endpoint for backward compatibility with old HTTP transport clients
|
|
164
|
+
// Some clients may POST to /mcp/message instead of /mcp
|
|
165
|
+
this.app.post(`${endpoint}/message`, async (req, res) => {
|
|
166
|
+
await this.handlePost(req, res);
|
|
167
|
+
});
|
|
168
|
+
// MCP Endpoint - GET for SSE streams (main endpoint)
|
|
169
|
+
this.app.get(endpoint, (req, res) => {
|
|
170
|
+
this.handleGet(req, res);
|
|
171
|
+
});
|
|
172
|
+
// Legacy SSE endpoint for backward compatibility with old HTTP transport clients
|
|
173
|
+
// Some clients may connect to /mcp/sse instead of /mcp
|
|
174
|
+
this.app.get(`${endpoint}/sse`, (req, res) => {
|
|
175
|
+
this.handleGet(req, res);
|
|
176
|
+
});
|
|
177
|
+
// MCP Endpoint - DELETE for session termination
|
|
178
|
+
this.app.delete(endpoint, (req, res) => {
|
|
179
|
+
this.handleDelete(req, res);
|
|
180
|
+
});
|
|
181
|
+
// Backward compatibility: /sse endpoint (alias for GET /mcp)
|
|
182
|
+
this.app.get(`${endpoint}/sse`, (req, res) => {
|
|
183
|
+
this.handleGet(req, res);
|
|
184
|
+
});
|
|
185
|
+
// Backward compatibility: /message endpoint (alias for POST /mcp)
|
|
186
|
+
this.app.post(`${endpoint}/message`, async (req, res) => {
|
|
187
|
+
// Simple message handler that doesn't require all the session/SSE logic
|
|
188
|
+
try {
|
|
189
|
+
const message = req.body;
|
|
190
|
+
if (!message || !message.jsonrpc) {
|
|
191
|
+
res.status(400).json({ error: 'Invalid JSON-RPC message' });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// Pass to message handler
|
|
195
|
+
if (this.messageHandler) {
|
|
196
|
+
await this.messageHandler(message);
|
|
197
|
+
}
|
|
198
|
+
res.json({ status: 'received' });
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
console.error('Error handling message:', error);
|
|
202
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
// Info endpoint for GET on /message
|
|
206
|
+
this.app.get(`${endpoint}/message`, (req, res) => {
|
|
207
|
+
res.json({
|
|
208
|
+
endpoint: `${endpoint}/message`,
|
|
209
|
+
method: 'POST',
|
|
210
|
+
description: 'Send JSON-RPC messages to the MCP server',
|
|
211
|
+
usage: 'POST with Content-Type: application/json',
|
|
212
|
+
example: {
|
|
213
|
+
jsonrpc: '2.0',
|
|
214
|
+
method: 'initialize',
|
|
215
|
+
params: {
|
|
216
|
+
protocolVersion: '2024-11-05',
|
|
217
|
+
capabilities: {},
|
|
218
|
+
clientInfo: { name: 'test-client', version: '1.0.0' }
|
|
219
|
+
},
|
|
220
|
+
id: 1
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
// Health check
|
|
225
|
+
this.app.get(`${endpoint}/health`, (req, res) => {
|
|
226
|
+
res.json({
|
|
227
|
+
status: 'ok',
|
|
228
|
+
transport: 'streamable-http',
|
|
229
|
+
version: '2025-06-18',
|
|
230
|
+
sessions: this.sessions.size,
|
|
231
|
+
uptime: process.uptime(),
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
// Root documentation page (only in production mode when HTTP server runs)
|
|
235
|
+
// This route is added at the end to avoid conflicts with MCP endpoints
|
|
236
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
237
|
+
this.app.get('/', async (req, res) => {
|
|
238
|
+
try {
|
|
239
|
+
const tools = this.getToolsCallback ? await this.getToolsCallback() : [];
|
|
240
|
+
// Get host from request headers (supports X-Forwarded-Host for reverse proxies)
|
|
241
|
+
let host = req.get('x-forwarded-host') || req.get('host') || `${this.options.host}:${this.options.port}`;
|
|
242
|
+
// In production, remove port if it's standard HTTP/HTTPS port
|
|
243
|
+
// This handles cases where the server is behind a reverse proxy
|
|
244
|
+
if (process.env.NODE_ENV === 'production') {
|
|
245
|
+
// Remove port if it's 80 (HTTP) or 443 (HTTPS)
|
|
246
|
+
host = host.replace(/:(80|443)$/, '');
|
|
247
|
+
}
|
|
248
|
+
// Support X-Forwarded-Proto for reverse proxies (production deployments)
|
|
249
|
+
const protocol = req.get('x-forwarded-proto') || req.protocol || 'http';
|
|
250
|
+
const baseUrl = `${protocol}://${host}`;
|
|
251
|
+
const mcpEndpoint = `${baseUrl}${endpoint}`;
|
|
252
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
253
|
+
res.send(this.generateDocumentationPage(tools, mcpEndpoint));
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
console.error('Error generating documentation page:', error);
|
|
257
|
+
res.status(500).send('Error generating documentation page');
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Handle POST requests (client sending messages to server)
|
|
264
|
+
*/
|
|
265
|
+
async handlePost(req, res) {
|
|
266
|
+
try {
|
|
267
|
+
const message = req.body;
|
|
268
|
+
const sessionId = req.get('Mcp-Session-Id');
|
|
269
|
+
const accept = req.get('Accept') || '';
|
|
270
|
+
// Validate JSON-RPC message
|
|
271
|
+
if (!message || !message.jsonrpc || message.jsonrpc !== '2.0') {
|
|
272
|
+
res.status(400).json({
|
|
273
|
+
jsonrpc: '2.0',
|
|
274
|
+
error: { code: -32600, message: 'Invalid JSON-RPC message' }
|
|
275
|
+
});
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
// Check session
|
|
279
|
+
if (this.options.enableSessions && sessionId) {
|
|
280
|
+
const session = this.sessions.get(sessionId);
|
|
281
|
+
if (!session) {
|
|
282
|
+
res.status(404).json({
|
|
283
|
+
jsonrpc: '2.0',
|
|
284
|
+
error: { code: -32001, message: 'Session not found' }
|
|
285
|
+
});
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
session.lastActivity = Date.now();
|
|
289
|
+
}
|
|
290
|
+
// Handle different message types
|
|
291
|
+
const messageType = this.getMessageType(message);
|
|
292
|
+
if (messageType === 'notification' || messageType === 'response') {
|
|
293
|
+
// Notification or Response: Return 202 Accepted
|
|
294
|
+
if (this.messageHandler) {
|
|
295
|
+
await this.messageHandler(message);
|
|
296
|
+
}
|
|
297
|
+
res.status(202).send();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (messageType === 'request') {
|
|
301
|
+
// Request: Accept header check (be lenient - if not specified, assume they want SSE)
|
|
302
|
+
const supportsSSE = !accept || accept.includes('text/event-stream') || accept.includes('*/*');
|
|
303
|
+
const supportsJSON = accept.includes('application/json');
|
|
304
|
+
// Pass to message handler
|
|
305
|
+
if (this.messageHandler) {
|
|
306
|
+
await this.messageHandler(message);
|
|
307
|
+
}
|
|
308
|
+
// For InitializeRequest, create session if enabled
|
|
309
|
+
if (this.isInitializeRequest(message)) {
|
|
310
|
+
if (this.options.enableSessions && !sessionId) {
|
|
311
|
+
const newSessionId = this.generateSessionId();
|
|
312
|
+
const session = {
|
|
313
|
+
id: newSessionId,
|
|
314
|
+
streams: new Map(),
|
|
315
|
+
lastActivity: Date.now(),
|
|
316
|
+
messageQueue: [],
|
|
317
|
+
eventIdCounter: 0,
|
|
318
|
+
};
|
|
319
|
+
this.sessions.set(newSessionId, session);
|
|
320
|
+
res.setHeader('Mcp-Session-Id', newSessionId);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// For SSE: Just acknowledge receipt, response will come via existing SSE stream
|
|
324
|
+
if (supportsSSE) {
|
|
325
|
+
// Accept the request
|
|
326
|
+
res.status(202).send();
|
|
327
|
+
// Response will be sent via the send() method to existing SSE streams
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
// Single JSON response (less common)
|
|
331
|
+
res.setHeader('Content-Type', 'application/json');
|
|
332
|
+
// Response will be sent by the protocol layer
|
|
333
|
+
res._mcpWaitingForResponse = true;
|
|
334
|
+
res._mcpRequestId = message.id;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
console.error('POST error:', error);
|
|
340
|
+
res.status(500).json({
|
|
341
|
+
jsonrpc: '2.0',
|
|
342
|
+
error: { code: -32603, message: 'Internal error' }
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Handle GET requests (client opening SSE stream)
|
|
348
|
+
*/
|
|
349
|
+
handleGet(req, res) {
|
|
350
|
+
const sessionId = req.get('Mcp-Session-Id');
|
|
351
|
+
const lastEventId = req.get('Last-Event-ID');
|
|
352
|
+
const accept = req.get('Accept') || '';
|
|
353
|
+
// Check if client explicitly doesn't want SSE (e.g., asking for JSON only)
|
|
354
|
+
const rejectsSSE = accept && !accept.includes('*/*') && !accept.includes('text/event-stream') && accept.length > 0;
|
|
355
|
+
if (rejectsSSE) {
|
|
356
|
+
res.status(405).send('Method Not Allowed - This endpoint provides Server-Sent Events');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
// Check session
|
|
360
|
+
let session;
|
|
361
|
+
if (this.options.enableSessions) {
|
|
362
|
+
if (!sessionId) {
|
|
363
|
+
res.status(400).json({ error: 'Mcp-Session-Id required' });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
session = this.sessions.get(sessionId);
|
|
367
|
+
if (!session) {
|
|
368
|
+
res.status(404).json({ error: 'Session not found' });
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
session.lastActivity = Date.now();
|
|
372
|
+
}
|
|
373
|
+
// Setup SSE
|
|
374
|
+
// CRITICAL: Set CORS headers for SSE if enabled (must be before flushHeaders)
|
|
375
|
+
// SSE connections need CORS headers explicitly set before the stream starts
|
|
376
|
+
if (this.options.enableCors) {
|
|
377
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
378
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
379
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Authorization, Mcp-Session-Id, MCP-Protocol-Version, Last-Event-ID');
|
|
380
|
+
res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
|
|
381
|
+
}
|
|
382
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
383
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
384
|
+
res.setHeader('Connection', 'keep-alive');
|
|
385
|
+
res.flushHeaders();
|
|
386
|
+
// Create stream
|
|
387
|
+
const streamId = uuidv4();
|
|
388
|
+
const stream = {
|
|
389
|
+
id: streamId,
|
|
390
|
+
response: res,
|
|
391
|
+
eventIdCounter: 0,
|
|
392
|
+
closed: false,
|
|
393
|
+
};
|
|
394
|
+
// Send endpoint event immediately (required by MCP SDK)
|
|
395
|
+
// This tells the client where to POST messages
|
|
396
|
+
// Support X-Forwarded-Proto for reverse proxies (production deployments)
|
|
397
|
+
// This ensures the endpoint URL matches the client's connection protocol (HTTPS)
|
|
398
|
+
const protocol = req.get('x-forwarded-proto') || req.protocol || 'http';
|
|
399
|
+
// Detect client format based on request URL:
|
|
400
|
+
// - If client connects to /mcp/sse (old format) → return /mcp/message as endpoint
|
|
401
|
+
// - If client connects to /mcp (new format) → return /mcp as endpoint
|
|
402
|
+
// Use originalUrl or url to get the full path including /sse
|
|
403
|
+
const requestPath = req.originalUrl || req.url || req.path;
|
|
404
|
+
const isOldFormat = requestPath.includes(`${this.options.endpoint}/sse`) ||
|
|
405
|
+
requestPath.endsWith('/sse') ||
|
|
406
|
+
req.path === `${this.options.endpoint}/sse`;
|
|
407
|
+
const messageEndpoint = isOldFormat
|
|
408
|
+
? `${this.options.endpoint}/message` // Old format: POST to /mcp/message
|
|
409
|
+
: this.options.endpoint; // New format: POST to /mcp
|
|
410
|
+
const endpointUrl = `${protocol}://${req.get('host')}${messageEndpoint}`;
|
|
411
|
+
try {
|
|
412
|
+
res.write(`event: endpoint\n`);
|
|
413
|
+
res.write(`data: ${endpointUrl}\n\n`);
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
console.error('Error sending endpoint event:', error);
|
|
417
|
+
stream.closed = true;
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
// Add to session or activeStreams
|
|
421
|
+
if (session) {
|
|
422
|
+
session.streams.set(streamId, stream);
|
|
423
|
+
// Resume support: replay messages after lastEventId
|
|
424
|
+
if (lastEventId) {
|
|
425
|
+
this.replayMessages(session, stream, lastEventId);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
// Sessionless mode: track in activeStreams
|
|
430
|
+
this.activeStreams.set(streamId, stream);
|
|
431
|
+
}
|
|
432
|
+
// Handle client disconnect
|
|
433
|
+
req.on('close', () => {
|
|
434
|
+
stream.closed = true;
|
|
435
|
+
if (session) {
|
|
436
|
+
session.streams.delete(streamId);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
this.activeStreams.delete(streamId);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
// Send ping every 30 seconds to keep connection alive
|
|
443
|
+
const pingInterval = setInterval(() => {
|
|
444
|
+
if (stream.closed) {
|
|
445
|
+
clearInterval(pingInterval);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
try {
|
|
449
|
+
res.write(': ping\n\n');
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
clearInterval(pingInterval);
|
|
453
|
+
stream.closed = true;
|
|
454
|
+
}
|
|
455
|
+
}, 30000);
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Handle DELETE requests (session termination)
|
|
459
|
+
*/
|
|
460
|
+
handleDelete(req, res) {
|
|
461
|
+
const sessionId = req.get('Mcp-Session-Id');
|
|
462
|
+
if (!sessionId) {
|
|
463
|
+
res.status(400).json({ error: 'Mcp-Session-Id required' });
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const session = this.sessions.get(sessionId);
|
|
467
|
+
if (!session) {
|
|
468
|
+
res.status(404).json({ error: 'Session not found' });
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
// Close all streams
|
|
472
|
+
for (const stream of session.streams.values()) {
|
|
473
|
+
try {
|
|
474
|
+
stream.response.end();
|
|
475
|
+
stream.closed = true;
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
// Ignore
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// Remove session
|
|
482
|
+
this.sessions.delete(sessionId);
|
|
483
|
+
res.status(200).json({ status: 'session terminated' });
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Start SSE stream for a request
|
|
487
|
+
*/
|
|
488
|
+
async startSSEStream(req, res, request, sessionId) {
|
|
489
|
+
// Setup SSE
|
|
490
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
491
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
492
|
+
res.setHeader('Connection', 'keep-alive');
|
|
493
|
+
res.flushHeaders();
|
|
494
|
+
// Create stream
|
|
495
|
+
const streamId = uuidv4();
|
|
496
|
+
const stream = {
|
|
497
|
+
id: streamId,
|
|
498
|
+
response: res,
|
|
499
|
+
eventIdCounter: 0,
|
|
500
|
+
closed: false,
|
|
501
|
+
};
|
|
502
|
+
// Store stream reference for this request
|
|
503
|
+
req._mcpStreamId = streamId;
|
|
504
|
+
req._mcpStream = stream;
|
|
505
|
+
// Add to session or activeStreams
|
|
506
|
+
if (sessionId) {
|
|
507
|
+
const session = this.sessions.get(sessionId);
|
|
508
|
+
if (session) {
|
|
509
|
+
session.streams.set(streamId, stream);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
// Sessionless mode: track in activeStreams
|
|
514
|
+
this.activeStreams.set(streamId, stream);
|
|
515
|
+
}
|
|
516
|
+
// Handle client disconnect
|
|
517
|
+
req.on('close', () => {
|
|
518
|
+
stream.closed = true;
|
|
519
|
+
if (sessionId) {
|
|
520
|
+
const session = this.sessions.get(sessionId);
|
|
521
|
+
if (session) {
|
|
522
|
+
session.streams.delete(streamId);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
this.activeStreams.delete(streamId);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Send message to client(s)
|
|
532
|
+
*/
|
|
533
|
+
async send(message) {
|
|
534
|
+
// Find target session/stream
|
|
535
|
+
// For responses, send to the stream that made the request
|
|
536
|
+
if (this.isResponse(message)) {
|
|
537
|
+
const response = message;
|
|
538
|
+
await this.sendToRequestStream(response);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
// For requests and notifications, send to all active streams
|
|
542
|
+
// First, send to session-based streams
|
|
543
|
+
for (const session of this.sessions.values()) {
|
|
544
|
+
for (const stream of session.streams.values()) {
|
|
545
|
+
if (!stream.closed) {
|
|
546
|
+
await this.sendToStream(stream, message, session);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
// Then, send to sessionless streams
|
|
551
|
+
for (const stream of this.activeStreams.values()) {
|
|
552
|
+
if (!stream.closed) {
|
|
553
|
+
await this.sendToStreamSessionless(stream, message);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Send message to a specific stream
|
|
559
|
+
*/
|
|
560
|
+
async sendToStream(stream, message, session) {
|
|
561
|
+
try {
|
|
562
|
+
const eventId = `${session.id}-${++stream.eventIdCounter}`;
|
|
563
|
+
const data = JSON.stringify(message);
|
|
564
|
+
stream.response.write(`id: ${eventId}\n`);
|
|
565
|
+
stream.response.write(`data: ${data}\n\n`);
|
|
566
|
+
// Store in queue for resumability
|
|
567
|
+
session.messageQueue.push({
|
|
568
|
+
message,
|
|
569
|
+
streamId: stream.id,
|
|
570
|
+
eventId,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
console.error('Error sending to stream:', error);
|
|
575
|
+
stream.closed = true;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Send response to the stream that made the request
|
|
580
|
+
*/
|
|
581
|
+
async sendToRequestStream(response) {
|
|
582
|
+
// Find the stream associated with this request
|
|
583
|
+
// For session-based streams
|
|
584
|
+
for (const session of this.sessions.values()) {
|
|
585
|
+
for (const stream of session.streams.values()) {
|
|
586
|
+
if (!stream.closed) {
|
|
587
|
+
await this.sendToStream(stream, response, session);
|
|
588
|
+
// Keep stream open for multiple requests - stream will close when client disconnects
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// For sessionless streams - CRITICAL: Keep stream open for multiple requests
|
|
593
|
+
// The SSE stream should stay open to handle initialize, ping, tool calls, etc.
|
|
594
|
+
// Closing it after the first response breaks subsequent requests
|
|
595
|
+
for (const stream of this.activeStreams.values()) {
|
|
596
|
+
if (!stream.closed) {
|
|
597
|
+
await this.sendToStreamSessionless(stream, response);
|
|
598
|
+
// Keep stream open - it will be closed when the client disconnects naturally
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Send message to a sessionless stream
|
|
604
|
+
*/
|
|
605
|
+
async sendToStreamSessionless(stream, message) {
|
|
606
|
+
try {
|
|
607
|
+
const eventId = `${stream.id}-${++stream.eventIdCounter}`;
|
|
608
|
+
const data = JSON.stringify(message);
|
|
609
|
+
stream.response.write(`id: ${eventId}\n`);
|
|
610
|
+
stream.response.write(`data: ${data}\n\n`);
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
console.error('Error sending to sessionless stream:', error);
|
|
614
|
+
stream.closed = true;
|
|
615
|
+
this.activeStreams.delete(stream.id);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Replay messages for resumability
|
|
620
|
+
*/
|
|
621
|
+
replayMessages(session, stream, lastEventId) {
|
|
622
|
+
const messages = session.messageQueue.filter((msg) => msg.streamId === stream.id && msg.eventId > lastEventId);
|
|
623
|
+
for (const { message, eventId } of messages) {
|
|
624
|
+
try {
|
|
625
|
+
const data = JSON.stringify(message);
|
|
626
|
+
stream.response.write(`id: ${eventId}\n`);
|
|
627
|
+
stream.response.write(`data: ${data}\n\n`);
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
console.error('Error replaying message:', error);
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Start the HTTP server
|
|
637
|
+
*/
|
|
638
|
+
async start() {
|
|
639
|
+
if (this.server) {
|
|
640
|
+
await this.close();
|
|
641
|
+
}
|
|
642
|
+
return new Promise((resolve, reject) => {
|
|
643
|
+
const errorHandler = (error) => {
|
|
644
|
+
console.error(`Failed to start Streamable HTTP transport: ${error.message}`);
|
|
645
|
+
this.server = null;
|
|
646
|
+
reject(error);
|
|
647
|
+
};
|
|
648
|
+
try {
|
|
649
|
+
const server = this.app.listen(this.options.port, this.options.host);
|
|
650
|
+
server.once('error', errorHandler);
|
|
651
|
+
server.once('listening', () => {
|
|
652
|
+
server.removeListener('error', errorHandler);
|
|
653
|
+
server.on('error', (error) => {
|
|
654
|
+
if (this.errorHandler) {
|
|
655
|
+
this.errorHandler(error);
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
this.server = server;
|
|
659
|
+
console.error(`🌐 MCP Streamable HTTP transport listening on http://${this.options.host}:${this.options.port}${this.options.endpoint}`);
|
|
660
|
+
console.error(` Protocol: MCP 2025-06-18`);
|
|
661
|
+
console.error(` Sessions: ${this.options.enableSessions ? 'enabled' : 'disabled'}`);
|
|
662
|
+
resolve();
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
reject(error);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Register additional HTTP routes
|
|
672
|
+
* Allows modules (like OAuthModule) to add custom endpoints
|
|
673
|
+
*/
|
|
674
|
+
on(path, handler) {
|
|
675
|
+
this.app.get(path, handler);
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Close the transport
|
|
679
|
+
*/
|
|
680
|
+
async close() {
|
|
681
|
+
// Clear session cleanup
|
|
682
|
+
if (this.sessionCleanupInterval) {
|
|
683
|
+
clearInterval(this.sessionCleanupInterval);
|
|
684
|
+
}
|
|
685
|
+
// Close all sessions
|
|
686
|
+
for (const session of this.sessions.values()) {
|
|
687
|
+
for (const stream of session.streams.values()) {
|
|
688
|
+
try {
|
|
689
|
+
stream.response.end();
|
|
690
|
+
stream.closed = true;
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
// Ignore
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
this.sessions.clear();
|
|
698
|
+
// Close HTTP server
|
|
699
|
+
if (this.server) {
|
|
700
|
+
return new Promise((resolve) => {
|
|
701
|
+
const server = this.server;
|
|
702
|
+
this.server = null;
|
|
703
|
+
server.closeAllConnections?.();
|
|
704
|
+
server.close((err) => {
|
|
705
|
+
if (err) {
|
|
706
|
+
console.error('HTTP server close error:', err.message);
|
|
707
|
+
}
|
|
708
|
+
if (this.closeHandler) {
|
|
709
|
+
this.closeHandler();
|
|
710
|
+
}
|
|
711
|
+
resolve();
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
if (this.closeHandler) {
|
|
716
|
+
this.closeHandler();
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Set message handler
|
|
721
|
+
*/
|
|
722
|
+
set onmessage(handler) {
|
|
723
|
+
this.messageHandler = handler;
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Set close handler
|
|
727
|
+
*/
|
|
728
|
+
set onclose(handler) {
|
|
729
|
+
this.closeHandler = handler;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Set error handler
|
|
733
|
+
*/
|
|
734
|
+
set onerror(handler) {
|
|
735
|
+
this.errorHandler = handler;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Start session cleanup interval
|
|
739
|
+
*/
|
|
740
|
+
startSessionCleanup() {
|
|
741
|
+
this.sessionCleanupInterval = setInterval(() => {
|
|
742
|
+
const now = Date.now();
|
|
743
|
+
for (const [sessionId, session] of this.sessions.entries()) {
|
|
744
|
+
if (now - session.lastActivity > this.options.sessionTimeout) {
|
|
745
|
+
// Cleanup expired session
|
|
746
|
+
for (const stream of session.streams.values()) {
|
|
747
|
+
try {
|
|
748
|
+
stream.response.end();
|
|
749
|
+
stream.closed = true;
|
|
750
|
+
}
|
|
751
|
+
catch (error) {
|
|
752
|
+
// Ignore
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
this.sessions.delete(sessionId);
|
|
756
|
+
console.error(`Session ${sessionId} expired and cleaned up`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}, 60000); // Check every minute
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Helper methods
|
|
763
|
+
*/
|
|
764
|
+
generateSessionId() {
|
|
765
|
+
return uuidv4();
|
|
766
|
+
}
|
|
767
|
+
getMessageType(message) {
|
|
768
|
+
if ('method' in message && 'id' in message)
|
|
769
|
+
return 'request';
|
|
770
|
+
if ('result' in message || 'error' in message)
|
|
771
|
+
return 'response';
|
|
772
|
+
return 'notification';
|
|
773
|
+
}
|
|
774
|
+
isResponse(message) {
|
|
775
|
+
return 'result' in message || 'error' in message;
|
|
776
|
+
}
|
|
777
|
+
isInitializeRequest(message) {
|
|
778
|
+
return 'method' in message && message.method === 'initialize';
|
|
779
|
+
}
|
|
780
|
+
isLocalhost(host) {
|
|
781
|
+
// Extract hostname without port (handles both IPv4 and IPv6 formats)
|
|
782
|
+
let hostname = host;
|
|
783
|
+
if (host.includes('[') && host.includes(']')) {
|
|
784
|
+
// IPv6 with port format: [::1]:3000
|
|
785
|
+
hostname = host.substring(host.indexOf('[') + 1, host.indexOf(']'));
|
|
786
|
+
}
|
|
787
|
+
else if (host.includes(':') && (host.match(/:/g) || []).length > 1) {
|
|
788
|
+
// Raw IPv6: ::1
|
|
789
|
+
hostname = host;
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
// IPv4 or hostname: localhost:3000 or 127.0.0.1:3000
|
|
793
|
+
hostname = host.split(':')[0];
|
|
794
|
+
}
|
|
795
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Get the Express app (for adding custom routes)
|
|
799
|
+
*/
|
|
800
|
+
getApp() {
|
|
801
|
+
return this.app;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Set callback to get tools list for documentation page
|
|
805
|
+
*/
|
|
806
|
+
setToolsCallback(callback) {
|
|
807
|
+
this.getToolsCallback = callback;
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Set server configuration for documentation page
|
|
811
|
+
*/
|
|
812
|
+
setServerConfig(config) {
|
|
813
|
+
this.serverConfig = config;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Generate HTML documentation page
|
|
817
|
+
*/
|
|
818
|
+
generateDocumentationPage(tools, mcpEndpoint) {
|
|
819
|
+
const serverName = this.serverConfig?.name || 'NitroStack MCP Server';
|
|
820
|
+
const serverVersion = this.serverConfig?.version || '1.0.0';
|
|
821
|
+
const serverDescription = this.serverConfig?.description || 'A powerful MCP server built with NitroStack';
|
|
822
|
+
return `<!DOCTYPE html>
|
|
823
|
+
<html lang="en">
|
|
824
|
+
<head>
|
|
825
|
+
<meta charset="UTF-8">
|
|
826
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
827
|
+
<title>${serverName} - MCP Server Documentation</title>
|
|
828
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
829
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
830
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
831
|
+
<style>
|
|
832
|
+
:root {
|
|
833
|
+
--nitrocloud-primary: hsl(217, 91%, 60%);
|
|
834
|
+
--nitrocloud-primary-dark: hsl(217, 91%, 50%);
|
|
835
|
+
--nitrocloud-gradient-start: hsl(217, 91%, 60%);
|
|
836
|
+
--nitrocloud-gradient-end: hsl(221, 83%, 53%);
|
|
837
|
+
--background: hsl(0, 0%, 100%);
|
|
838
|
+
--foreground: hsl(222.2, 84%, 4.9%);
|
|
839
|
+
--primary: hsl(221.2, 83.2%, 53.3%);
|
|
840
|
+
--primary-foreground: hsl(210, 40%, 98%);
|
|
841
|
+
--secondary: hsl(210, 40%, 96.1%);
|
|
842
|
+
--muted: hsl(210, 40%, 96.1%);
|
|
843
|
+
--muted-foreground: hsl(215.4, 16.3%, 46.9%);
|
|
844
|
+
--border: hsl(214.3, 31.8%, 91.4%);
|
|
845
|
+
--radius: 0.75rem;
|
|
846
|
+
}
|
|
847
|
+
* {
|
|
848
|
+
margin: 0;
|
|
849
|
+
padding: 0;
|
|
850
|
+
box-sizing: border-box;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
body {
|
|
854
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
855
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
|
856
|
+
min-height: 100vh;
|
|
857
|
+
padding: 2rem;
|
|
858
|
+
color: var(--foreground);
|
|
859
|
+
line-height: 1.6;
|
|
860
|
+
-webkit-font-smoothing: antialiased;
|
|
861
|
+
-moz-osx-font-smoothing: grayscale;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.container {
|
|
865
|
+
max-width: 1280px;
|
|
866
|
+
margin: 0 auto;
|
|
867
|
+
background: var(--background);
|
|
868
|
+
border-radius: 24px;
|
|
869
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
870
|
+
overflow: hidden;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
.header {
|
|
874
|
+
background: linear-gradient(135deg, var(--nitrocloud-gradient-start) 0%, var(--nitrocloud-gradient-end) 100%);
|
|
875
|
+
color: white;
|
|
876
|
+
padding: 4rem 2rem;
|
|
877
|
+
text-align: center;
|
|
878
|
+
position: relative;
|
|
879
|
+
overflow: hidden;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
.header::before {
|
|
883
|
+
content: '';
|
|
884
|
+
position: absolute;
|
|
885
|
+
top: 0;
|
|
886
|
+
left: 0;
|
|
887
|
+
right: 0;
|
|
888
|
+
bottom: 0;
|
|
889
|
+
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, transparent 100%);
|
|
890
|
+
pointer-events: none;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
.header > * {
|
|
894
|
+
position: relative;
|
|
895
|
+
z-index: 1;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
.logo-container {
|
|
899
|
+
margin-bottom: 2rem;
|
|
900
|
+
display: flex;
|
|
901
|
+
justify-content: center;
|
|
902
|
+
align-items: center;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
.logo {
|
|
906
|
+
height: 80px;
|
|
907
|
+
width: auto;
|
|
908
|
+
max-width: 200px;
|
|
909
|
+
object-fit: contain;
|
|
910
|
+
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.3));
|
|
911
|
+
transition: transform 0.3s ease;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
.logo:hover {
|
|
915
|
+
transform: scale(1.05);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
.header h1 {
|
|
919
|
+
font-size: 3rem;
|
|
920
|
+
font-weight: 700;
|
|
921
|
+
margin-bottom: 0.5rem;
|
|
922
|
+
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
923
|
+
letter-spacing: -0.025em;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
.header .version {
|
|
927
|
+
font-size: 1rem;
|
|
928
|
+
opacity: 0.95;
|
|
929
|
+
font-weight: 400;
|
|
930
|
+
letter-spacing: 0.05em;
|
|
931
|
+
text-transform: uppercase;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
.header .description {
|
|
935
|
+
margin-top: 1rem;
|
|
936
|
+
font-size: 1.125rem;
|
|
937
|
+
opacity: 0.95;
|
|
938
|
+
font-weight: 400;
|
|
939
|
+
max-width: 600px;
|
|
940
|
+
margin-left: auto;
|
|
941
|
+
margin-right: auto;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
.content {
|
|
945
|
+
padding: 3rem 2rem;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
.section {
|
|
949
|
+
margin-bottom: 4rem;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
.section:last-child {
|
|
953
|
+
margin-bottom: 0;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
.section h2 {
|
|
957
|
+
font-size: 2rem;
|
|
958
|
+
font-weight: 700;
|
|
959
|
+
color: var(--foreground);
|
|
960
|
+
margin-bottom: 1.5rem;
|
|
961
|
+
padding-bottom: 0.75rem;
|
|
962
|
+
border-bottom: 3px solid var(--nitrocloud-primary);
|
|
963
|
+
letter-spacing: -0.02em;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
.connection-info {
|
|
967
|
+
background: linear-gradient(to right, var(--secondary) 0%, var(--muted) 100%);
|
|
968
|
+
border-left: 4px solid var(--nitrocloud-primary);
|
|
969
|
+
padding: 2rem;
|
|
970
|
+
border-radius: var(--radius);
|
|
971
|
+
margin-bottom: 2rem;
|
|
972
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
.connection-info p {
|
|
976
|
+
font-weight: 600;
|
|
977
|
+
color: var(--foreground);
|
|
978
|
+
margin-bottom: 0.75rem;
|
|
979
|
+
font-size: 0.9375rem;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
.connection-info code {
|
|
983
|
+
background: hsl(222.2, 84%, 4.9%);
|
|
984
|
+
color: hsl(142, 76%, 36%);
|
|
985
|
+
padding: 1rem 1.25rem;
|
|
986
|
+
border-radius: 8px;
|
|
987
|
+
font-family: 'Monaco', 'Courier New', 'Menlo', monospace;
|
|
988
|
+
display: block;
|
|
989
|
+
margin-top: 0.75rem;
|
|
990
|
+
word-break: break-all;
|
|
991
|
+
font-size: 0.875rem;
|
|
992
|
+
line-height: 1.6;
|
|
993
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.connection-info .description {
|
|
997
|
+
margin-top: 1rem;
|
|
998
|
+
color: var(--muted-foreground);
|
|
999
|
+
font-size: 0.9375rem;
|
|
1000
|
+
line-height: 1.6;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
.tools-grid {
|
|
1004
|
+
display: grid;
|
|
1005
|
+
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
1006
|
+
gap: 1.5rem;
|
|
1007
|
+
margin-top: 1.5rem;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
.tool-card {
|
|
1011
|
+
background: var(--background);
|
|
1012
|
+
border: 2px solid var(--border);
|
|
1013
|
+
border-radius: var(--radius);
|
|
1014
|
+
padding: 1.75rem;
|
|
1015
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
1016
|
+
position: relative;
|
|
1017
|
+
overflow: hidden;
|
|
1018
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
.tool-card:hover {
|
|
1022
|
+
border-color: var(--nitrocloud-primary);
|
|
1023
|
+
box-shadow: 0 8px 24px rgba(59, 159, 255, 0.15);
|
|
1024
|
+
transform: translateY(-4px);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
.tool-card::before {
|
|
1028
|
+
content: '';
|
|
1029
|
+
position: absolute;
|
|
1030
|
+
top: 0;
|
|
1031
|
+
left: 0;
|
|
1032
|
+
right: 0;
|
|
1033
|
+
height: 4px;
|
|
1034
|
+
background: linear-gradient(90deg, var(--nitrocloud-gradient-start), var(--nitrocloud-gradient-end));
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.tool-name {
|
|
1038
|
+
font-size: 1.25rem;
|
|
1039
|
+
font-weight: 600;
|
|
1040
|
+
color: var(--foreground);
|
|
1041
|
+
margin-bottom: 0.75rem;
|
|
1042
|
+
display: flex;
|
|
1043
|
+
align-items: center;
|
|
1044
|
+
gap: 0.5rem;
|
|
1045
|
+
letter-spacing: -0.01em;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
.tool-name::before {
|
|
1049
|
+
content: '⚡';
|
|
1050
|
+
font-size: 1.25rem;
|
|
1051
|
+
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
.tool-description {
|
|
1055
|
+
color: var(--muted-foreground);
|
|
1056
|
+
margin-bottom: 1rem;
|
|
1057
|
+
line-height: 1.625;
|
|
1058
|
+
font-size: 0.9375rem;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
.tool-schema {
|
|
1062
|
+
background: var(--secondary);
|
|
1063
|
+
border-radius: 8px;
|
|
1064
|
+
padding: 1rem;
|
|
1065
|
+
margin-top: 1rem;
|
|
1066
|
+
font-size: 0.875rem;
|
|
1067
|
+
border: 1px solid var(--border);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
.tool-schema summary {
|
|
1071
|
+
cursor: pointer;
|
|
1072
|
+
font-weight: 600;
|
|
1073
|
+
color: var(--nitrocloud-primary);
|
|
1074
|
+
margin-bottom: 0.5rem;
|
|
1075
|
+
user-select: none;
|
|
1076
|
+
transition: color 0.2s;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
.tool-schema summary:hover {
|
|
1080
|
+
color: var(--nitrocloud-primary-dark);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
.tool-schema pre {
|
|
1084
|
+
background: hsl(222.2, 84%, 4.9%);
|
|
1085
|
+
color: hsl(142, 76%, 36%);
|
|
1086
|
+
padding: 1rem;
|
|
1087
|
+
border-radius: 6px;
|
|
1088
|
+
overflow-x: auto;
|
|
1089
|
+
margin-top: 0.75rem;
|
|
1090
|
+
font-size: 0.8125rem;
|
|
1091
|
+
line-height: 1.6;
|
|
1092
|
+
font-family: 'Monaco', 'Courier New', 'Menlo', monospace;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
.badge {
|
|
1096
|
+
display: inline-flex;
|
|
1097
|
+
align-items: center;
|
|
1098
|
+
padding: 0.375rem 0.75rem;
|
|
1099
|
+
border-radius: 12px;
|
|
1100
|
+
font-size: 0.75rem;
|
|
1101
|
+
font-weight: 600;
|
|
1102
|
+
margin-top: 0.5rem;
|
|
1103
|
+
transition: all 0.2s;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
.badge.widget {
|
|
1107
|
+
background: linear-gradient(135deg, hsl(271, 81%, 56%) 0%, hsl(271, 81%, 46%) 100%);
|
|
1108
|
+
color: white;
|
|
1109
|
+
box-shadow: 0 2px 8px rgba(196, 132, 252, 0.3);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
.empty-state {
|
|
1113
|
+
text-align: center;
|
|
1114
|
+
padding: 4rem 2rem;
|
|
1115
|
+
color: var(--muted-foreground);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
.empty-state svg {
|
|
1119
|
+
width: 64px;
|
|
1120
|
+
height: 64px;
|
|
1121
|
+
margin: 0 auto 1.5rem;
|
|
1122
|
+
opacity: 0.5;
|
|
1123
|
+
color: var(--muted-foreground);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
.empty-state p {
|
|
1127
|
+
font-size: 1rem;
|
|
1128
|
+
font-weight: 500;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
.footer {
|
|
1132
|
+
background: linear-gradient(to right, var(--secondary) 0%, var(--muted) 100%);
|
|
1133
|
+
padding: 2.5rem 2rem;
|
|
1134
|
+
text-align: center;
|
|
1135
|
+
color: var(--muted-foreground);
|
|
1136
|
+
border-top: 1px solid var(--border);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
.footer p {
|
|
1140
|
+
font-size: 0.9375rem;
|
|
1141
|
+
line-height: 1.6;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
.footer a {
|
|
1145
|
+
color: var(--nitrocloud-primary);
|
|
1146
|
+
text-decoration: none;
|
|
1147
|
+
font-weight: 600;
|
|
1148
|
+
transition: color 0.2s;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
.footer a:hover {
|
|
1152
|
+
color: var(--nitrocloud-primary-dark);
|
|
1153
|
+
text-decoration: underline;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
@media (max-width: 768px) {
|
|
1157
|
+
body {
|
|
1158
|
+
padding: 1rem;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
.header {
|
|
1162
|
+
padding: 3rem 1.5rem;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
.header h1 {
|
|
1166
|
+
font-size: 2.25rem;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
.content {
|
|
1170
|
+
padding: 2rem 1.5rem;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
.section h2 {
|
|
1174
|
+
font-size: 1.75rem;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
.tools-grid {
|
|
1178
|
+
grid-template-columns: 1fr;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
.connection-info {
|
|
1182
|
+
padding: 1.5rem;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
@media (prefers-color-scheme: dark) {
|
|
1187
|
+
:root {
|
|
1188
|
+
--background: hsl(222.2, 84%, 4.9%);
|
|
1189
|
+
--foreground: hsl(210, 40%, 98%);
|
|
1190
|
+
--primary: hsl(217, 91%, 60%);
|
|
1191
|
+
--secondary: hsl(217.2, 32.6%, 17.5%);
|
|
1192
|
+
--muted: hsl(217.2, 32.6%, 17.5%);
|
|
1193
|
+
--muted-foreground: hsl(215, 20.2%, 65.1%);
|
|
1194
|
+
--border: hsl(217.2, 32.6%, 17.5%);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
.connection-info code {
|
|
1198
|
+
background: hsl(217.2, 32.6%, 17.5%);
|
|
1199
|
+
color: hsl(142, 76%, 56%);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
.tool-schema pre {
|
|
1203
|
+
background: hsl(217.2, 32.6%, 17.5%);
|
|
1204
|
+
color: hsl(142, 76%, 56%);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
</style>
|
|
1208
|
+
</head>
|
|
1209
|
+
<body>
|
|
1210
|
+
<div class="container">
|
|
1211
|
+
<div class="header">
|
|
1212
|
+
${this.logoBase64 ? `
|
|
1213
|
+
<div class="logo-container">
|
|
1214
|
+
<img src="data:image/png;base64,${this.logoBase64}" alt="NitroCloud Logo" class="logo">
|
|
1215
|
+
</div>
|
|
1216
|
+
` : ''}
|
|
1217
|
+
<h1>${serverName}</h1>
|
|
1218
|
+
<div class="version">v${serverVersion}</div>
|
|
1219
|
+
<div class="description">${serverDescription}</div>
|
|
1220
|
+
</div>
|
|
1221
|
+
|
|
1222
|
+
<div class="content">
|
|
1223
|
+
<div class="section">
|
|
1224
|
+
<h2>🔌 Connection Information</h2>
|
|
1225
|
+
<div class="connection-info">
|
|
1226
|
+
<p>MCP Endpoint</p>
|
|
1227
|
+
<code>${mcpEndpoint}</code>
|
|
1228
|
+
<p class="description">
|
|
1229
|
+
Connect to this MCP server using the endpoint above. The server supports Server-Sent Events (SSE) for real-time bidirectional communication following the Model Context Protocol specification.
|
|
1230
|
+
</p>
|
|
1231
|
+
</div>
|
|
1232
|
+
</div>
|
|
1233
|
+
|
|
1234
|
+
<div class="section">
|
|
1235
|
+
<h2>🛠️ Available Tools</h2>
|
|
1236
|
+
${tools.length > 0 ? `
|
|
1237
|
+
<div class="tools-grid">
|
|
1238
|
+
${tools.map(tool => `
|
|
1239
|
+
<div class="tool-card">
|
|
1240
|
+
<div class="tool-name">${this.escapeHtml(tool.name)}</div>
|
|
1241
|
+
<div class="tool-description">${this.escapeHtml(tool.description || 'No description available')}</div>
|
|
1242
|
+
${tool.widget || tool.outputTemplate || tool._meta?.['openai/outputTemplate'] ? `
|
|
1243
|
+
<span class="badge widget">🎨 Has UI Widget</span>
|
|
1244
|
+
` : ''}
|
|
1245
|
+
${tool.inputSchema ? `
|
|
1246
|
+
<details class="tool-schema">
|
|
1247
|
+
<summary>Input Schema</summary>
|
|
1248
|
+
<pre>${this.escapeHtml(JSON.stringify(tool.inputSchema, null, 2))}</pre>
|
|
1249
|
+
</details>
|
|
1250
|
+
` : ''}
|
|
1251
|
+
</div>
|
|
1252
|
+
`).join('')}
|
|
1253
|
+
</div>
|
|
1254
|
+
` : `
|
|
1255
|
+
<div class="empty-state">
|
|
1256
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
1257
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
1258
|
+
</svg>
|
|
1259
|
+
<p>No tools are currently registered on this server.</p>
|
|
1260
|
+
</div>
|
|
1261
|
+
`}
|
|
1262
|
+
</div>
|
|
1263
|
+
</div>
|
|
1264
|
+
|
|
1265
|
+
<div class="footer">
|
|
1266
|
+
<p>Built with <a href="https://nitrostack.ai" target="_blank" rel="noopener noreferrer">NitroStack</a> - The TypeScript MCP Framework</p>
|
|
1267
|
+
<p style="margin-top: 0.5rem; font-size: 0.875rem;">Model Context Protocol Server</p>
|
|
1268
|
+
</div>
|
|
1269
|
+
</div>
|
|
1270
|
+
</body>
|
|
1271
|
+
</html>`;
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Escape HTML to prevent XSS
|
|
1275
|
+
*/
|
|
1276
|
+
escapeHtml(text) {
|
|
1277
|
+
const map = {
|
|
1278
|
+
'&': '&',
|
|
1279
|
+
'<': '<',
|
|
1280
|
+
'>': '>',
|
|
1281
|
+
'"': '"',
|
|
1282
|
+
"'": ''',
|
|
1283
|
+
};
|
|
1284
|
+
return text.replace(/[&<>"']/g, (m) => map[m]);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
//# sourceMappingURL=streamable-http.js.map
|