infranodus-mcp-server 1.2.4 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -31
- package/dist/http-server.js +209 -27
- package/dist/http-server.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/instructions.d.ts +11 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +37 -0
- package/dist/instructions.js.map +1 -0
- package/dist/prompts/index.js +1 -1
- package/dist/resources/about.d.ts.map +1 -1
- package/dist/resources/about.js +26 -24
- package/dist/resources/about.js.map +1 -1
- 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 +54 -0
- package/dist/resources/llms-txt.js.map +1 -0
- package/dist/schemas/index.d.ts +673 -86
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +304 -87
- package/dist/schemas/index.js.map +1 -1
- package/dist/tools/analyzeExistingGraph.d.ts.map +1 -1
- package/dist/tools/analyzeExistingGraph.js +13 -8
- package/dist/tools/analyzeExistingGraph.js.map +1 -1
- package/dist/tools/analyzeText.d.ts +36 -0
- package/dist/tools/analyzeText.d.ts.map +1 -0
- package/dist/tools/analyzeText.js +96 -0
- package/dist/tools/analyzeText.js.map +1 -0
- package/dist/tools/createKnowledgeGraph.d.ts +2 -2
- package/dist/tools/createKnowledgeGraph.d.ts.map +1 -1
- package/dist/tools/createKnowledgeGraph.js +26 -2
- package/dist/tools/createKnowledgeGraph.js.map +1 -1
- package/dist/tools/developConceptualBridges.d.ts +4 -3
- package/dist/tools/developConceptualBridges.d.ts.map +1 -1
- package/dist/tools/developConceptualBridges.js +45 -11
- package/dist/tools/developConceptualBridges.js.map +1 -1
- package/dist/tools/developLatentTopics.d.ts +4 -3
- package/dist/tools/developLatentTopics.d.ts.map +1 -1
- package/dist/tools/developLatentTopics.js +45 -11
- package/dist/tools/developLatentTopics.js.map +1 -1
- package/dist/tools/developTextTool.d.ts +3 -2
- package/dist/tools/developTextTool.d.ts.map +1 -1
- package/dist/tools/developTextTool.js +73 -23
- package/dist/tools/developTextTool.js.map +1 -1
- package/dist/tools/generateContentGaps.d.ts +3 -2
- package/dist/tools/generateContentGaps.d.ts.map +1 -1
- package/dist/tools/generateContentGaps.js +38 -10
- package/dist/tools/generateContentGaps.js.map +1 -1
- package/dist/tools/generateContextualHint.d.ts +3 -2
- package/dist/tools/generateContextualHint.d.ts.map +1 -1
- package/dist/tools/generateContextualHint.js +36 -8
- package/dist/tools/generateContextualHint.js.map +1 -1
- package/dist/tools/generateDifferenceGraphFromTexts.d.ts +15 -6
- package/dist/tools/generateDifferenceGraphFromTexts.d.ts.map +1 -1
- package/dist/tools/generateDifferenceGraphFromTexts.js +90 -7
- package/dist/tools/generateDifferenceGraphFromTexts.js.map +1 -1
- package/dist/tools/generateGoogleResultsVsQueriesGraph.d.ts +3 -1
- package/dist/tools/generateGoogleResultsVsQueriesGraph.d.ts.map +1 -1
- package/dist/tools/generateGoogleResultsVsQueriesGraph.js +18 -9
- package/dist/tools/generateGoogleResultsVsQueriesGraph.js.map +1 -1
- package/dist/tools/generateGoogleSearchQueriesGraph.d.ts +3 -1
- package/dist/tools/generateGoogleSearchQueriesGraph.d.ts.map +1 -1
- package/dist/tools/generateGoogleSearchQueriesGraph.js +21 -10
- package/dist/tools/generateGoogleSearchQueriesGraph.js.map +1 -1
- package/dist/tools/generateGoogleSearchResultsGraph.d.ts +4 -2
- package/dist/tools/generateGoogleSearchResultsGraph.d.ts.map +1 -1
- package/dist/tools/generateGoogleSearchResultsGraph.js +22 -10
- package/dist/tools/generateGoogleSearchResultsGraph.js.map +1 -1
- package/dist/tools/generateKnowledgeGraph.d.ts +2 -2
- package/dist/tools/generateKnowledgeGraph.d.ts.map +1 -1
- package/dist/tools/generateKnowledgeGraph.js +26 -2
- package/dist/tools/generateKnowledgeGraph.js.map +1 -1
- package/dist/tools/generateMergedGraphFromTexts.d.ts +52 -0
- package/dist/tools/generateMergedGraphFromTexts.d.ts.map +1 -0
- package/dist/tools/generateMergedGraphFromTexts.js +159 -0
- package/dist/tools/generateMergedGraphFromTexts.js.map +1 -0
- package/dist/tools/generateOverlapGraphFromTexts.d.ts +15 -6
- package/dist/tools/generateOverlapGraphFromTexts.d.ts.map +1 -1
- package/dist/tools/generateOverlapGraphFromTexts.js +91 -7
- package/dist/tools/generateOverlapGraphFromTexts.js.map +1 -1
- package/dist/tools/generateResearchIdeas.d.ts +4 -2
- package/dist/tools/generateResearchIdeas.d.ts.map +1 -1
- package/dist/tools/generateResearchIdeas.js +54 -13
- package/dist/tools/generateResearchIdeas.js.map +1 -1
- package/dist/tools/generateResearchQuestions.d.ts +3 -2
- package/dist/tools/generateResearchQuestions.d.ts.map +1 -1
- package/dist/tools/generateResearchQuestions.js +44 -9
- package/dist/tools/generateResearchQuestions.js.map +1 -1
- package/dist/tools/generateResponses.d.ts +34 -0
- package/dist/tools/generateResponses.d.ts.map +1 -0
- package/dist/tools/generateResponses.js +110 -0
- package/dist/tools/generateResponses.js.map +1 -0
- package/dist/tools/generateSEOGraphTool.d.ts +2 -0
- package/dist/tools/generateSEOGraphTool.d.ts.map +1 -1
- package/dist/tools/generateSEOGraphTool.js +16 -75
- package/dist/tools/generateSEOGraphTool.js.map +1 -1
- package/dist/tools/generateTopicalClusters.d.ts +3 -2
- package/dist/tools/generateTopicalClusters.d.ts.map +1 -1
- package/dist/tools/generateTopicalClusters.js +38 -10
- package/dist/tools/generateTopicalClusters.js.map +1 -1
- package/dist/tools/getMemory.d.ts.map +1 -1
- package/dist/tools/getMemory.js +7 -5
- package/dist/tools/getMemory.js.map +1 -1
- package/dist/tools/index.d.ts +4 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/listGraphs.d.ts +1 -1
- package/dist/tools/listGraphs.d.ts.map +1 -1
- package/dist/tools/listGraphs.js +7 -5
- package/dist/tools/listGraphs.js.map +1 -1
- 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/types/index.d.ts +23 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/transformers.d.ts +2 -1
- package/dist/utils/transformers.d.ts.map +1 -1
- package/dist/utils/transformers.js +39 -1
- package/dist/utils/transformers.js.map +1 -1
- 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.txt +469 -0
- package/llms.txt +64 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -34,68 +34,75 @@ InfraNodus MCP Server enables LLM workflows and AI assistants to analyze text us
|
|
|
34
34
|
- Access previously saved analyses
|
|
35
35
|
- Export graph data with full statistics
|
|
36
36
|
|
|
37
|
-
3. **
|
|
37
|
+
3. **analyze_text**
|
|
38
|
+
|
|
39
|
+
- Analyze a text, URL, or YouTube transcript
|
|
40
|
+
- Extract and analyze a graph from text or URL; provide either text or url
|
|
41
|
+
- Get topics, clusters, statements, graph structure, and AI summary as requested
|
|
42
|
+
|
|
43
|
+
4. **generate_content_gaps**
|
|
38
44
|
|
|
39
45
|
- Detect missing connections in discourse
|
|
40
46
|
- Identify underexplored topics
|
|
41
47
|
- Generate research questions
|
|
42
48
|
- Suggest content development opportunities
|
|
43
49
|
|
|
44
|
-
|
|
50
|
+
5. **generate_topical_clusters**
|
|
45
51
|
|
|
46
52
|
- Generate topics and clusters of keywords from text using knowledge graph analysis
|
|
47
53
|
- Make sure to beyond genetic insights and detect smaller topics
|
|
48
54
|
- Use the topical clusters to establish topical authority for SEO
|
|
49
55
|
|
|
50
|
-
|
|
56
|
+
6. **generate_contextual_hint**
|
|
51
57
|
|
|
52
58
|
- Generate a topical overview of a text and provide insights for LLMs to generate better responses
|
|
53
59
|
- Use it to get a high-level understanding of a text
|
|
54
60
|
- Use it to augment prompts in your LLM workflows and AI assistants
|
|
55
61
|
|
|
56
|
-
|
|
62
|
+
7. **generate_research_questions**
|
|
57
63
|
|
|
58
|
-
- Generate research questions that bridge content gaps
|
|
64
|
+
- Generate research questions that bridge content gaps from text, URL, or an existing InfraNodus graph
|
|
59
65
|
- Use them as prompts in your LLM models and AI workflows
|
|
60
66
|
- Use any AI model (included in InfraNodus API)
|
|
61
67
|
- Content gaps are identified based on topical clustering
|
|
62
68
|
|
|
63
|
-
|
|
69
|
+
8. **generate_research_ideas**
|
|
64
70
|
|
|
65
71
|
- Generate innovative research ideas based on content gaps identified in the text
|
|
66
72
|
- Get actionable ideas to improve the text and develop the discourse
|
|
67
73
|
- Use any AI model (included in InfraNodus API)
|
|
68
74
|
- Ideas are generated from gaps between topical clusters
|
|
69
75
|
|
|
70
|
-
|
|
76
|
+
9. **optimize_text_structure**
|
|
71
77
|
|
|
72
|
-
-
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
78
|
+
- Analyze the level of bias and coherence in text using knowledge graph analysis
|
|
79
|
+
- If the text is too biased, develop the represented topics to balance the discourse
|
|
80
|
+
- If the text is focused or diversified, develop the content gaps to deepen the analysis
|
|
81
|
+
- If the text is dispersed, focus the most common gap topics to improve coherence
|
|
82
|
+
- Choose response type: response, idea, question, or transcend
|
|
76
83
|
|
|
77
|
-
|
|
84
|
+
10. **generate_responses_from_graph**
|
|
78
85
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
- Generate responses based on an existing InfraNodus graph
|
|
87
|
+
- Integrate them into your LLM workflows and AI assistants
|
|
88
|
+
- Use any AI model (included in InfraNodus API)
|
|
89
|
+
- Use any prompt
|
|
83
90
|
|
|
84
|
-
|
|
91
|
+
11. **develop_conceptual_bridges**
|
|
85
92
|
|
|
86
93
|
- Analyze text and develop latent ideas based on concepts that connect this text to a broader discourse
|
|
87
94
|
- Discover hidden themes and patterns that link your text to wider contexts
|
|
88
95
|
- Use any AI model (included in InfraNodus API)
|
|
89
96
|
- Generate insights that help develop the discourse
|
|
90
97
|
|
|
91
|
-
|
|
98
|
+
12. **develop_latent_topics**
|
|
92
99
|
|
|
93
100
|
- Analyze text and extract underdeveloped topics with ideas on how to develop them
|
|
94
101
|
- Identify topics that need more attention and elaboration
|
|
95
102
|
- Use any AI model (included in InfraNodus API)
|
|
96
103
|
- Get actionable suggestions for content expansion
|
|
97
104
|
|
|
98
|
-
|
|
105
|
+
13. **develop_text_tool**
|
|
99
106
|
|
|
100
107
|
- Comprehensive text analysis combining content gap ideas, latent topics, and conceptual bridges
|
|
101
108
|
- Executes multiple analyses in sequence with progress tracking
|
|
@@ -103,44 +110,49 @@ InfraNodus MCP Server enables LLM workflows and AI assistants to analyze text us
|
|
|
103
110
|
- Identifies latent topics and conceptual bridges to develop
|
|
104
111
|
- Finds content gaps for deeper exploration
|
|
105
112
|
|
|
106
|
-
|
|
113
|
+
14. **create_knowledge_graph**
|
|
107
114
|
|
|
108
115
|
- Create a knowledge graph in InfraNodus from text and provide a link to it
|
|
109
116
|
- Use it to create a knowledge graph in InfraNodus from text
|
|
110
117
|
|
|
111
|
-
|
|
118
|
+
15. **overlap_between_texts**
|
|
112
119
|
|
|
113
120
|
- Create knowledge graphs from two or more texts and find the overlap (similarities) between them
|
|
114
121
|
- Use it to find similar topics and keywords across different texts
|
|
115
122
|
|
|
116
|
-
|
|
123
|
+
16. **merged_graph_from_texts**
|
|
124
|
+
|
|
125
|
+
- Build a graph of all the texts and URLs provided, providing topical clusters and gaps present in the merged graph generated from all the texts
|
|
126
|
+
- Use it to combine multiple sources into one graph and see clusters and content gaps across the merged content
|
|
127
|
+
|
|
128
|
+
17. **difference_between_texts**
|
|
117
129
|
|
|
118
130
|
- Compare knowledge graphs from two or more texts and find what's not present in the first graph that's present in the others
|
|
119
131
|
- Use it to find how one text can be enriched with the others
|
|
120
132
|
|
|
121
|
-
|
|
133
|
+
18. **analyze_google_search_results**
|
|
122
134
|
|
|
123
135
|
- Generate a graph with keywords and topics for Google search results for a certain query
|
|
124
136
|
- Use it to understand the current informational supply (what people find)
|
|
125
137
|
|
|
126
|
-
|
|
138
|
+
19. **analyze_related_search_queries**
|
|
127
139
|
|
|
128
140
|
- Generate a graph from the search queries suggested by Google for a certain query
|
|
129
141
|
- Use it to understand the current informational demand (what people are looking for)
|
|
130
142
|
|
|
131
|
-
|
|
143
|
+
20. **search_queries_vs_search_results**
|
|
132
144
|
|
|
133
145
|
- Generate a graph of keyword combinations and topics people tend to search for that do not readily appear in the search results for the same queries
|
|
134
146
|
- Use it to understand what people search for but don't yet find
|
|
135
147
|
|
|
136
|
-
|
|
148
|
+
21. **generate_seo_report**
|
|
137
149
|
|
|
138
150
|
- Analyze content for SEO optimization by comparing it with Google search results and search queries
|
|
139
151
|
- Identify content gaps and opportunities for better search visibility
|
|
140
152
|
- Get comprehensive analysis of what's in search results but not in your text
|
|
141
153
|
- Discover what people search for but don't find in current results
|
|
142
154
|
|
|
143
|
-
|
|
155
|
+
22. **memory_add_relations**
|
|
144
156
|
|
|
145
157
|
- Add relations to the InfraNodus memory from text
|
|
146
158
|
- Automatically detect entities or use [[wikilinks]] syntax to mark them
|
|
@@ -148,7 +160,7 @@ InfraNodus MCP Server enables LLM workflows and AI assistants to analyze text us
|
|
|
148
160
|
- Support automatic entity extraction or manual entity marking
|
|
149
161
|
- Provide links to created memory graphs for easy access
|
|
150
162
|
|
|
151
|
-
|
|
163
|
+
23. **memory_get_relations**
|
|
152
164
|
|
|
153
165
|
- Retrieve relations from InfraNodus memory for specific entities
|
|
154
166
|
- Search for entity relations using [[wikilinks]] syntax
|
|
@@ -156,7 +168,7 @@ InfraNodus MCP Server enables LLM workflows and AI assistants to analyze text us
|
|
|
156
168
|
- Extract statements and relationships from stored knowledge graphs
|
|
157
169
|
- Support both entity-specific searches and full context retrieval
|
|
158
170
|
|
|
159
|
-
|
|
171
|
+
24. **retrieve_from_knowledge_base**
|
|
160
172
|
|
|
161
173
|
- Retrieve context from an existing InfraNodus knowledge graph using GraphRAG
|
|
162
174
|
- Query your knowledge base with a natural language prompt to get relevant statements
|
|
@@ -164,13 +176,13 @@ InfraNodus MCP Server enables LLM workflows and AI assistants to analyze text us
|
|
|
164
176
|
- Optionally retrieve the full graph, statements, or extended analysis
|
|
165
177
|
- Ideal for augmenting LLM responses with domain-specific knowledge
|
|
166
178
|
|
|
167
|
-
|
|
179
|
+
25. **search**
|
|
168
180
|
|
|
169
181
|
- Search through existing InfraNodus graphs
|
|
170
182
|
- Also use it to search through the public graphs of a specific user
|
|
171
183
|
- Compatible with ChatGPT Deep Research mode via Developer Mode > Connectors
|
|
172
184
|
|
|
173
|
-
|
|
185
|
+
26. **fetch**
|
|
174
186
|
|
|
175
187
|
- Fetch a specific search result for a graph
|
|
176
188
|
- Can be used in ChatGPT Deep Research mode via Developer Mode > Connectors
|
|
@@ -523,6 +535,20 @@ Retrieves and analyzes an existing graph from your InfraNodus account.
|
|
|
523
535
|
- `includeStatements` (boolean): Include statements in response
|
|
524
536
|
- `includeGraphSummary` (boolean): Include graph summary
|
|
525
537
|
|
|
538
|
+
### analyze_text
|
|
539
|
+
|
|
540
|
+
Analyze a text, URL, or YouTube transcript. Extract and analyze a graph from text or URL; provide either text or url.
|
|
541
|
+
|
|
542
|
+
**Parameters:**
|
|
543
|
+
|
|
544
|
+
- `text` (string, optional): Text to analyze. Provide either this or url.
|
|
545
|
+
- `url` (string, optional): URL to fetch content from (e.g. webpage or YouTube transcript). Provide either this or text.
|
|
546
|
+
- `includeStatements` (boolean): Include processed statements in response
|
|
547
|
+
- `includeGraph` (boolean): Include full graph structure in response
|
|
548
|
+
- `addNodesAndEdges` (boolean): Include nodes and edges in response
|
|
549
|
+
- `includeGraphSummary` (boolean): Include AI-generated graph summary for RAG prompt augmentation
|
|
550
|
+
- `modifyAnalyzedText` (string): Entity detection — "none", "detectEntities", or "extractEntitiesOnly"
|
|
551
|
+
|
|
526
552
|
### generate_content_gaps
|
|
527
553
|
|
|
528
554
|
Identifies content gaps and missing connections in text.
|
package/dist/http-server.js
CHANGED
|
@@ -5,16 +5,36 @@
|
|
|
5
5
|
import express from "express";
|
|
6
6
|
import cors from "cors";
|
|
7
7
|
import helmet from "helmet";
|
|
8
|
+
import crypto from "crypto";
|
|
8
9
|
import * as dotenv from "dotenv";
|
|
9
10
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
11
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
10
12
|
import createServer from "./index.js";
|
|
11
13
|
import { exchangeApiKeyForToken, verifyAccessToken, revokeSession, getSessionCount, registerClient, getClient, createAuthorizationCode, exchangeAuthorizationCode, validateApiKey, } from "./auth/oauth-provider.js";
|
|
14
|
+
import * as fs from "fs";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
12
17
|
// Load environment variables
|
|
13
18
|
dotenv.config();
|
|
14
19
|
// Configuration
|
|
15
20
|
const PORT = parseInt(process.env.PORT || "3000", 10);
|
|
16
21
|
const CORS_ORIGIN = process.env.CORS_ORIGIN || "*";
|
|
17
22
|
const INFRANODUS_API_BASE = process.env.INFRANODUS_API_BASE || "https://infranodus.com/api/v1";
|
|
23
|
+
// Allow HTTP only for localhost/127.0.0.1 (local dev); otherwise require HTTPS
|
|
24
|
+
function isRedirectUriAllowed(redirectUri) {
|
|
25
|
+
if (redirectUri.startsWith("https://"))
|
|
26
|
+
return true;
|
|
27
|
+
if (!redirectUri.startsWith("http://"))
|
|
28
|
+
return false;
|
|
29
|
+
try {
|
|
30
|
+
const u = new URL(redirectUri);
|
|
31
|
+
const host = u.hostname.toLowerCase();
|
|
32
|
+
return host === "localhost" || host === "127.0.0.1";
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
18
38
|
// Store auth info on request (using a symbol to avoid conflicts with MCP SDK)
|
|
19
39
|
const AUTH_KEY = Symbol("auth");
|
|
20
40
|
// Create Express app
|
|
@@ -40,16 +60,21 @@ app.use((req, res, next) => {
|
|
|
40
60
|
})}`);
|
|
41
61
|
next();
|
|
42
62
|
});
|
|
43
|
-
// Store transports per session for MCP
|
|
63
|
+
// Store transports per session for MCP (Streamable HTTP)
|
|
44
64
|
const transports = new Map();
|
|
65
|
+
// Store SSE transports per session for legacy MCP clients
|
|
66
|
+
const sseTransports = new Map();
|
|
45
67
|
/**
|
|
46
|
-
* Auth middleware - validates Bearer token and attaches auth info to request
|
|
68
|
+
* Auth middleware - validates Bearer token and attaches auth info to request.
|
|
69
|
+
* Supports both JWT access tokens (from OAuth flow) and raw InfraNodus API keys.
|
|
47
70
|
*/
|
|
48
|
-
function authMiddleware(req, res, next) {
|
|
71
|
+
async function authMiddleware(req, res, next) {
|
|
49
72
|
console.log(`[AUTH] Checking auth for ${req.method} ${req.path}`);
|
|
50
73
|
const authHeader = req.headers.authorization;
|
|
51
74
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
52
75
|
console.log(`[AUTH] Missing or invalid Authorization header`);
|
|
76
|
+
const baseUrl = `${req.protocol}://${req.get("host")}`;
|
|
77
|
+
res.setHeader("WWW-Authenticate", `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"`);
|
|
53
78
|
const error = {
|
|
54
79
|
error: "unauthorized",
|
|
55
80
|
error_description: "Missing or invalid Authorization header",
|
|
@@ -59,19 +84,41 @@ function authMiddleware(req, res, next) {
|
|
|
59
84
|
}
|
|
60
85
|
const token = authHeader.slice(7); // Remove "Bearer " prefix
|
|
61
86
|
console.log(`[AUTH] Token present, verifying...`);
|
|
87
|
+
// Try JWT verification first (fast path for OAuth-authenticated clients)
|
|
62
88
|
const authInfo = verifyAccessToken(token);
|
|
63
|
-
if (
|
|
64
|
-
console.log(`[AUTH]
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
error_description: "Access token is invalid or expired",
|
|
68
|
-
};
|
|
69
|
-
res.status(401).json(error);
|
|
89
|
+
if (authInfo) {
|
|
90
|
+
console.log(`[AUTH] Authenticated via JWT as ${authInfo.userName} (${authInfo.userId})`);
|
|
91
|
+
req[AUTH_KEY] = authInfo;
|
|
92
|
+
next();
|
|
70
93
|
return;
|
|
71
94
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
95
|
+
// JWT failed — try treating the token as a raw InfraNodus API key
|
|
96
|
+
console.log(`[AUTH] JWT verification failed, trying as raw API key...`);
|
|
97
|
+
try {
|
|
98
|
+
const userInfo = await validateApiKey(token);
|
|
99
|
+
if (userInfo) {
|
|
100
|
+
console.log(`[AUTH] Authenticated via raw API key as ${userInfo.userName} (${userInfo.userId})`);
|
|
101
|
+
req[AUTH_KEY] = {
|
|
102
|
+
userId: userInfo.userId,
|
|
103
|
+
userName: userInfo.userName,
|
|
104
|
+
apiKey: token,
|
|
105
|
+
sessionId: crypto.randomUUID(),
|
|
106
|
+
};
|
|
107
|
+
next();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
console.log(`[AUTH] Raw API key validation error: ${err}`);
|
|
113
|
+
}
|
|
114
|
+
console.log(`[AUTH] All authentication methods failed`);
|
|
115
|
+
const baseUrl = `${req.protocol}://${req.get("host")}`;
|
|
116
|
+
res.setHeader("WWW-Authenticate", `Bearer resource_metadata="${baseUrl}/.well-known/oauth-protected-resource"`);
|
|
117
|
+
const error = {
|
|
118
|
+
error: "invalid_token",
|
|
119
|
+
error_description: "Access token is invalid or expired",
|
|
120
|
+
};
|
|
121
|
+
res.status(401).json(error);
|
|
75
122
|
}
|
|
76
123
|
/**
|
|
77
124
|
* Helper to get auth info from request
|
|
@@ -142,12 +189,11 @@ app.get("/oauth/authorize", (req, res) => {
|
|
|
142
189
|
});
|
|
143
190
|
return;
|
|
144
191
|
}
|
|
145
|
-
// Accept
|
|
146
|
-
|
|
147
|
-
if (!redirect_uri.startsWith("https://")) {
|
|
192
|
+
// Accept HTTPS or http://localhost / http://127.0.0.1 (for local dev)
|
|
193
|
+
if (!isRedirectUriAllowed(redirect_uri)) {
|
|
148
194
|
res.status(400).json({
|
|
149
195
|
error: "invalid_request",
|
|
150
|
-
error_description: "redirect_uri must use HTTPS",
|
|
196
|
+
error_description: "redirect_uri must use HTTPS or http://localhost or http://127.0.0.1",
|
|
151
197
|
});
|
|
152
198
|
return;
|
|
153
199
|
}
|
|
@@ -205,8 +251,8 @@ app.post("/oauth/authorize", express.urlencoded({ extended: true }), async (req,
|
|
|
205
251
|
res.status(400).json({ error: "invalid_client" });
|
|
206
252
|
return;
|
|
207
253
|
}
|
|
208
|
-
// Validate redirect_uri (
|
|
209
|
-
if (!redirect_uri || !redirect_uri
|
|
254
|
+
// Validate redirect_uri (HTTPS or localhost/127.0.0.1 for local dev)
|
|
255
|
+
if (!redirect_uri || !isRedirectUriAllowed(redirect_uri)) {
|
|
210
256
|
res.status(400).json({
|
|
211
257
|
error: "invalid_request",
|
|
212
258
|
error_description: "Invalid redirect_uri",
|
|
@@ -365,6 +411,19 @@ app.get("/.well-known/openid-configuration", (req, res) => {
|
|
|
365
411
|
scopes_supported: ["mcp", "read", "write"],
|
|
366
412
|
});
|
|
367
413
|
});
|
|
414
|
+
/**
|
|
415
|
+
* GET /.well-known/oauth-protected-resource - OAuth2 Protected Resource Metadata (RFC 9728)
|
|
416
|
+
* MCP clients fetch this to discover the authorization server for this resource.
|
|
417
|
+
*/
|
|
418
|
+
app.get("/.well-known/oauth-protected-resource", (req, res) => {
|
|
419
|
+
const baseUrl = `${req.protocol}://${req.get("host")}`;
|
|
420
|
+
res.json({
|
|
421
|
+
resource: baseUrl,
|
|
422
|
+
authorization_servers: [baseUrl],
|
|
423
|
+
scopes_supported: ["mcp", "read", "write"],
|
|
424
|
+
bearer_methods_supported: ["header"],
|
|
425
|
+
});
|
|
426
|
+
});
|
|
368
427
|
// ============================================================================
|
|
369
428
|
// MCP Endpoints
|
|
370
429
|
// ============================================================================
|
|
@@ -481,14 +540,23 @@ app.post("/mcp", authMiddleware, async (req, res) => {
|
|
|
481
540
|
}
|
|
482
541
|
});
|
|
483
542
|
/**
|
|
484
|
-
* GET /mcp -
|
|
543
|
+
* GET /mcp - Auto-detect transport:
|
|
544
|
+
* SSE client (no mcp-session-id, Accept: text/event-stream) → SSE transport
|
|
545
|
+
* Streamable HTTP client (has mcp-session-id) → existing Streamable HTTP
|
|
485
546
|
*/
|
|
486
547
|
app.get("/mcp", authMiddleware, async (req, res) => {
|
|
487
548
|
try {
|
|
488
|
-
|
|
549
|
+
const wantsSSE = !req.headers["mcp-session-id"] &&
|
|
550
|
+
req.headers.accept?.includes("text/event-stream");
|
|
551
|
+
if (wantsSSE) {
|
|
552
|
+
await handleSseConnection(req, res);
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
await handleMcpRequest(req, res);
|
|
556
|
+
}
|
|
489
557
|
}
|
|
490
558
|
catch (error) {
|
|
491
|
-
console.error("MCP GET error:", error);
|
|
559
|
+
console.error("MCP GET /mcp error:", error);
|
|
492
560
|
if (!res.headersSent) {
|
|
493
561
|
res.status(500).json({ error: "Internal server error" });
|
|
494
562
|
}
|
|
@@ -509,6 +577,97 @@ app.delete("/mcp", authMiddleware, async (req, res) => {
|
|
|
509
577
|
}
|
|
510
578
|
});
|
|
511
579
|
// ============================================================================
|
|
580
|
+
// Legacy SSE Transport Endpoints (for clients like Make.com)
|
|
581
|
+
// ============================================================================
|
|
582
|
+
/**
|
|
583
|
+
* Shared helper: establish an SSE connection for the authenticated user.
|
|
584
|
+
* Creates an SSEServerTransport, wires up an MCP server, and starts streaming.
|
|
585
|
+
*/
|
|
586
|
+
async function handleSseConnection(req, res) {
|
|
587
|
+
const auth = getAuth(req);
|
|
588
|
+
if (!auth) {
|
|
589
|
+
res.status(401).json({ error: "unauthorized" });
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
console.log(`[SSE] New SSE connection from ${auth.userName} (${auth.userId})`);
|
|
593
|
+
const transport = new SSEServerTransport("/message", res);
|
|
594
|
+
const sessionId = transport.sessionId;
|
|
595
|
+
sseTransports.set(sessionId, transport);
|
|
596
|
+
const config = {
|
|
597
|
+
apiKey: auth.apiKey,
|
|
598
|
+
apiBase: INFRANODUS_API_BASE,
|
|
599
|
+
};
|
|
600
|
+
const server = createServer({ config });
|
|
601
|
+
transport.onclose = () => {
|
|
602
|
+
console.log(`[SSE] Transport closed for session ${sessionId}`);
|
|
603
|
+
sseTransports.delete(sessionId);
|
|
604
|
+
};
|
|
605
|
+
await server.connect(transport);
|
|
606
|
+
console.log(`[SSE] Server connected, session ${sessionId}`);
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* GET /sse - Establish SSE stream (legacy MCP transport)
|
|
610
|
+
* The client connects here first to get an SSE stream, then POSTs messages to /message
|
|
611
|
+
*/
|
|
612
|
+
app.get("/sse", authMiddleware, async (req, res) => {
|
|
613
|
+
try {
|
|
614
|
+
await handleSseConnection(req, res);
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
console.error("SSE GET /sse error:", error);
|
|
618
|
+
if (!res.headersSent) {
|
|
619
|
+
res.status(500).json({ error: "Internal server error" });
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
/**
|
|
624
|
+
* POST /message - Receive messages from SSE clients (legacy MCP transport)
|
|
625
|
+
*/
|
|
626
|
+
app.post("/message", authMiddleware, async (req, res) => {
|
|
627
|
+
const sessionId = req.query.sessionId;
|
|
628
|
+
if (!sessionId) {
|
|
629
|
+
res.status(400).json({ error: "Missing sessionId query parameter" });
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const transport = sseTransports.get(sessionId);
|
|
633
|
+
if (!transport) {
|
|
634
|
+
res.status(404).json({
|
|
635
|
+
error: "Session not found. The SSE connection may have been closed.",
|
|
636
|
+
});
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
console.log(`[SSE] POST /message for session ${sessionId}`);
|
|
640
|
+
try {
|
|
641
|
+
await transport.handlePostMessage(req, res, req.body);
|
|
642
|
+
}
|
|
643
|
+
catch (error) {
|
|
644
|
+
console.error(`[SSE] Error handling message:`, error);
|
|
645
|
+
if (!res.headersSent) {
|
|
646
|
+
res.status(500).json({ error: "Internal server error" });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
// ============================================================================
|
|
651
|
+
// LLM Documentation Endpoints (llms.txt standard)
|
|
652
|
+
// ============================================================================
|
|
653
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
654
|
+
function serveLlmsFile(filename, res) {
|
|
655
|
+
const filePath = path.resolve(__dirname, "../", filename);
|
|
656
|
+
try {
|
|
657
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
658
|
+
res.type("text/markdown; charset=utf-8").send(content);
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
res.status(404).send(`${filename} not found`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
app.get("/llms.txt", (_req, res) => {
|
|
665
|
+
serveLlmsFile("llms.txt", res);
|
|
666
|
+
});
|
|
667
|
+
app.get("/llms-full.txt", (_req, res) => {
|
|
668
|
+
serveLlmsFile("llms-full.txt", res);
|
|
669
|
+
});
|
|
670
|
+
// ============================================================================
|
|
512
671
|
// Health & Status Endpoints
|
|
513
672
|
// ============================================================================
|
|
514
673
|
/**
|
|
@@ -523,16 +682,20 @@ app.get("/health", (req, res) => {
|
|
|
523
682
|
});
|
|
524
683
|
});
|
|
525
684
|
/**
|
|
526
|
-
* GET / - Server info (only when no auth header
|
|
685
|
+
* GET / - Server info (only when no auth header and not an MCP/SSE client)
|
|
527
686
|
*/
|
|
528
687
|
app.get("/", (req, res, next) => {
|
|
529
688
|
// If auth header is present, treat as MCP request
|
|
530
689
|
if (req.headers.authorization) {
|
|
531
690
|
return next();
|
|
532
691
|
}
|
|
692
|
+
// If client wants SSE or MCP, let it fall through to authMiddleware (→ 401 triggers OAuth)
|
|
693
|
+
if (req.headers.accept?.includes("text/event-stream")) {
|
|
694
|
+
return next();
|
|
695
|
+
}
|
|
533
696
|
res.json({
|
|
534
697
|
name: "InfraNodus MCP Server",
|
|
535
|
-
version: "1.
|
|
698
|
+
version: "1.4.0",
|
|
536
699
|
description: "MCP server for InfraNodus knowledge graph analysis",
|
|
537
700
|
endpoints: {
|
|
538
701
|
oauth: {
|
|
@@ -546,6 +709,10 @@ app.get("/", (req, res, next) => {
|
|
|
546
709
|
disconnect: "DELETE / or DELETE /mcp",
|
|
547
710
|
},
|
|
548
711
|
health: "GET /health",
|
|
712
|
+
llms: {
|
|
713
|
+
index: "GET /llms.txt",
|
|
714
|
+
full: "GET /llms-full.txt",
|
|
715
|
+
},
|
|
549
716
|
},
|
|
550
717
|
authentication: "Bearer token (obtain via /oauth/token with api_key)",
|
|
551
718
|
});
|
|
@@ -566,11 +733,20 @@ app.post("/", authMiddleware, async (req, res) => {
|
|
|
566
733
|
}
|
|
567
734
|
});
|
|
568
735
|
/**
|
|
569
|
-
* GET / with auth -
|
|
736
|
+
* GET / with auth - Auto-detect transport:
|
|
737
|
+
* SSE client (no mcp-session-id, Accept: text/event-stream) → SSE transport
|
|
738
|
+
* Streamable HTTP client (has mcp-session-id) → existing Streamable HTTP
|
|
570
739
|
*/
|
|
571
740
|
app.get("/", authMiddleware, async (req, res) => {
|
|
572
741
|
try {
|
|
573
|
-
|
|
742
|
+
const wantsSSE = !req.headers["mcp-session-id"] &&
|
|
743
|
+
req.headers.accept?.includes("text/event-stream");
|
|
744
|
+
if (wantsSSE) {
|
|
745
|
+
await handleSseConnection(req, res);
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
await handleMcpRequest(req, res);
|
|
749
|
+
}
|
|
574
750
|
}
|
|
575
751
|
catch (error) {
|
|
576
752
|
console.error("MCP GET / error:", error);
|
|
@@ -599,10 +775,12 @@ app.delete("/", authMiddleware, async (req, res) => {
|
|
|
599
775
|
// Handle graceful shutdown
|
|
600
776
|
process.on("SIGINT", () => {
|
|
601
777
|
console.log("\nShutting down HTTP server...");
|
|
602
|
-
// Close all transports
|
|
603
778
|
for (const transport of transports.values()) {
|
|
604
779
|
transport.close();
|
|
605
780
|
}
|
|
781
|
+
for (const transport of sseTransports.values()) {
|
|
782
|
+
transport.close();
|
|
783
|
+
}
|
|
606
784
|
process.exit(0);
|
|
607
785
|
});
|
|
608
786
|
process.on("SIGTERM", () => {
|
|
@@ -610,6 +788,9 @@ process.on("SIGTERM", () => {
|
|
|
610
788
|
for (const transport of transports.values()) {
|
|
611
789
|
transport.close();
|
|
612
790
|
}
|
|
791
|
+
for (const transport of sseTransports.values()) {
|
|
792
|
+
transport.close();
|
|
793
|
+
}
|
|
613
794
|
process.exit(0);
|
|
614
795
|
});
|
|
615
796
|
// Start the server
|
|
@@ -618,6 +799,7 @@ app.listen(PORT, () => {
|
|
|
618
799
|
console.log(` - Health: http://localhost:${PORT}/health`);
|
|
619
800
|
console.log(` - OAuth token: POST http://localhost:${PORT}/oauth/token`);
|
|
620
801
|
console.log(` - MCP endpoint: http://localhost:${PORT}/mcp`);
|
|
802
|
+
console.log(` - SSE endpoint: http://localhost:${PORT}/sse (legacy)`);
|
|
621
803
|
console.log("");
|
|
622
804
|
console.log("Environment:");
|
|
623
805
|
console.log(` - CORS_ORIGIN: ${CORS_ORIGIN}`);
|