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.
- package/LICENSE +21 -0
- package/README.md +629 -0
- package/bin/infranodus-mcp-server.js +8 -0
- package/bin/keywordgraph-mcp-server.js +8 -0
- package/bin/mcp-server.js +56 -0
- package/dist/api/client.d.ts +3 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +30 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/config-store.d.ts +4 -0
- package/dist/api/config-store.d.ts.map +1 -0
- package/dist/api/config-store.js +12 -0
- package/dist/api/config-store.js.map +1 -0
- package/dist/auth/oauth-provider.d.ts +56 -0
- package/dist/auth/oauth-provider.d.ts.map +1 -0
- package/dist/auth/oauth-provider.js +295 -0
- package/dist/auth/oauth-provider.js.map +1 -0
- package/dist/auth/types.d.ts +93 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +5 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/config/brand.d.ts +58 -0
- package/dist/config/brand.d.ts.map +1 -0
- package/dist/config/brand.js +97 -0
- package/dist/config/brand.js.map +1 -0
- package/dist/config/index.d.ts +26 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +20 -0
- package/dist/config/index.js.map +1 -0
- package/dist/http-server.d.ts +6 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +907 -0
- package/dist/http-server.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +136 -0
- package/dist/index.js.map +1 -0
- package/dist/instructions.d.ts +11 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +38 -0
- package/dist/instructions.js.map +1 -0
- package/dist/prompts/index.d.ts +24 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +394 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/resources/about.d.ts +17 -0
- package/dist/resources/about.d.ts.map +1 -0
- package/dist/resources/about.js +64 -0
- package/dist/resources/about.js.map +1 -0
- package/dist/resources/llms-txt.d.ts +33 -0
- package/dist/resources/llms-txt.d.ts.map +1 -0
- package/dist/resources/llms-txt.js +65 -0
- package/dist/resources/llms-txt.js.map +1 -0
- package/dist/schemas/index.d.ts +1178 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +1121 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/tools/addMemory.d.ts +36 -0
- package/dist/tools/addMemory.d.ts.map +1 -0
- package/dist/tools/addMemory.js +82 -0
- package/dist/tools/addMemory.js.map +1 -0
- package/dist/tools/analyzeExistingGraph.d.ts +37 -0
- package/dist/tools/analyzeExistingGraph.d.ts.map +1 -0
- package/dist/tools/analyzeExistingGraph.js +85 -0
- package/dist/tools/analyzeExistingGraph.js.map +1 -0
- package/dist/tools/analyzeLlmResults.d.ts +37 -0
- package/dist/tools/analyzeLlmResults.d.ts.map +1 -0
- package/dist/tools/analyzeLlmResults.js +165 -0
- package/dist/tools/analyzeLlmResults.js.map +1 -0
- package/dist/tools/analyzeText.d.ts +36 -0
- package/dist/tools/analyzeText.d.ts.map +1 -0
- package/dist/tools/analyzeText.js +97 -0
- package/dist/tools/analyzeText.js.map +1 -0
- package/dist/tools/createKnowledgeGraph.d.ts +36 -0
- package/dist/tools/createKnowledgeGraph.d.ts.map +1 -0
- package/dist/tools/createKnowledgeGraph.js +105 -0
- package/dist/tools/createKnowledgeGraph.js.map +1 -0
- package/dist/tools/developConceptualBridges.d.ts +34 -0
- package/dist/tools/developConceptualBridges.d.ts.map +1 -0
- package/dist/tools/developConceptualBridges.js +107 -0
- package/dist/tools/developConceptualBridges.js.map +1 -0
- package/dist/tools/developLatentConcepts copy.d.ts +27 -0
- package/dist/tools/developLatentConcepts copy.d.ts.map +1 -0
- package/dist/tools/developLatentConcepts copy.js +68 -0
- package/dist/tools/developLatentConcepts copy.js.map +1 -0
- package/dist/tools/developLatentConcepts.d.ts +27 -0
- package/dist/tools/developLatentConcepts.d.ts.map +1 -0
- package/dist/tools/developLatentConcepts.js +68 -0
- package/dist/tools/developLatentConcepts.js.map +1 -0
- package/dist/tools/developLatentTopics.d.ts +34 -0
- package/dist/tools/developLatentTopics.d.ts.map +1 -0
- package/dist/tools/developLatentTopics.js +107 -0
- package/dist/tools/developLatentTopics.js.map +1 -0
- package/dist/tools/developTextTool.d.ts +39 -0
- package/dist/tools/developTextTool.d.ts.map +1 -0
- package/dist/tools/developTextTool.js +210 -0
- package/dist/tools/developTextTool.js.map +1 -0
- package/dist/tools/generateContentGaps.d.ts +32 -0
- package/dist/tools/generateContentGaps.d.ts.map +1 -0
- package/dist/tools/generateContentGaps.js +96 -0
- package/dist/tools/generateContentGaps.js.map +1 -0
- package/dist/tools/generateContextualHint.d.ts +33 -0
- package/dist/tools/generateContextualHint.d.ts.map +1 -0
- package/dist/tools/generateContextualHint.js +99 -0
- package/dist/tools/generateContextualHint.js.map +1 -0
- package/dist/tools/generateDifferenceGraphFromTexts.d.ts +52 -0
- package/dist/tools/generateDifferenceGraphFromTexts.d.ts.map +1 -0
- package/dist/tools/generateDifferenceGraphFromTexts.js +160 -0
- package/dist/tools/generateDifferenceGraphFromTexts.js.map +1 -0
- package/dist/tools/generateGoogleResultsVsQueriesGraph.d.ts +37 -0
- package/dist/tools/generateGoogleResultsVsQueriesGraph.d.ts.map +1 -0
- package/dist/tools/generateGoogleResultsVsQueriesGraph.js +84 -0
- package/dist/tools/generateGoogleResultsVsQueriesGraph.js.map +1 -0
- package/dist/tools/generateGoogleSearchQueriesGraph.d.ts +39 -0
- package/dist/tools/generateGoogleSearchQueriesGraph.d.ts.map +1 -0
- package/dist/tools/generateGoogleSearchQueriesGraph.js +88 -0
- package/dist/tools/generateGoogleSearchQueriesGraph.js.map +1 -0
- package/dist/tools/generateGoogleSearchResultsGraph.d.ts +38 -0
- package/dist/tools/generateGoogleSearchResultsGraph.d.ts.map +1 -0
- package/dist/tools/generateGoogleSearchResultsGraph.js +88 -0
- package/dist/tools/generateGoogleSearchResultsGraph.js.map +1 -0
- package/dist/tools/generateKnowledgeGraph.d.ts +35 -0
- package/dist/tools/generateKnowledgeGraph.d.ts.map +1 -0
- package/dist/tools/generateKnowledgeGraph.js +105 -0
- package/dist/tools/generateKnowledgeGraph.js.map +1 -0
- package/dist/tools/generateMergedGraphFromTexts.d.ts +52 -0
- package/dist/tools/generateMergedGraphFromTexts.d.ts.map +1 -0
- package/dist/tools/generateMergedGraphFromTexts.js +160 -0
- package/dist/tools/generateMergedGraphFromTexts.js.map +1 -0
- package/dist/tools/generateOntologyGraph.d.ts +36 -0
- package/dist/tools/generateOntologyGraph.d.ts.map +1 -0
- package/dist/tools/generateOntologyGraph.js +171 -0
- package/dist/tools/generateOntologyGraph.js.map +1 -0
- package/dist/tools/generateOverlapGraphFromTexts.d.ts +52 -0
- package/dist/tools/generateOverlapGraphFromTexts.d.ts.map +1 -0
- package/dist/tools/generateOverlapGraphFromTexts.js +159 -0
- package/dist/tools/generateOverlapGraphFromTexts.js.map +1 -0
- package/dist/tools/generateResearchIdeas.d.ts +39 -0
- package/dist/tools/generateResearchIdeas.d.ts.map +1 -0
- package/dist/tools/generateResearchIdeas.js +117 -0
- package/dist/tools/generateResearchIdeas.js.map +1 -0
- package/dist/tools/generateResearchQuestions.d.ts +37 -0
- package/dist/tools/generateResearchQuestions.d.ts.map +1 -0
- package/dist/tools/generateResearchQuestions.js +111 -0
- package/dist/tools/generateResearchQuestions.js.map +1 -0
- package/dist/tools/generateResearchQuestionsFromGraph.d.ts +34 -0
- package/dist/tools/generateResearchQuestionsFromGraph.d.ts.map +1 -0
- package/dist/tools/generateResearchQuestionsFromGraph.js +75 -0
- package/dist/tools/generateResearchQuestionsFromGraph.js.map +1 -0
- package/dist/tools/generateResponses.d.ts +35 -0
- package/dist/tools/generateResponses.d.ts.map +1 -0
- package/dist/tools/generateResponses.js +112 -0
- package/dist/tools/generateResponses.js.map +1 -0
- package/dist/tools/generateResponsesFromGraph.d.ts +33 -0
- package/dist/tools/generateResponsesFromGraph.d.ts.map +1 -0
- package/dist/tools/generateResponsesFromGraph.js +74 -0
- package/dist/tools/generateResponsesFromGraph.js.map +1 -0
- package/dist/tools/generateSEOGraphTool.d.ts +39 -0
- package/dist/tools/generateSEOGraphTool.d.ts.map +1 -0
- package/dist/tools/generateSEOGraphTool.js +235 -0
- package/dist/tools/generateSEOGraphTool.js.map +1 -0
- package/dist/tools/generateTextOverview.d.ts +31 -0
- package/dist/tools/generateTextOverview.d.ts.map +1 -0
- package/dist/tools/generateTextOverview.js +68 -0
- package/dist/tools/generateTextOverview.js.map +1 -0
- package/dist/tools/generateTopicalClusters.d.ts +34 -0
- package/dist/tools/generateTopicalClusters.d.ts.map +1 -0
- package/dist/tools/generateTopicalClusters.js +134 -0
- package/dist/tools/generateTopicalClusters.js.map +1 -0
- package/dist/tools/getMemory.d.ts +32 -0
- package/dist/tools/getMemory.d.ts.map +1 -0
- package/dist/tools/getMemory.js +121 -0
- package/dist/tools/getMemory.js.map +1 -0
- package/dist/tools/index.d.ts +30 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +30 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/listGraphs.d.ts +37 -0
- package/dist/tools/listGraphs.d.ts.map +1 -0
- package/dist/tools/listGraphs.js +127 -0
- package/dist/tools/listGraphs.js.map +1 -0
- package/dist/tools/optimizeTextStructure.d.ts +34 -0
- package/dist/tools/optimizeTextStructure.d.ts.map +1 -0
- package/dist/tools/optimizeTextStructure.js +108 -0
- package/dist/tools/optimizeTextStructure.js.map +1 -0
- package/dist/tools/retrieveContextForPromptFromGraph.d.ts +37 -0
- package/dist/tools/retrieveContextForPromptFromGraph.d.ts.map +1 -0
- package/dist/tools/retrieveContextForPromptFromGraph.js +80 -0
- package/dist/tools/retrieveContextForPromptFromGraph.js.map +1 -0
- package/dist/tools/searchGraph.d.ts +27 -0
- package/dist/tools/searchGraph.d.ts.map +1 -0
- package/dist/tools/searchGraph.js +57 -0
- package/dist/tools/searchGraph.js.map +1 -0
- package/dist/tools/searchGraphs.d.ts +33 -0
- package/dist/tools/searchGraphs.d.ts.map +1 -0
- package/dist/tools/searchGraphs.js +67 -0
- package/dist/tools/searchGraphs.js.map +1 -0
- package/dist/tools/searchGraphsFetch.d.ts +31 -0
- package/dist/tools/searchGraphsFetch.d.ts.map +1 -0
- package/dist/tools/searchGraphsFetch.js +78 -0
- package/dist/tools/searchGraphsFetch.js.map +1 -0
- package/dist/tools/sequentialThinking.d.ts +71 -0
- package/dist/tools/sequentialThinking.d.ts.map +1 -0
- package/dist/tools/sequentialThinking.js +218 -0
- package/dist/tools/sequentialThinking.js.map +1 -0
- package/dist/types/index.d.ts +378 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/progress.d.ts +21 -0
- package/dist/utils/progress.d.ts.map +1 -0
- package/dist/utils/progress.js +50 -0
- package/dist/utils/progress.js.map +1 -0
- package/dist/utils/transformers.d.ts +31 -0
- package/dist/utils/transformers.d.ts.map +1 -0
- package/dist/utils/transformers.js +484 -0
- package/dist/utils/transformers.js.map +1 -0
- package/dist/utils/urlContent.d.ts +35 -0
- package/dist/utils/urlContent.d.ts.map +1 -0
- package/dist/utils/urlContent.js +87 -0
- package/dist/utils/urlContent.js.map +1 -0
- package/llms-full.infranodus.txt +469 -0
- package/llms-full.keywordgraph.txt +469 -0
- package/llms-full.txt +469 -0
- package/llms.infranodus.txt +64 -0
- package/llms.keywordgraph.txt +64 -0
- package/llms.txt +64 -0
- 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, "<")
|
|
30
|
+
.replace(/>/g, ">")
|
|
31
|
+
.replace(/"/g, """)
|
|
32
|
+
.replace(/'/g, "'");
|
|
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
|