keywordgraph-mcp-server 1.6.1

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 (228) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +629 -0
  3. package/bin/infranodus-mcp-server.js +8 -0
  4. package/bin/keywordgraph-mcp-server.js +8 -0
  5. package/bin/mcp-server.js +56 -0
  6. package/dist/api/client.d.ts +3 -0
  7. package/dist/api/client.d.ts.map +1 -0
  8. package/dist/api/client.js +30 -0
  9. package/dist/api/client.js.map +1 -0
  10. package/dist/api/config-store.d.ts +4 -0
  11. package/dist/api/config-store.d.ts.map +1 -0
  12. package/dist/api/config-store.js +12 -0
  13. package/dist/api/config-store.js.map +1 -0
  14. package/dist/auth/oauth-provider.d.ts +56 -0
  15. package/dist/auth/oauth-provider.d.ts.map +1 -0
  16. package/dist/auth/oauth-provider.js +295 -0
  17. package/dist/auth/oauth-provider.js.map +1 -0
  18. package/dist/auth/types.d.ts +93 -0
  19. package/dist/auth/types.d.ts.map +1 -0
  20. package/dist/auth/types.js +5 -0
  21. package/dist/auth/types.js.map +1 -0
  22. package/dist/config/brand.d.ts +58 -0
  23. package/dist/config/brand.d.ts.map +1 -0
  24. package/dist/config/brand.js +97 -0
  25. package/dist/config/brand.js.map +1 -0
  26. package/dist/config/index.d.ts +26 -0
  27. package/dist/config/index.d.ts.map +1 -0
  28. package/dist/config/index.js +20 -0
  29. package/dist/config/index.js.map +1 -0
  30. package/dist/http-server.d.ts +6 -0
  31. package/dist/http-server.d.ts.map +1 -0
  32. package/dist/http-server.js +907 -0
  33. package/dist/http-server.js.map +1 -0
  34. package/dist/index.d.ts +29 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +136 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/instructions.d.ts +11 -0
  39. package/dist/instructions.d.ts.map +1 -0
  40. package/dist/instructions.js +38 -0
  41. package/dist/instructions.js.map +1 -0
  42. package/dist/prompts/index.d.ts +24 -0
  43. package/dist/prompts/index.d.ts.map +1 -0
  44. package/dist/prompts/index.js +394 -0
  45. package/dist/prompts/index.js.map +1 -0
  46. package/dist/resources/about.d.ts +17 -0
  47. package/dist/resources/about.d.ts.map +1 -0
  48. package/dist/resources/about.js +64 -0
  49. package/dist/resources/about.js.map +1 -0
  50. package/dist/resources/llms-txt.d.ts +33 -0
  51. package/dist/resources/llms-txt.d.ts.map +1 -0
  52. package/dist/resources/llms-txt.js +65 -0
  53. package/dist/resources/llms-txt.js.map +1 -0
  54. package/dist/schemas/index.d.ts +1178 -0
  55. package/dist/schemas/index.d.ts.map +1 -0
  56. package/dist/schemas/index.js +1121 -0
  57. package/dist/schemas/index.js.map +1 -0
  58. package/dist/tools/addMemory.d.ts +36 -0
  59. package/dist/tools/addMemory.d.ts.map +1 -0
  60. package/dist/tools/addMemory.js +82 -0
  61. package/dist/tools/addMemory.js.map +1 -0
  62. package/dist/tools/analyzeExistingGraph.d.ts +37 -0
  63. package/dist/tools/analyzeExistingGraph.d.ts.map +1 -0
  64. package/dist/tools/analyzeExistingGraph.js +85 -0
  65. package/dist/tools/analyzeExistingGraph.js.map +1 -0
  66. package/dist/tools/analyzeLlmResults.d.ts +37 -0
  67. package/dist/tools/analyzeLlmResults.d.ts.map +1 -0
  68. package/dist/tools/analyzeLlmResults.js +165 -0
  69. package/dist/tools/analyzeLlmResults.js.map +1 -0
  70. package/dist/tools/analyzeText.d.ts +36 -0
  71. package/dist/tools/analyzeText.d.ts.map +1 -0
  72. package/dist/tools/analyzeText.js +97 -0
  73. package/dist/tools/analyzeText.js.map +1 -0
  74. package/dist/tools/createKnowledgeGraph.d.ts +36 -0
  75. package/dist/tools/createKnowledgeGraph.d.ts.map +1 -0
  76. package/dist/tools/createKnowledgeGraph.js +105 -0
  77. package/dist/tools/createKnowledgeGraph.js.map +1 -0
  78. package/dist/tools/developConceptualBridges.d.ts +34 -0
  79. package/dist/tools/developConceptualBridges.d.ts.map +1 -0
  80. package/dist/tools/developConceptualBridges.js +107 -0
  81. package/dist/tools/developConceptualBridges.js.map +1 -0
  82. package/dist/tools/developLatentConcepts copy.d.ts +27 -0
  83. package/dist/tools/developLatentConcepts copy.d.ts.map +1 -0
  84. package/dist/tools/developLatentConcepts copy.js +68 -0
  85. package/dist/tools/developLatentConcepts copy.js.map +1 -0
  86. package/dist/tools/developLatentConcepts.d.ts +27 -0
  87. package/dist/tools/developLatentConcepts.d.ts.map +1 -0
  88. package/dist/tools/developLatentConcepts.js +68 -0
  89. package/dist/tools/developLatentConcepts.js.map +1 -0
  90. package/dist/tools/developLatentTopics.d.ts +34 -0
  91. package/dist/tools/developLatentTopics.d.ts.map +1 -0
  92. package/dist/tools/developLatentTopics.js +107 -0
  93. package/dist/tools/developLatentTopics.js.map +1 -0
  94. package/dist/tools/developTextTool.d.ts +39 -0
  95. package/dist/tools/developTextTool.d.ts.map +1 -0
  96. package/dist/tools/developTextTool.js +210 -0
  97. package/dist/tools/developTextTool.js.map +1 -0
  98. package/dist/tools/generateContentGaps.d.ts +32 -0
  99. package/dist/tools/generateContentGaps.d.ts.map +1 -0
  100. package/dist/tools/generateContentGaps.js +96 -0
  101. package/dist/tools/generateContentGaps.js.map +1 -0
  102. package/dist/tools/generateContextualHint.d.ts +33 -0
  103. package/dist/tools/generateContextualHint.d.ts.map +1 -0
  104. package/dist/tools/generateContextualHint.js +99 -0
  105. package/dist/tools/generateContextualHint.js.map +1 -0
  106. package/dist/tools/generateDifferenceGraphFromTexts.d.ts +52 -0
  107. package/dist/tools/generateDifferenceGraphFromTexts.d.ts.map +1 -0
  108. package/dist/tools/generateDifferenceGraphFromTexts.js +160 -0
  109. package/dist/tools/generateDifferenceGraphFromTexts.js.map +1 -0
  110. package/dist/tools/generateGoogleResultsVsQueriesGraph.d.ts +37 -0
  111. package/dist/tools/generateGoogleResultsVsQueriesGraph.d.ts.map +1 -0
  112. package/dist/tools/generateGoogleResultsVsQueriesGraph.js +84 -0
  113. package/dist/tools/generateGoogleResultsVsQueriesGraph.js.map +1 -0
  114. package/dist/tools/generateGoogleSearchQueriesGraph.d.ts +39 -0
  115. package/dist/tools/generateGoogleSearchQueriesGraph.d.ts.map +1 -0
  116. package/dist/tools/generateGoogleSearchQueriesGraph.js +88 -0
  117. package/dist/tools/generateGoogleSearchQueriesGraph.js.map +1 -0
  118. package/dist/tools/generateGoogleSearchResultsGraph.d.ts +38 -0
  119. package/dist/tools/generateGoogleSearchResultsGraph.d.ts.map +1 -0
  120. package/dist/tools/generateGoogleSearchResultsGraph.js +88 -0
  121. package/dist/tools/generateGoogleSearchResultsGraph.js.map +1 -0
  122. package/dist/tools/generateKnowledgeGraph.d.ts +35 -0
  123. package/dist/tools/generateKnowledgeGraph.d.ts.map +1 -0
  124. package/dist/tools/generateKnowledgeGraph.js +105 -0
  125. package/dist/tools/generateKnowledgeGraph.js.map +1 -0
  126. package/dist/tools/generateMergedGraphFromTexts.d.ts +52 -0
  127. package/dist/tools/generateMergedGraphFromTexts.d.ts.map +1 -0
  128. package/dist/tools/generateMergedGraphFromTexts.js +160 -0
  129. package/dist/tools/generateMergedGraphFromTexts.js.map +1 -0
  130. package/dist/tools/generateOntologyGraph.d.ts +36 -0
  131. package/dist/tools/generateOntologyGraph.d.ts.map +1 -0
  132. package/dist/tools/generateOntologyGraph.js +171 -0
  133. package/dist/tools/generateOntologyGraph.js.map +1 -0
  134. package/dist/tools/generateOverlapGraphFromTexts.d.ts +52 -0
  135. package/dist/tools/generateOverlapGraphFromTexts.d.ts.map +1 -0
  136. package/dist/tools/generateOverlapGraphFromTexts.js +159 -0
  137. package/dist/tools/generateOverlapGraphFromTexts.js.map +1 -0
  138. package/dist/tools/generateResearchIdeas.d.ts +39 -0
  139. package/dist/tools/generateResearchIdeas.d.ts.map +1 -0
  140. package/dist/tools/generateResearchIdeas.js +117 -0
  141. package/dist/tools/generateResearchIdeas.js.map +1 -0
  142. package/dist/tools/generateResearchQuestions.d.ts +37 -0
  143. package/dist/tools/generateResearchQuestions.d.ts.map +1 -0
  144. package/dist/tools/generateResearchQuestions.js +111 -0
  145. package/dist/tools/generateResearchQuestions.js.map +1 -0
  146. package/dist/tools/generateResearchQuestionsFromGraph.d.ts +34 -0
  147. package/dist/tools/generateResearchQuestionsFromGraph.d.ts.map +1 -0
  148. package/dist/tools/generateResearchQuestionsFromGraph.js +75 -0
  149. package/dist/tools/generateResearchQuestionsFromGraph.js.map +1 -0
  150. package/dist/tools/generateResponses.d.ts +35 -0
  151. package/dist/tools/generateResponses.d.ts.map +1 -0
  152. package/dist/tools/generateResponses.js +112 -0
  153. package/dist/tools/generateResponses.js.map +1 -0
  154. package/dist/tools/generateResponsesFromGraph.d.ts +33 -0
  155. package/dist/tools/generateResponsesFromGraph.d.ts.map +1 -0
  156. package/dist/tools/generateResponsesFromGraph.js +74 -0
  157. package/dist/tools/generateResponsesFromGraph.js.map +1 -0
  158. package/dist/tools/generateSEOGraphTool.d.ts +39 -0
  159. package/dist/tools/generateSEOGraphTool.d.ts.map +1 -0
  160. package/dist/tools/generateSEOGraphTool.js +235 -0
  161. package/dist/tools/generateSEOGraphTool.js.map +1 -0
  162. package/dist/tools/generateTextOverview.d.ts +31 -0
  163. package/dist/tools/generateTextOverview.d.ts.map +1 -0
  164. package/dist/tools/generateTextOverview.js +68 -0
  165. package/dist/tools/generateTextOverview.js.map +1 -0
  166. package/dist/tools/generateTopicalClusters.d.ts +34 -0
  167. package/dist/tools/generateTopicalClusters.d.ts.map +1 -0
  168. package/dist/tools/generateTopicalClusters.js +134 -0
  169. package/dist/tools/generateTopicalClusters.js.map +1 -0
  170. package/dist/tools/getMemory.d.ts +32 -0
  171. package/dist/tools/getMemory.d.ts.map +1 -0
  172. package/dist/tools/getMemory.js +121 -0
  173. package/dist/tools/getMemory.js.map +1 -0
  174. package/dist/tools/index.d.ts +30 -0
  175. package/dist/tools/index.d.ts.map +1 -0
  176. package/dist/tools/index.js +30 -0
  177. package/dist/tools/index.js.map +1 -0
  178. package/dist/tools/listGraphs.d.ts +37 -0
  179. package/dist/tools/listGraphs.d.ts.map +1 -0
  180. package/dist/tools/listGraphs.js +127 -0
  181. package/dist/tools/listGraphs.js.map +1 -0
  182. package/dist/tools/optimizeTextStructure.d.ts +34 -0
  183. package/dist/tools/optimizeTextStructure.d.ts.map +1 -0
  184. package/dist/tools/optimizeTextStructure.js +108 -0
  185. package/dist/tools/optimizeTextStructure.js.map +1 -0
  186. package/dist/tools/retrieveContextForPromptFromGraph.d.ts +37 -0
  187. package/dist/tools/retrieveContextForPromptFromGraph.d.ts.map +1 -0
  188. package/dist/tools/retrieveContextForPromptFromGraph.js +80 -0
  189. package/dist/tools/retrieveContextForPromptFromGraph.js.map +1 -0
  190. package/dist/tools/searchGraph.d.ts +27 -0
  191. package/dist/tools/searchGraph.d.ts.map +1 -0
  192. package/dist/tools/searchGraph.js +57 -0
  193. package/dist/tools/searchGraph.js.map +1 -0
  194. package/dist/tools/searchGraphs.d.ts +33 -0
  195. package/dist/tools/searchGraphs.d.ts.map +1 -0
  196. package/dist/tools/searchGraphs.js +67 -0
  197. package/dist/tools/searchGraphs.js.map +1 -0
  198. package/dist/tools/searchGraphsFetch.d.ts +31 -0
  199. package/dist/tools/searchGraphsFetch.d.ts.map +1 -0
  200. package/dist/tools/searchGraphsFetch.js +78 -0
  201. package/dist/tools/searchGraphsFetch.js.map +1 -0
  202. package/dist/tools/sequentialThinking.d.ts +71 -0
  203. package/dist/tools/sequentialThinking.d.ts.map +1 -0
  204. package/dist/tools/sequentialThinking.js +218 -0
  205. package/dist/tools/sequentialThinking.js.map +1 -0
  206. package/dist/types/index.d.ts +378 -0
  207. package/dist/types/index.d.ts.map +1 -0
  208. package/dist/types/index.js +2 -0
  209. package/dist/types/index.js.map +1 -0
  210. package/dist/utils/progress.d.ts +21 -0
  211. package/dist/utils/progress.d.ts.map +1 -0
  212. package/dist/utils/progress.js +50 -0
  213. package/dist/utils/progress.js.map +1 -0
  214. package/dist/utils/transformers.d.ts +31 -0
  215. package/dist/utils/transformers.d.ts.map +1 -0
  216. package/dist/utils/transformers.js +484 -0
  217. package/dist/utils/transformers.js.map +1 -0
  218. package/dist/utils/urlContent.d.ts +35 -0
  219. package/dist/utils/urlContent.d.ts.map +1 -0
  220. package/dist/utils/urlContent.js +87 -0
  221. package/dist/utils/urlContent.js.map +1 -0
  222. package/llms-full.infranodus.txt +469 -0
  223. package/llms-full.keywordgraph.txt +469 -0
  224. package/llms-full.txt +469 -0
  225. package/llms.infranodus.txt +64 -0
  226. package/llms.keywordgraph.txt +64 -0
  227. package/llms.txt +64 -0
  228. package/package.json +78 -0
@@ -0,0 +1,907 @@
1
+ /**
2
+ * HTTP Server entry point for the MCP Server
3
+ * Exposes the MCP server over HTTP with OAuth2-style authentication
4
+ */
5
+ import express from "express";
6
+ import cors from "cors";
7
+ import helmet from "helmet";
8
+ import crypto from "crypto";
9
+ import * as dotenv from "dotenv";
10
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
11
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
12
+ import createServer from "./index.js";
13
+ import { exchangeApiKeyForToken, verifyAccessToken, revokeSession, getSessionCount, registerClient, getClient, createAuthorizationCode, exchangeAuthorizationCode, validateApiKey, } from "./auth/oauth-provider.js";
14
+ import * as http from "http";
15
+ import { Socket } from "net";
16
+ import * as fs from "fs";
17
+ import * as path from "path";
18
+ import { fileURLToPath } from "url";
19
+ import { brand, brandApiBase } from "./config/brand.js";
20
+ // Load environment variables
21
+ dotenv.config();
22
+ // Configuration
23
+ const PORT = parseInt(process.env.PORT || "3000", 10);
24
+ const CORS_ORIGIN = process.env.CORS_ORIGIN || "*";
25
+ const BRAND_API_BASE = brandApiBase();
26
+ function escapeHtml(str) {
27
+ return str
28
+ .replace(/&/g, "&")
29
+ .replace(/</g, "&lt;")
30
+ .replace(/>/g, "&gt;")
31
+ .replace(/"/g, "&quot;")
32
+ .replace(/'/g, "&#39;");
33
+ }
34
+ // Allow HTTP only for localhost/127.0.0.1 (local dev); otherwise require HTTPS
35
+ function isRedirectUriAllowed(redirectUri) {
36
+ if (redirectUri.startsWith("https://"))
37
+ return true;
38
+ if (!redirectUri.startsWith("http://"))
39
+ return false;
40
+ try {
41
+ const u = new URL(redirectUri);
42
+ const host = u.hostname.toLowerCase();
43
+ return host === "localhost" || host === "127.0.0.1";
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ // Store auth info on request (using a symbol to avoid conflicts with MCP SDK)
50
+ const AUTH_KEY = Symbol("auth");
51
+ // Create Express app
52
+ const app = express();
53
+ // Trust proxy for correct protocol detection behind reverse proxies (Fly.io, etc.)
54
+ app.set("trust proxy", true);
55
+ // Middleware
56
+ app.use(helmet({
57
+ contentSecurityPolicy: false, // Disable for SSE compatibility
58
+ }));
59
+ app.use(cors({
60
+ origin: CORS_ORIGIN,
61
+ credentials: true,
62
+ }));
63
+ app.use(express.json());
64
+ // Log ALL incoming requests
65
+ app.use((req, res, next) => {
66
+ console.log(`[HTTP] ${req.method} ${req.path} from ${req.ip}`);
67
+ console.log(`[HTTP] Headers: ${JSON.stringify({
68
+ authorization: req.headers.authorization ? "[present]" : "[missing]",
69
+ "content-type": req.headers["content-type"],
70
+ accept: req.headers.accept,
71
+ })}`);
72
+ next();
73
+ });
74
+ // Store transports per session for MCP (Streamable HTTP)
75
+ const transports = new Map();
76
+ // Store SSE transports per session for legacy MCP clients
77
+ // userId is stored alongside the transport to prevent session hijacking
78
+ const sseTransports = new Map();
79
+ /**
80
+ * Auth middleware - validates Bearer token and attaches auth info to request.
81
+ * Supports both JWT access tokens (from OAuth flow) and raw API keys.
82
+ */
83
+ async function authMiddleware(req, res, next) {
84
+ console.log(`[AUTH] Checking auth for ${req.method} ${req.path}`);
85
+ const authHeader = req.headers.authorization;
86
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
87
+ console.log(`[AUTH] Missing or invalid Authorization header`);
88
+ const baseUrl = `${req.protocol}://${req.get("host")}`;
89
+ res.setHeader("WWW-Authenticate", `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"`);
90
+ const error = {
91
+ error: "unauthorized",
92
+ error_description: "Missing or invalid Authorization header",
93
+ };
94
+ res.status(401).json(error);
95
+ return;
96
+ }
97
+ const token = authHeader.slice(7); // Remove "Bearer " prefix
98
+ console.log(`[AUTH] Token present, verifying...`);
99
+ // Try JWT verification first (fast path for OAuth-authenticated clients)
100
+ const authInfo = verifyAccessToken(token);
101
+ if (authInfo) {
102
+ console.log(`[AUTH] Authenticated via JWT as ${authInfo.userName} (${authInfo.userId})`);
103
+ req[AUTH_KEY] = authInfo;
104
+ next();
105
+ return;
106
+ }
107
+ // JWT failed — try treating the token as a raw API key
108
+ console.log(`[AUTH] JWT verification failed, trying as raw API key...`);
109
+ try {
110
+ const userInfo = await validateApiKey(token);
111
+ if (userInfo) {
112
+ console.log(`[AUTH] Authenticated via raw API key as ${userInfo.userName} (${userInfo.userId})`);
113
+ req[AUTH_KEY] = {
114
+ userId: userInfo.userId,
115
+ userName: userInfo.userName,
116
+ apiKey: token,
117
+ sessionId: crypto.randomUUID(),
118
+ };
119
+ next();
120
+ return;
121
+ }
122
+ }
123
+ catch (err) {
124
+ console.log(`[AUTH] Raw API key validation error: ${err}`);
125
+ }
126
+ console.log(`[AUTH] All authentication methods failed`);
127
+ const baseUrl = `${req.protocol}://${req.get("host")}`;
128
+ res.setHeader("WWW-Authenticate", `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"`);
129
+ const error = {
130
+ error: "invalid_token",
131
+ error_description: "Access token is invalid or expired",
132
+ };
133
+ res.status(401).json(error);
134
+ }
135
+ /**
136
+ * Helper to get auth info from request
137
+ */
138
+ function getAuth(req) {
139
+ return req[AUTH_KEY];
140
+ }
141
+ // ============================================================================
142
+ // OAuth2 Endpoints
143
+ // ============================================================================
144
+ /**
145
+ * POST /oauth/register - Dynamic Client Registration (RFC 7591)
146
+ */
147
+ app.post("/oauth/register", (req, res) => {
148
+ try {
149
+ const body = req.body;
150
+ if (!body.redirect_uris ||
151
+ !Array.isArray(body.redirect_uris) ||
152
+ body.redirect_uris.length === 0) {
153
+ res.status(400).json({
154
+ error: "invalid_request",
155
+ error_description: "redirect_uris is required and must be a non-empty array",
156
+ });
157
+ return;
158
+ }
159
+ const clientResponse = registerClient(body);
160
+ res.status(201).json(clientResponse);
161
+ }
162
+ catch (error) {
163
+ console.error("Client registration error:", error);
164
+ res.status(500).json({
165
+ error: "server_error",
166
+ error_description: "Internal server error",
167
+ });
168
+ }
169
+ });
170
+ /**
171
+ * GET /oauth/authorize - Authorization endpoint (shows form)
172
+ */
173
+ app.get("/oauth/authorize", (req, res) => {
174
+ const { response_type, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method, } = req.query;
175
+ // Validate required parameters
176
+ if (response_type !== "code") {
177
+ res.status(400).json({ error: "unsupported_response_type" });
178
+ return;
179
+ }
180
+ if (!client_id || typeof client_id !== "string") {
181
+ res.status(400).json({
182
+ error: "invalid_request",
183
+ error_description: "client_id is required",
184
+ });
185
+ return;
186
+ }
187
+ // Accept any UUID-formatted client_id (stateless - no need for pre-registration)
188
+ // This is necessary because client registrations don't persist across instances/restarts
189
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
190
+ if (!uuidRegex.test(client_id)) {
191
+ res.status(400).json({
192
+ error: "invalid_client",
193
+ error_description: "Invalid client_id format",
194
+ });
195
+ return;
196
+ }
197
+ if (!redirect_uri || typeof redirect_uri !== "string") {
198
+ res.status(400).json({
199
+ error: "invalid_request",
200
+ error_description: "redirect_uri is required",
201
+ });
202
+ return;
203
+ }
204
+ // Accept HTTPS or http://localhost / http://127.0.0.1 (for local dev)
205
+ if (!isRedirectUriAllowed(redirect_uri)) {
206
+ res.status(400).json({
207
+ error: "invalid_request",
208
+ error_description: "redirect_uri must use HTTPS or http://localhost or http://127.0.0.1",
209
+ });
210
+ return;
211
+ }
212
+ // Get client name from registration if available, otherwise use client_id
213
+ const client = getClient(client_id);
214
+ const clientName = escapeHtml(client?.client_name || client_id);
215
+ const safeClientId = escapeHtml(client_id);
216
+ const safeRedirectUri = escapeHtml(redirect_uri);
217
+ const safeState = escapeHtml(String(state || ""));
218
+ const safeScope = escapeHtml(String(scope || ""));
219
+ const safeCodeChallenge = escapeHtml(String(code_challenge || ""));
220
+ const safeCodeChallengeMethod = escapeHtml(String(code_challenge_method || ""));
221
+ // Render a simple HTML authorization form
222
+ const html = `
223
+ <!DOCTYPE html>
224
+ <html>
225
+ <head>
226
+ <title>${brand.name} MCP - Authorize</title>
227
+ <meta name="viewport" content="width=device-width, initial-scale=1">
228
+ <style>
229
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 400px; margin: 50px auto; padding: 20px; }
230
+ h1 { color: #333; font-size: 24px; }
231
+ .client-name { color: #666; margin-bottom: 20px; }
232
+ label { display: block; margin: 15px 0 5px; font-weight: 500; }
233
+ input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
234
+ button { width: 100%; padding: 12px; background: #4F46E5; color: white; border: none; border-radius: 4px; font-size: 16px; cursor: pointer; margin-top: 20px; }
235
+ button:hover { background: #4338CA; }
236
+ .info { background: #F3F4F6; padding: 15px; border-radius: 4px; margin-bottom: 20px; font-size: 14px; color: #666; }
237
+ </style>
238
+ </head>
239
+ <body>
240
+ <h1>Authorize Access</h1>
241
+ <p class="client-name">Application: <strong>${clientName}</strong></p>
242
+ <div class="info">
243
+ Enter your ${brand.name} API key to authorize this application to access your ${brand.name} data.
244
+ </div>
245
+ <form method="POST" action="/oauth/authorize">
246
+ <input type="hidden" name="client_id" value="${safeClientId}">
247
+ <input type="hidden" name="redirect_uri" value="${safeRedirectUri}">
248
+ <input type="hidden" name="response_type" value="code">
249
+ <input type="hidden" name="state" value="${safeState}">
250
+ <input type="hidden" name="scope" value="${safeScope}">
251
+ <input type="hidden" name="code_challenge" value="${safeCodeChallenge}">
252
+ <input type="hidden" name="code_challenge_method" value="${safeCodeChallengeMethod}">
253
+ <label for="api_key">${brand.name} API Key</label>
254
+ <input type="password" id="api_key" name="api_key" required placeholder="Enter your API key">
255
+ <button type="submit">Authorize</button>
256
+ </form>
257
+ </body>
258
+ </html>`;
259
+ res.type("html").send(html);
260
+ });
261
+ /**
262
+ * POST /oauth/authorize - Process authorization (form submission)
263
+ */
264
+ app.post("/oauth/authorize", express.urlencoded({ extended: true }), async (req, res) => {
265
+ const { client_id, redirect_uri, response_type, state, scope, code_challenge, code_challenge_method, api_key, } = req.body;
266
+ // Validate client_id format (stateless - accept any valid UUID)
267
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
268
+ if (!client_id || !uuidRegex.test(client_id)) {
269
+ res.status(400).json({ error: "invalid_client" });
270
+ return;
271
+ }
272
+ // Validate redirect_uri (HTTPS or localhost/127.0.0.1 for local dev)
273
+ if (!redirect_uri || !isRedirectUriAllowed(redirect_uri)) {
274
+ res.status(400).json({
275
+ error: "invalid_request",
276
+ error_description: "Invalid redirect_uri",
277
+ });
278
+ return;
279
+ }
280
+ // Validate API key
281
+ if (!api_key) {
282
+ const redirectUrl = new URL(redirect_uri);
283
+ redirectUrl.searchParams.set("error", "access_denied");
284
+ redirectUrl.searchParams.set("error_description", "API key is required");
285
+ if (state)
286
+ redirectUrl.searchParams.set("state", state);
287
+ res.redirect(redirectUrl.toString());
288
+ return;
289
+ }
290
+ const userInfo = await validateApiKey(api_key);
291
+ if (!userInfo) {
292
+ const redirectUrl = new URL(redirect_uri);
293
+ redirectUrl.searchParams.set("error", "access_denied");
294
+ redirectUrl.searchParams.set("error_description", "Invalid API key");
295
+ if (state)
296
+ redirectUrl.searchParams.set("state", state);
297
+ res.redirect(redirectUrl.toString());
298
+ return;
299
+ }
300
+ // Create authorization code
301
+ const code = createAuthorizationCode(client_id, redirect_uri, api_key, scope, code_challenge, code_challenge_method);
302
+ // Redirect with code
303
+ const redirectUrl = new URL(redirect_uri);
304
+ redirectUrl.searchParams.set("code", code);
305
+ if (state)
306
+ redirectUrl.searchParams.set("state", state);
307
+ res.redirect(redirectUrl.toString());
308
+ });
309
+ /**
310
+ * POST /oauth/token - Exchange authorization code or API key for access token
311
+ */
312
+ app.post("/oauth/token", express.urlencoded({ extended: true }), async (req, res) => {
313
+ try {
314
+ let { grant_type, code, redirect_uri, client_id, client_secret, code_verifier, api_key, } = req.body;
315
+ // Support client_secret_basic authentication (credentials in Authorization header)
316
+ const authHeader = req.headers.authorization;
317
+ if (authHeader && authHeader.startsWith("Basic ")) {
318
+ const base64Credentials = authHeader.slice(6);
319
+ const credentials = Buffer.from(base64Credentials, "base64").toString("utf-8");
320
+ const [headerClientId, headerClientSecret] = credentials.split(":");
321
+ if (headerClientId)
322
+ client_id = headerClientId;
323
+ if (headerClientSecret)
324
+ client_secret = headerClientSecret;
325
+ }
326
+ // Handle authorization_code grant
327
+ if (grant_type === "authorization_code") {
328
+ if (!code || !redirect_uri || !client_id) {
329
+ res.status(400).json({
330
+ error: "invalid_request",
331
+ error_description: "Missing required parameters for authorization_code grant",
332
+ });
333
+ return;
334
+ }
335
+ // Note: We skip strict client validation here because:
336
+ // 1. Multiple server instances don't share client registration state
337
+ // 2. The real authentication is via the API key in the auth code
338
+ // 3. The authorization code itself validates the client_id and redirect_uri
339
+ const tokenResponse = await exchangeAuthorizationCode(code, client_id, redirect_uri, code_verifier);
340
+ if (!tokenResponse) {
341
+ res.status(400).json({
342
+ error: "invalid_grant",
343
+ error_description: "Invalid or expired authorization code",
344
+ });
345
+ return;
346
+ }
347
+ res.json(tokenResponse);
348
+ return;
349
+ }
350
+ // Handle direct API key exchange (for backward compatibility)
351
+ if (api_key || (!grant_type && req.body.api_key)) {
352
+ const key = api_key || req.body.api_key;
353
+ const tokenResponse = await exchangeApiKeyForToken(key);
354
+ if (!tokenResponse) {
355
+ res.status(401).json({
356
+ error: "invalid_grant",
357
+ error_description: `Invalid ${brand.name} API key`,
358
+ });
359
+ return;
360
+ }
361
+ res.json(tokenResponse);
362
+ return;
363
+ }
364
+ res.status(400).json({
365
+ error: "unsupported_grant_type",
366
+ error_description: "Supported grant types: authorization_code, api_key",
367
+ });
368
+ }
369
+ catch (error) {
370
+ console.error("Token exchange error:", error);
371
+ res.status(500).json({
372
+ error: "server_error",
373
+ error_description: "Internal server error",
374
+ });
375
+ }
376
+ });
377
+ /**
378
+ * POST /oauth/revoke - Revoke an access token
379
+ */
380
+ app.post("/oauth/revoke", authMiddleware, (req, res) => {
381
+ const auth = getAuth(req);
382
+ if (auth) {
383
+ revokeSession(auth.sessionId);
384
+ }
385
+ res.status(200).json({ revoked: true });
386
+ });
387
+ /**
388
+ * GET /.well-known/oauth-authorization-server - OAuth2 Authorization Server Metadata (RFC 8414)
389
+ */
390
+ app.get("/.well-known/oauth-authorization-server", (req, res) => {
391
+ const baseUrl = `${req.protocol}://${req.get("host")}`;
392
+ res.json({
393
+ issuer: baseUrl,
394
+ authorization_endpoint: `${baseUrl}/oauth/authorize`,
395
+ token_endpoint: `${baseUrl}/oauth/token`,
396
+ registration_endpoint: `${baseUrl}/oauth/register`,
397
+ revocation_endpoint: `${baseUrl}/oauth/revoke`,
398
+ token_endpoint_auth_methods_supported: [
399
+ "client_secret_basic",
400
+ "client_secret_post",
401
+ "none",
402
+ ],
403
+ grant_types_supported: ["authorization_code", "refresh_token"],
404
+ response_types_supported: ["code"],
405
+ code_challenge_methods_supported: ["S256", "plain"],
406
+ scopes_supported: ["mcp", "read", "write"],
407
+ service_documentation: `${baseUrl}/`,
408
+ });
409
+ });
410
+ /**
411
+ * GET /.well-known/openid-configuration - OpenID Connect Discovery (some clients check this)
412
+ */
413
+ app.get("/.well-known/openid-configuration", (req, res) => {
414
+ const baseUrl = `${req.protocol}://${req.get("host")}`;
415
+ res.json({
416
+ issuer: baseUrl,
417
+ authorization_endpoint: `${baseUrl}/oauth/authorize`,
418
+ token_endpoint: `${baseUrl}/oauth/token`,
419
+ registration_endpoint: `${baseUrl}/oauth/register`,
420
+ revocation_endpoint: `${baseUrl}/oauth/revoke`,
421
+ token_endpoint_auth_methods_supported: [
422
+ "client_secret_basic",
423
+ "client_secret_post",
424
+ "none",
425
+ ],
426
+ grant_types_supported: ["authorization_code", "refresh_token"],
427
+ response_types_supported: ["code"],
428
+ code_challenge_methods_supported: ["S256", "plain"],
429
+ scopes_supported: ["mcp", "read", "write"],
430
+ });
431
+ });
432
+ /**
433
+ * GET /.well-known/oauth-protected-resource - OAuth2 Protected Resource Metadata (RFC 9728)
434
+ * MCP clients fetch this to discover the authorization server for this resource.
435
+ */
436
+ app.get("/.well-known/oauth-protected-resource", (req, res) => {
437
+ const baseUrl = `${req.protocol}://${req.get("host")}`;
438
+ res.json({
439
+ resource: baseUrl,
440
+ authorization_servers: [baseUrl],
441
+ scopes_supported: ["mcp", "read", "write"],
442
+ bearer_methods_supported: ["header"],
443
+ });
444
+ });
445
+ // ============================================================================
446
+ // MCP Endpoints
447
+ // ============================================================================
448
+ /**
449
+ * Send a synthetic MCP request through a transport (used for auto-initialization).
450
+ * Creates mock HTTP objects, passes the body, and discards the response.
451
+ */
452
+ async function sendSyntheticRequest(transport, body, headers = {}) {
453
+ const method = body?.method || "unknown";
454
+ console.log(`[MCP:auto-init] Sending synthetic "${method}" request`);
455
+ const socket = new Socket();
456
+ socket.on("error", (err) => {
457
+ console.log(`[MCP:auto-init] Socket error (ignored): ${err.message}`);
458
+ });
459
+ const mockReq = new http.IncomingMessage(socket);
460
+ mockReq.method = "POST";
461
+ mockReq.headers = {
462
+ "content-type": "application/json",
463
+ accept: "application/json, text/event-stream",
464
+ ...headers,
465
+ };
466
+ const mockRes = new http.ServerResponse(mockReq);
467
+ const discardSocket = new Socket();
468
+ discardSocket.on("error", (err) => {
469
+ console.log(`[MCP:auto-init] Discard socket error (ignored): ${err.message}`);
470
+ });
471
+ mockRes.assignSocket(discardSocket);
472
+ let responseBody = "";
473
+ const originalEnd = mockRes.end.bind(mockRes);
474
+ mockRes.end = (chunk, ...args) => {
475
+ if (chunk) {
476
+ responseBody += typeof chunk === "string" ? chunk : chunk.toString();
477
+ }
478
+ console.log(`[MCP:auto-init] "${method}" response: status=${mockRes.statusCode} body=${responseBody.slice(0, 300)}`);
479
+ return originalEnd(chunk, ...args);
480
+ };
481
+ try {
482
+ await transport.handleRequest(mockReq, mockRes, body);
483
+ console.log(`[MCP:auto-init] "${method}" handleRequest completed`);
484
+ }
485
+ catch (e) {
486
+ console.error(`[MCP:auto-init] "${method}" handleRequest threw:`, e);
487
+ }
488
+ socket.destroy();
489
+ discardSocket.destroy();
490
+ }
491
+ /**
492
+ * Auto-initialize a newly created transport by sending synthetic initialize
493
+ * and initialized messages. Used when a client resumes a stale session
494
+ * (e.g. after server restart) without re-sending initialize.
495
+ */
496
+ async function autoInitializeTransport(transport, sessionId) {
497
+ await sendSyntheticRequest(transport, {
498
+ jsonrpc: "2.0",
499
+ method: "initialize",
500
+ id: "auto-init",
501
+ params: {
502
+ protocolVersion: "2025-03-26",
503
+ clientInfo: { name: "auto-reinit", version: "1.0.0" },
504
+ capabilities: {},
505
+ },
506
+ });
507
+ await sendSyntheticRequest(transport, {
508
+ jsonrpc: "2.0",
509
+ method: "notifications/initialized",
510
+ }, { "mcp-session-id": sessionId });
511
+ }
512
+ /**
513
+ * Handle MCP requests with per-user sessions
514
+ */
515
+ async function handleMcpRequest(req, res) {
516
+ const auth = getAuth(req);
517
+ if (!auth) {
518
+ console.log("[MCP] Request rejected: no auth");
519
+ res.status(401).json({ error: "unauthorized" });
520
+ return;
521
+ }
522
+ console.log(`[MCP] ${req.method} request from user ${auth.userName} (${auth.userId})`);
523
+ console.log(`[MCP] Headers:`, JSON.stringify({
524
+ "content-type": req.headers["content-type"],
525
+ "mcp-session-id": req.headers["mcp-session-id"],
526
+ accept: req.headers["accept"],
527
+ }));
528
+ console.log(`[MCP] Body:`, JSON.stringify(req.body));
529
+ // Use session ID as the transport key
530
+ const sessionId = auth.sessionId;
531
+ let transport = transports.get(sessionId);
532
+ // Check for existing session ID in request (for session reuse)
533
+ const existingSessionId = req.headers["mcp-session-id"];
534
+ if (existingSessionId) {
535
+ console.log(`[MCP] Existing MCP session ID in header: ${existingSessionId}`);
536
+ }
537
+ // Check if this is an initialize request - if so, close old transport and create fresh one
538
+ const isInitializeRequest = req.body?.method === "initialize";
539
+ if (isInitializeRequest && transport) {
540
+ console.log(`[MCP] Received initialize on existing session, closing old transport`);
541
+ try {
542
+ await transport.close();
543
+ }
544
+ catch (e) {
545
+ // Ignore close errors
546
+ }
547
+ transports.delete(sessionId);
548
+ transport = undefined;
549
+ }
550
+ if (!transport) {
551
+ console.log(`[MCP] Creating new transport for session ${sessionId}`);
552
+ transport = new StreamableHTTPServerTransport({
553
+ sessionIdGenerator: () => sessionId,
554
+ onsessioninitialized: (newSessionId) => {
555
+ console.log(`[MCP] Session initialized: ${newSessionId}`);
556
+ },
557
+ });
558
+ const config = {
559
+ apiKey: auth.apiKey,
560
+ apiBase: BRAND_API_BASE,
561
+ userId: auth.userId,
562
+ userName: auth.userName,
563
+ };
564
+ const server = createServer({ config });
565
+ await server.connect(transport);
566
+ console.log(`[MCP] Server connected to transport`);
567
+ transports.set(sessionId, transport);
568
+ transport.onclose = () => {
569
+ console.log(`[MCP] Transport closed for session ${sessionId}`);
570
+ transports.delete(sessionId);
571
+ };
572
+ // If the client didn't send initialize (e.g. resuming a stale session after
573
+ // server restart), auto-initialize the transport so the real request succeeds.
574
+ if (!isInitializeRequest) {
575
+ console.log(`[MCP] Auto-initializing transport for stale session ${sessionId}`);
576
+ try {
577
+ await autoInitializeTransport(transport, sessionId);
578
+ console.log(`[MCP] Auto-initialization completed for session ${sessionId}`);
579
+ }
580
+ catch (e) {
581
+ console.error(`[MCP] Auto-initialization failed for session ${sessionId}:`, e);
582
+ }
583
+ }
584
+ }
585
+ else {
586
+ console.log(`[MCP] Reusing existing transport for session ${sessionId}`);
587
+ }
588
+ try {
589
+ // Capture ALL response data for logging (including raw writes)
590
+ const originalWrite = res.write.bind(res);
591
+ const originalEnd = res.end.bind(res);
592
+ const originalSetHeader = res.setHeader.bind(res);
593
+ res.setHeader = (name, value) => {
594
+ console.log(`[MCP] Response header: ${name} = ${value}`);
595
+ return originalSetHeader(name, value);
596
+ };
597
+ res.write = (chunk, ...args) => {
598
+ const data = typeof chunk === "string" ? chunk : chunk?.toString?.();
599
+ console.log(`[MCP] Response write:`, data?.slice(0, 500));
600
+ return originalWrite(chunk, ...args);
601
+ };
602
+ res.end = (chunk, ...args) => {
603
+ if (chunk) {
604
+ const data = typeof chunk === "string" ? chunk : chunk?.toString?.();
605
+ console.log(`[MCP] Response end with data:`, data?.slice(0, 500));
606
+ }
607
+ console.log(`[MCP] Response ended, status: ${res.statusCode}`);
608
+ return originalEnd(chunk, ...args);
609
+ };
610
+ // Handle the request (cast to IncomingMessage for MCP SDK compatibility)
611
+ await transport.handleRequest(req, res, req.body);
612
+ console.log(`[MCP] handleRequest completed`);
613
+ }
614
+ catch (error) {
615
+ console.error(`[MCP] Error handling request:`, error);
616
+ throw error;
617
+ }
618
+ }
619
+ /**
620
+ * POST /mcp - Handle MCP messages
621
+ */
622
+ app.post("/mcp", authMiddleware, async (req, res) => {
623
+ try {
624
+ await handleMcpRequest(req, res);
625
+ }
626
+ catch (error) {
627
+ console.error("MCP POST error:", error);
628
+ if (!res.headersSent) {
629
+ res.status(500).json({ error: "Internal server error" });
630
+ }
631
+ }
632
+ });
633
+ /**
634
+ * GET /mcp - Auto-detect transport:
635
+ * SSE client (no mcp-session-id, Accept: text/event-stream) → SSE transport
636
+ * Streamable HTTP client (has mcp-session-id) → existing Streamable HTTP
637
+ */
638
+ app.get("/mcp", authMiddleware, async (req, res) => {
639
+ try {
640
+ const wantsSSE = !req.headers["mcp-session-id"] &&
641
+ req.headers.accept?.includes("text/event-stream");
642
+ if (wantsSSE) {
643
+ await handleSseConnection(req, res);
644
+ }
645
+ else {
646
+ await handleMcpRequest(req, res);
647
+ }
648
+ }
649
+ catch (error) {
650
+ console.error("MCP GET /mcp error:", error);
651
+ if (!res.headersSent) {
652
+ res.status(500).json({ error: "Internal server error" });
653
+ }
654
+ }
655
+ });
656
+ /**
657
+ * DELETE /mcp - End MCP session
658
+ */
659
+ app.delete("/mcp", authMiddleware, async (req, res) => {
660
+ try {
661
+ await handleMcpRequest(req, res);
662
+ }
663
+ catch (error) {
664
+ console.error("MCP DELETE error:", error);
665
+ if (!res.headersSent) {
666
+ res.status(500).json({ error: "Internal server error" });
667
+ }
668
+ }
669
+ });
670
+ // ============================================================================
671
+ // Legacy SSE Transport Endpoints (for clients like Make.com)
672
+ // ============================================================================
673
+ /**
674
+ * Shared helper: establish an SSE connection for the authenticated user.
675
+ * Creates an SSEServerTransport, wires up an MCP server, and starts streaming.
676
+ */
677
+ async function handleSseConnection(req, res) {
678
+ const auth = getAuth(req);
679
+ if (!auth) {
680
+ res.status(401).json({ error: "unauthorized" });
681
+ return;
682
+ }
683
+ console.log(`[SSE] New SSE connection from ${auth.userName} (${auth.userId})`);
684
+ const transport = new SSEServerTransport("/message", res);
685
+ const sessionId = transport.sessionId;
686
+ sseTransports.set(sessionId, { transport, userId: auth.userId });
687
+ const config = {
688
+ apiKey: auth.apiKey,
689
+ apiBase: BRAND_API_BASE,
690
+ userId: auth.userId,
691
+ userName: auth.userName,
692
+ };
693
+ const server = createServer({ config });
694
+ transport.onclose = () => {
695
+ console.log(`[SSE] Transport closed for session ${sessionId}`);
696
+ sseTransports.delete(sessionId);
697
+ };
698
+ await server.connect(transport);
699
+ console.log(`[SSE] Server connected, session ${sessionId}`);
700
+ }
701
+ /**
702
+ * GET /sse - Establish SSE stream (legacy MCP transport)
703
+ * The client connects here first to get an SSE stream, then POSTs messages to /message
704
+ */
705
+ app.get("/sse", authMiddleware, async (req, res) => {
706
+ try {
707
+ await handleSseConnection(req, res);
708
+ }
709
+ catch (error) {
710
+ console.error("SSE GET /sse error:", error);
711
+ if (!res.headersSent) {
712
+ res.status(500).json({ error: "Internal server error" });
713
+ }
714
+ }
715
+ });
716
+ /**
717
+ * POST /message - Receive messages from SSE clients (legacy MCP transport)
718
+ */
719
+ app.post("/message", authMiddleware, async (req, res) => {
720
+ const sessionId = req.query.sessionId;
721
+ if (!sessionId) {
722
+ res.status(400).json({ error: "Missing sessionId query parameter" });
723
+ return;
724
+ }
725
+ const session = sseTransports.get(sessionId);
726
+ if (!session) {
727
+ res.status(404).json({
728
+ error: "Session not found. The SSE connection may have been closed.",
729
+ });
730
+ return;
731
+ }
732
+ const auth = getAuth(req);
733
+ if (!auth || auth.userId !== session.userId) {
734
+ res.status(403).json({ error: "Session belongs to a different user" });
735
+ return;
736
+ }
737
+ console.log(`[SSE] POST /message for session ${sessionId}`);
738
+ try {
739
+ await session.transport.handlePostMessage(req, res, req.body);
740
+ }
741
+ catch (error) {
742
+ console.error(`[SSE] Error handling message:`, error);
743
+ if (!res.headersSent) {
744
+ res.status(500).json({ error: "Internal server error" });
745
+ }
746
+ }
747
+ });
748
+ // ============================================================================
749
+ // LLM Documentation Endpoints (llms.txt standard)
750
+ // ============================================================================
751
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
752
+ function serveLlmsFile(filename, res) {
753
+ const filePath = path.resolve(__dirname, "../", filename);
754
+ try {
755
+ const content = fs.readFileSync(filePath, "utf-8");
756
+ res.type("text/markdown; charset=utf-8").send(content);
757
+ }
758
+ catch {
759
+ res.status(404).send(`${filename} not found`);
760
+ }
761
+ }
762
+ app.get("/llms.txt", (_req, res) => {
763
+ serveLlmsFile("llms.txt", res);
764
+ });
765
+ app.get("/llms-full.txt", (_req, res) => {
766
+ serveLlmsFile("llms-full.txt", res);
767
+ });
768
+ // ============================================================================
769
+ // Health & Status Endpoints
770
+ // ============================================================================
771
+ /**
772
+ * GET /health - Health check endpoint
773
+ */
774
+ app.get("/health", (req, res) => {
775
+ res.json({
776
+ status: "healthy",
777
+ timestamp: new Date().toISOString(),
778
+ sessions: getSessionCount(),
779
+ transports: transports.size,
780
+ });
781
+ });
782
+ /**
783
+ * GET / - Server info (only when no auth header and not an MCP/SSE client)
784
+ */
785
+ app.get("/", (req, res, next) => {
786
+ // If auth header is present, treat as MCP request
787
+ if (req.headers.authorization) {
788
+ return next();
789
+ }
790
+ // If client wants SSE or MCP, let it fall through to authMiddleware (→ 401 triggers OAuth)
791
+ if (req.headers.accept?.includes("text/event-stream")) {
792
+ return next();
793
+ }
794
+ res.json({
795
+ name: `${brand.name} MCP Server`,
796
+ version: "1.6.1",
797
+ description: `MCP server for ${brand.name} knowledge graph analysis`,
798
+ endpoints: {
799
+ oauth: {
800
+ token: "POST /oauth/token",
801
+ revoke: "POST /oauth/revoke",
802
+ metadata: "GET /.well-known/oauth-authorization-server",
803
+ },
804
+ mcp: {
805
+ messages: "POST / or POST /mcp",
806
+ notifications: "GET / or GET /mcp",
807
+ disconnect: "DELETE / or DELETE /mcp",
808
+ },
809
+ health: "GET /health",
810
+ llms: {
811
+ index: "GET /llms.txt",
812
+ full: "GET /llms-full.txt",
813
+ },
814
+ },
815
+ authentication: "Bearer token (obtain via /oauth/token with api_key)",
816
+ });
817
+ });
818
+ /**
819
+ * POST / - Handle MCP messages at root (for clients that POST to base URL)
820
+ */
821
+ app.post("/", authMiddleware, async (req, res) => {
822
+ try {
823
+ console.log(`[MCP] Handling POST / as MCP request`);
824
+ await handleMcpRequest(req, res);
825
+ }
826
+ catch (error) {
827
+ console.error("MCP POST / error:", error);
828
+ if (!res.headersSent) {
829
+ res.status(500).json({ error: "Internal server error" });
830
+ }
831
+ }
832
+ });
833
+ /**
834
+ * GET / with auth - Auto-detect transport:
835
+ * SSE client (no mcp-session-id, Accept: text/event-stream) → SSE transport
836
+ * Streamable HTTP client (has mcp-session-id) → existing Streamable HTTP
837
+ */
838
+ app.get("/", authMiddleware, async (req, res) => {
839
+ try {
840
+ const wantsSSE = !req.headers["mcp-session-id"] &&
841
+ req.headers.accept?.includes("text/event-stream");
842
+ if (wantsSSE) {
843
+ await handleSseConnection(req, res);
844
+ }
845
+ else {
846
+ await handleMcpRequest(req, res);
847
+ }
848
+ }
849
+ catch (error) {
850
+ console.error("MCP GET / error:", error);
851
+ if (!res.headersSent) {
852
+ res.status(500).json({ error: "Internal server error" });
853
+ }
854
+ }
855
+ });
856
+ /**
857
+ * DELETE / - End MCP session at root
858
+ */
859
+ app.delete("/", authMiddleware, async (req, res) => {
860
+ try {
861
+ await handleMcpRequest(req, res);
862
+ }
863
+ catch (error) {
864
+ console.error("MCP DELETE / error:", error);
865
+ if (!res.headersSent) {
866
+ res.status(500).json({ error: "Internal server error" });
867
+ }
868
+ }
869
+ });
870
+ // ============================================================================
871
+ // Start Server
872
+ // ============================================================================
873
+ // Handle graceful shutdown
874
+ process.on("SIGINT", () => {
875
+ console.log("\nShutting down HTTP server...");
876
+ for (const transport of transports.values()) {
877
+ transport.close();
878
+ }
879
+ for (const session of sseTransports.values()) {
880
+ session.transport.close();
881
+ }
882
+ process.exit(0);
883
+ });
884
+ process.on("SIGTERM", () => {
885
+ console.log("\nShutting down HTTP server...");
886
+ for (const transport of transports.values()) {
887
+ transport.close();
888
+ }
889
+ for (const session of sseTransports.values()) {
890
+ session.transport.close();
891
+ }
892
+ process.exit(0);
893
+ });
894
+ // Start the server
895
+ app.listen(PORT, () => {
896
+ console.log(`${brand.name} MCP HTTP Server running on port ${PORT}`);
897
+ console.log(` - Health: http://localhost:${PORT}/health`);
898
+ console.log(` - OAuth token: POST http://localhost:${PORT}/oauth/token`);
899
+ console.log(` - MCP endpoint: http://localhost:${PORT}/mcp`);
900
+ console.log(` - SSE endpoint: http://localhost:${PORT}/sse (legacy)`);
901
+ console.log("");
902
+ console.log("Environment:");
903
+ console.log(` - CORS_ORIGIN: ${CORS_ORIGIN}`);
904
+ console.log(` - ${brand.envPrefix}_API_BASE: ${BRAND_API_BASE}`);
905
+ console.log(` - JWT_SECRET: ${process.env.JWT_SECRET ? "[set]" : "[generated]"}`);
906
+ });
907
+ //# sourceMappingURL=http-server.js.map