From e1b6d7caaeb672412538e48d4898ea0d5d8fa1c0 Mon Sep 17 00:00:00 2001 From: buggyhunter <82619612+buggyhunter@users.noreply.github.com> Date: Fri, 18 Jul 2025 11:10:45 +0300 Subject: [PATCH 1/8] CTX7-272: ip address header --- src/index.ts | 16 ++++++++++++---- src/lib/api.ts | 29 +++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/index.ts b/src/index.ts index bc1b3f9..898e30f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import { createServer } from "http"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { Command } from "commander"; +import { IncomingMessage } from "http"; const DEFAULT_MINIMUM_TOKENS = 10000; @@ -46,8 +47,12 @@ const CLI_PORT = (() => { // Store SSE transports by session ID const sseTransports: Record = {}; +function getClientIp(req: IncomingMessage): string | undefined { + return req.socket?.remoteAddress || undefined; +} + // Function to create a new server instance with all tools registered -function createServerInstance() { +function createServerInstance(clientIp?: string) { const server = new McpServer( { name: "Context7", @@ -87,7 +92,7 @@ For ambiguous queries, request clarification before proceeding with a best-guess .describe("Library name to search for and retrieve a Context7-compatible library ID."), }, async ({ libraryName }) => { - const searchResponse: SearchResponse = await searchLibraries(libraryName); + const searchResponse: SearchResponse = await searchLibraries(libraryName, clientIp); if (!searchResponse.results || searchResponse.results.length === 0) { return { @@ -154,7 +159,7 @@ ${resultsText}`, const fetchDocsResponse = await fetchLibraryDocumentation(context7CompatibleLibraryID, { tokens, topic, - }); + }, clientIp); if (!fetchDocsResponse) { return { @@ -205,8 +210,11 @@ async function main() { } try { + // Extract client IP address using socket remote address (most reliable) + const clientIp = getClientIp(req); + // Create new server instance for each request - const requestServer = createServerInstance(); + const requestServer = createServerInstance(clientIp); if (url === "/mcp") { const transport = new StreamableHTTPServerTransport({ diff --git a/src/lib/api.ts b/src/lib/api.ts index 026e1b0..2a27ff8 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -6,13 +6,20 @@ const DEFAULT_TYPE = "txt"; /** * Searches for libraries matching the given query * @param query The search query + * @param clientIp Optional client IP address to include in headers * @returns Search results or null if the request fails */ -export async function searchLibraries(query: string): Promise { +export async function searchLibraries(query: string, clientIp?: string): Promise { try { const url = new URL(`${CONTEXT7_API_BASE_URL}/v1/search`); url.searchParams.set("query", query); - const response = await fetch(url); + + const headers: Record = {}; + if (clientIp) { + headers["X-Client-IP"] = clientIp; + } + + const response = await fetch(url, { headers }); if (!response.ok) { const errorCode = response.status; if (errorCode === 429) { @@ -39,6 +46,7 @@ export async function searchLibraries(query: string): Promise { * Fetches documentation context for a specific library * @param libraryId The library ID to fetch documentation for * @param options Options for the request + * @param clientIp Optional client IP address to include in headers * @returns The documentation text or null if the request fails */ export async function fetchLibraryDocumentation( @@ -46,7 +54,8 @@ export async function fetchLibraryDocumentation( options: { tokens?: number; topic?: string; - } = {} + } = {}, + clientIp?: string ): Promise { try { if (libraryId.startsWith("/")) { @@ -56,11 +65,15 @@ export async function fetchLibraryDocumentation( if (options.tokens) url.searchParams.set("tokens", options.tokens.toString()); if (options.topic) url.searchParams.set("topic", options.topic); url.searchParams.set("type", DEFAULT_TYPE); - const response = await fetch(url, { - headers: { - "X-Context7-Source": "mcp-server", - }, - }); + + const headers: Record = { + "X-Context7-Source": "mcp-server", + }; + if (clientIp) { + headers["X-Client-IP"] = clientIp; + } + + const response = await fetch(url, { headers }); if (!response.ok) { const errorCode = response.status; if (errorCode === 429) { From 52a62e6129ba81fd028d5f75e1afbab3ca120033 Mon Sep 17 00:00:00 2001 From: buggyhunter <82619612+buggyhunter@users.noreply.github.com> Date: Fri, 18 Jul 2025 18:18:55 +0300 Subject: [PATCH 2/8] CTX7-272: ip address header --- src/index.ts | 9 +++++++++ src/lib/api.ts | 44 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 898e30f..7d59335 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,6 +48,15 @@ const CLI_PORT = (() => { const sseTransports: Record = {}; function getClientIp(req: IncomingMessage): string | undefined { + // Check for X-Forwarded-For header (set by AWS ELB and other load balancers) + const forwardedFor = req.headers["x-forwarded-for"]; + if (forwardedFor) { + // X-Forwarded-For can contain multiple IPs, take the first one + const ips = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor; + return ips.split(",")[0].trim(); + } + + // Fall back to socket remote address return req.socket?.remoteAddress || undefined; } diff --git a/src/lib/api.ts b/src/lib/api.ts index 2a27ff8..b5563d8 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,8 +1,48 @@ import { SearchResponse } from "./types.js"; +import { createCipheriv, createDecipheriv, randomBytes } from "crypto"; const CONTEXT7_API_BASE_URL = "https://context7.com/api"; const DEFAULT_TYPE = "txt"; +// Encryption configuration +const ENCRYPTION_KEY = process.env.CLIENT_IP_ENCRYPTION_KEY || "default"; +const ALGORITHM = 'aes-256-cbc'; + +function encryptClientIp(clientIp: string): string { + + try { + const iv = randomBytes(16); + const cipher = createCipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, 'hex'), iv); + let encrypted = cipher.update(clientIp, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + return iv.toString('hex') + ':' + encrypted; + } catch (error) { + console.error("Error encrypting client IP:", error); + return clientIp; // Fallback to unencrypted + } +} + +export function decryptClientIp(encryptedIp: string): string | null { + + try { + const parts = encryptedIp.split(':'); + if (parts.length !== 2) { + // Not encrypted, return as-is + return encryptedIp; + } + + const iv = Buffer.from(parts[0], 'hex'); + const encrypted = parts[1]; + const decipher = createDecipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, 'hex'), iv); + let decrypted = decipher.update(encrypted, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; + } catch (error) { + console.error("Error decrypting client IP:", error); + return null; + } +} + /** * Searches for libraries matching the given query * @param query The search query @@ -16,7 +56,7 @@ export async function searchLibraries(query: string, clientIp?: string): Promise const headers: Record = {}; if (clientIp) { - headers["X-Client-IP"] = clientIp; + headers["X-Client-IP"] = encryptClientIp(clientIp); } const response = await fetch(url, { headers }); @@ -70,7 +110,7 @@ export async function fetchLibraryDocumentation( "X-Context7-Source": "mcp-server", }; if (clientIp) { - headers["X-Client-IP"] = clientIp; + headers["X-Client-IP"] = encryptClientIp(clientIp); } const response = await fetch(url, { headers }); From 7afcced137a32938fdb9bfb6d88392ab88ed02ac Mon Sep 17 00:00:00 2001 From: buggyhunter <82619612+buggyhunter@users.noreply.github.com> Date: Fri, 18 Jul 2025 19:01:03 +0300 Subject: [PATCH 3/8] CTX7-272: ip address header --- src/lib/api.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/lib/api.ts b/src/lib/api.ts index b5563d8..bc5eba4 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -5,10 +5,20 @@ const CONTEXT7_API_BASE_URL = "https://context7.com/api"; const DEFAULT_TYPE = "txt"; // Encryption configuration -const ENCRYPTION_KEY = process.env.CLIENT_IP_ENCRYPTION_KEY || "default"; +const ENCRYPTION_KEY = process.env.CLIENT_IP_ENCRYPTION_KEY || "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; const ALGORITHM = 'aes-256-cbc'; +// Validate encryption key +function validateEncryptionKey(key: string): boolean { + // Must be exactly 64 hex characters (32 bytes) + return /^[0-9a-fA-F]{64}$/.test(key); +} + function encryptClientIp(clientIp: string): string { + if (!validateEncryptionKey(ENCRYPTION_KEY)) { + console.error("Invalid encryption key format. Must be 64 hex characters."); + return clientIp; // Fallback to unencrypted + } try { const iv = randomBytes(16); @@ -23,6 +33,10 @@ function encryptClientIp(clientIp: string): string { } export function decryptClientIp(encryptedIp: string): string | null { + if (!validateEncryptionKey(ENCRYPTION_KEY)) { + console.error("Invalid encryption key format. Cannot decrypt."); + return null; + } try { const parts = encryptedIp.split(':'); From 086d4c2083a97fb445b65440ccf0e4960158fd1b Mon Sep 17 00:00:00 2001 From: buggyhunter <82619612+buggyhunter@users.noreply.github.com> Date: Fri, 18 Jul 2025 19:21:46 +0300 Subject: [PATCH 4/8] CTX7-272: ip address header --- src/lib/api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/api.ts b/src/lib/api.ts index bc5eba4..ffb4e16 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -70,7 +70,7 @@ export async function searchLibraries(query: string, clientIp?: string): Promise const headers: Record = {}; if (clientIp) { - headers["X-Client-IP"] = encryptClientIp(clientIp); + headers["mcp-client-ip"] = encryptClientIp(clientIp); } const response = await fetch(url, { headers }); @@ -124,7 +124,7 @@ export async function fetchLibraryDocumentation( "X-Context7-Source": "mcp-server", }; if (clientIp) { - headers["X-Client-IP"] = encryptClientIp(clientIp); + headers["mcp-client-ip"] = encryptClientIp(clientIp); } const response = await fetch(url, { headers }); From 294df1d4872eaff4c813f3d68143648c29764be8 Mon Sep 17 00:00:00 2001 From: buggyhunter <82619612+buggyhunter@users.noreply.github.com> Date: Fri, 18 Jul 2025 19:24:48 +0300 Subject: [PATCH 5/8] CTX7-272: ip address header --- src/index.ts | 16 ++++++++++------ src/lib/api.ts | 38 ++++++++++++++++++++------------------ 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7d59335..6af25d4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -55,7 +55,7 @@ function getClientIp(req: IncomingMessage): string | undefined { const ips = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor; return ips.split(",")[0].trim(); } - + // Fall back to socket remote address return req.socket?.remoteAddress || undefined; } @@ -165,10 +165,14 @@ ${resultsText}`, ), }, async ({ context7CompatibleLibraryID, tokens = DEFAULT_MINIMUM_TOKENS, topic = "" }) => { - const fetchDocsResponse = await fetchLibraryDocumentation(context7CompatibleLibraryID, { - tokens, - topic, - }, clientIp); + const fetchDocsResponse = await fetchLibraryDocumentation( + context7CompatibleLibraryID, + { + tokens, + topic, + }, + clientIp + ); if (!fetchDocsResponse) { return { @@ -221,7 +225,7 @@ async function main() { try { // Extract client IP address using socket remote address (most reliable) const clientIp = getClientIp(req); - + // Create new server instance for each request const requestServer = createServerInstance(clientIp); diff --git a/src/lib/api.ts b/src/lib/api.ts index ffb4e16..bb84cc0 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -5,8 +5,10 @@ const CONTEXT7_API_BASE_URL = "https://context7.com/api"; const DEFAULT_TYPE = "txt"; // Encryption configuration -const ENCRYPTION_KEY = process.env.CLIENT_IP_ENCRYPTION_KEY || "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; -const ALGORITHM = 'aes-256-cbc'; +const ENCRYPTION_KEY = + process.env.CLIENT_IP_ENCRYPTION_KEY || + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; +const ALGORITHM = "aes-256-cbc"; // Validate encryption key function validateEncryptionKey(key: string): boolean { @@ -19,13 +21,13 @@ function encryptClientIp(clientIp: string): string { console.error("Invalid encryption key format. Must be 64 hex characters."); return clientIp; // Fallback to unencrypted } - + try { const iv = randomBytes(16); - const cipher = createCipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, 'hex'), iv); - let encrypted = cipher.update(clientIp, 'utf8', 'hex'); - encrypted += cipher.final('hex'); - return iv.toString('hex') + ':' + encrypted; + const cipher = createCipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, "hex"), iv); + let encrypted = cipher.update(clientIp, "utf8", "hex"); + encrypted += cipher.final("hex"); + return iv.toString("hex") + ":" + encrypted; } catch (error) { console.error("Error encrypting client IP:", error); return clientIp; // Fallback to unencrypted @@ -37,19 +39,19 @@ export function decryptClientIp(encryptedIp: string): string | null { console.error("Invalid encryption key format. Cannot decrypt."); return null; } - + try { - const parts = encryptedIp.split(':'); + const parts = encryptedIp.split(":"); if (parts.length !== 2) { // Not encrypted, return as-is return encryptedIp; } - - const iv = Buffer.from(parts[0], 'hex'); + + const iv = Buffer.from(parts[0], "hex"); const encrypted = parts[1]; - const decipher = createDecipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, 'hex'), iv); - let decrypted = decipher.update(encrypted, 'hex', 'utf8'); - decrypted += decipher.final('utf8'); + const decipher = createDecipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, "hex"), iv); + let decrypted = decipher.update(encrypted, "hex", "utf8"); + decrypted += decipher.final("utf8"); return decrypted; } catch (error) { console.error("Error decrypting client IP:", error); @@ -67,12 +69,12 @@ export async function searchLibraries(query: string, clientIp?: string): Promise try { const url = new URL(`${CONTEXT7_API_BASE_URL}/v1/search`); url.searchParams.set("query", query); - + const headers: Record = {}; if (clientIp) { headers["mcp-client-ip"] = encryptClientIp(clientIp); } - + const response = await fetch(url, { headers }); if (!response.ok) { const errorCode = response.status; @@ -119,14 +121,14 @@ export async function fetchLibraryDocumentation( if (options.tokens) url.searchParams.set("tokens", options.tokens.toString()); if (options.topic) url.searchParams.set("topic", options.topic); url.searchParams.set("type", DEFAULT_TYPE); - + const headers: Record = { "X-Context7-Source": "mcp-server", }; if (clientIp) { headers["mcp-client-ip"] = encryptClientIp(clientIp); } - + const response = await fetch(url, { headers }); if (!response.ok) { const errorCode = response.status; From ddac8928a6fdc82ce7e85ba7509b331fcc022ed5 Mon Sep 17 00:00:00 2001 From: buggyhunter <82619612+buggyhunter@users.noreply.github.com> Date: Fri, 18 Jul 2025 19:26:52 +0300 Subject: [PATCH 6/8] CTX7-272: ip address header --- src/lib/api.ts | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/lib/api.ts b/src/lib/api.ts index bb84cc0..1298ec0 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -34,30 +34,7 @@ function encryptClientIp(clientIp: string): string { } } -export function decryptClientIp(encryptedIp: string): string | null { - if (!validateEncryptionKey(ENCRYPTION_KEY)) { - console.error("Invalid encryption key format. Cannot decrypt."); - return null; - } - try { - const parts = encryptedIp.split(":"); - if (parts.length !== 2) { - // Not encrypted, return as-is - return encryptedIp; - } - - const iv = Buffer.from(parts[0], "hex"); - const encrypted = parts[1]; - const decipher = createDecipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, "hex"), iv); - let decrypted = decipher.update(encrypted, "hex", "utf8"); - decrypted += decipher.final("utf8"); - return decrypted; - } catch (error) { - console.error("Error decrypting client IP:", error); - return null; - } -} /** * Searches for libraries matching the given query From 8b884f2a38ad6cb4ff2d81d2019f1e9b63bb37d4 Mon Sep 17 00:00:00 2001 From: buggyhunter <82619612+buggyhunter@users.noreply.github.com> Date: Fri, 18 Jul 2025 19:28:55 +0300 Subject: [PATCH 7/8] CTX7-272: ip address header --- src/lib/api.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/api.ts b/src/lib/api.ts index 1298ec0..41d4e7a 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,5 +1,5 @@ import { SearchResponse } from "./types.js"; -import { createCipheriv, createDecipheriv, randomBytes } from "crypto"; +import { createCipheriv, randomBytes } from "crypto"; const CONTEXT7_API_BASE_URL = "https://context7.com/api"; const DEFAULT_TYPE = "txt"; @@ -34,8 +34,6 @@ function encryptClientIp(clientIp: string): string { } } - - /** * Searches for libraries matching the given query * @param query The search query From 2ba1b619609f6d3e2b4d52409a38543ffe1419f6 Mon Sep 17 00:00:00 2001 From: enesgules Date: Fri, 18 Jul 2025 20:15:16 +0300 Subject: [PATCH 8/8] refactor: header generation and ip encryption --- src/lib/api.ts | 44 +++---------------------------------------- src/lib/encryption.ts | 40 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 41 deletions(-) create mode 100644 src/lib/encryption.ts diff --git a/src/lib/api.ts b/src/lib/api.ts index 41d4e7a..208cafa 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,39 +1,9 @@ import { SearchResponse } from "./types.js"; -import { createCipheriv, randomBytes } from "crypto"; +import { generateHeaders } from "./encryption.js"; const CONTEXT7_API_BASE_URL = "https://context7.com/api"; const DEFAULT_TYPE = "txt"; -// Encryption configuration -const ENCRYPTION_KEY = - process.env.CLIENT_IP_ENCRYPTION_KEY || - "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; -const ALGORITHM = "aes-256-cbc"; - -// Validate encryption key -function validateEncryptionKey(key: string): boolean { - // Must be exactly 64 hex characters (32 bytes) - return /^[0-9a-fA-F]{64}$/.test(key); -} - -function encryptClientIp(clientIp: string): string { - if (!validateEncryptionKey(ENCRYPTION_KEY)) { - console.error("Invalid encryption key format. Must be 64 hex characters."); - return clientIp; // Fallback to unencrypted - } - - try { - const iv = randomBytes(16); - const cipher = createCipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, "hex"), iv); - let encrypted = cipher.update(clientIp, "utf8", "hex"); - encrypted += cipher.final("hex"); - return iv.toString("hex") + ":" + encrypted; - } catch (error) { - console.error("Error encrypting client IP:", error); - return clientIp; // Fallback to unencrypted - } -} - /** * Searches for libraries matching the given query * @param query The search query @@ -45,10 +15,7 @@ export async function searchLibraries(query: string, clientIp?: string): Promise const url = new URL(`${CONTEXT7_API_BASE_URL}/v1/search`); url.searchParams.set("query", query); - const headers: Record = {}; - if (clientIp) { - headers["mcp-client-ip"] = encryptClientIp(clientIp); - } + const headers = generateHeaders(clientIp); const response = await fetch(url, { headers }); if (!response.ok) { @@ -97,12 +64,7 @@ export async function fetchLibraryDocumentation( if (options.topic) url.searchParams.set("topic", options.topic); url.searchParams.set("type", DEFAULT_TYPE); - const headers: Record = { - "X-Context7-Source": "mcp-server", - }; - if (clientIp) { - headers["mcp-client-ip"] = encryptClientIp(clientIp); - } + const headers = generateHeaders(clientIp, { "X-Context7-Source": "mcp-server" }); const response = await fetch(url, { headers }); if (!response.ok) { diff --git a/src/lib/encryption.ts b/src/lib/encryption.ts new file mode 100644 index 0000000..fae084b --- /dev/null +++ b/src/lib/encryption.ts @@ -0,0 +1,40 @@ +import { createCipheriv, randomBytes } from "crypto"; + +export const ENCRYPTION_KEY = + process.env.CLIENT_IP_ENCRYPTION_KEY || + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; +export const ALGORITHM = "aes-256-cbc"; + +export function validateEncryptionKey(key: string): boolean { + // Must be exactly 64 hex characters (32 bytes) + return /^[0-9a-fA-F]{64}$/.test(key); +} + +export function encryptClientIp(clientIp: string): string { + if (!validateEncryptionKey(ENCRYPTION_KEY)) { + console.error("Invalid encryption key format. Must be 64 hex characters."); + return clientIp; // Fallback to unencrypted + } + + try { + const iv = randomBytes(16); + const cipher = createCipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, "hex"), iv); + let encrypted = cipher.update(clientIp, "utf8", "hex"); + encrypted += cipher.final("hex"); + return iv.toString("hex") + ":" + encrypted; + } catch (error) { + console.error("Error encrypting client IP:", error); + return clientIp; // Fallback to unencrypted + } +} + +export function generateHeaders( + clientIp?: string, + extraHeaders: Record = {} +): Record { + const headers: Record = { ...extraHeaders }; + if (clientIp) { + headers["mcp-client-ip"] = encryptClientIp(clientIp); + } + return headers; +}