2025-10-26 10:57:39 +00:00
#!/usr/bin/env node
import express from 'express' ;
import cors from 'cors' ;
import compression from 'compression' ;
import rateLimit from 'express-rate-limit' ;
import { Server } from '@modelcontextprotocol/sdk/server/index.js' ;
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js' ;
import {
CallToolRequestSchema ,
ListToolsRequestSchema ,
ListResourcesRequestSchema ,
ReadResourceRequestSchema ,
} from '@modelcontextprotocol/sdk/types.js' ;
import HPRDataLoader from './data-loader.js' ;
// Configuration
const PORT = process . env . PORT || 3000 ;
const MAX _CONCURRENT _REQUESTS = 10 ;
2025-10-26 14:09:01 +00:00
// Increased the timeout for the long-lived SSE connection connect() call
const REQUEST _TIMEOUT _MS = 60000 ; // 60 seconds (was 30s)
2025-10-26 10:57:39 +00:00
const RATE _LIMIT _WINDOW _MS = 60000 ; // 1 minute
2025-10-26 14:09:01 +00:00
const RATE _LIMIT _MAX _REQUESTS = 100 ; // 100 requests per minute per IP
2025-10-26 10:57:39 +00:00
const MEMORY _THRESHOLD _MB = 450 ;
const CIRCUIT _BREAKER _THRESHOLD = 5 ;
2025-10-26 14:09:01 +00:00
const CIRCUIT _BREAKER _TIMEOUT _MS = 60000 ; // 60 seconds (how long it stays OPEN)
2025-10-26 12:50:18 +00:00
const SSE _HEARTBEAT _INTERVAL _MS = 20000 ; // 20 seconds to prevent proxy timeout
2025-10-26 10:57:39 +00:00
// Initialize data loader
console . error ( 'Loading HPR knowledge base data...' ) ;
const dataLoader = new HPRDataLoader ( ) ;
await dataLoader . load ( ) ;
console . error ( 'Data loaded successfully!' ) ;
2025-10-26 14:09:01 +00:00
// Map to store active SSE transports, keyed by connectionId
const activeSseTransports = new Map ( ) ;
2025-10-26 10:57:39 +00:00
// Circuit Breaker class for graceful degradation
class CircuitBreaker {
constructor ( threshold = CIRCUIT _BREAKER _THRESHOLD , timeout = CIRCUIT _BREAKER _TIMEOUT _MS ) {
this . failures = 0 ;
this . threshold = threshold ;
this . timeout = timeout ;
this . state = 'CLOSED' ; // CLOSED, OPEN, HALF_OPEN
this . nextAttempt = Date . now ( ) ;
}
async execute ( fn ) {
if ( this . state === 'OPEN' ) {
if ( Date . now ( ) < this . nextAttempt ) {
throw new Error ( 'Service temporarily unavailable. Please try again later.' ) ;
}
this . state = 'HALF_OPEN' ;
}
try {
const result = await fn ( ) ;
if ( this . state === 'HALF_OPEN' ) {
this . state = 'CLOSED' ;
this . failures = 0 ;
}
return result ;
} catch ( error ) {
this . failures ++ ;
if ( this . failures >= this . threshold ) {
this . state = 'OPEN' ;
this . nextAttempt = Date . now ( ) + this . timeout ;
console . error ( ` Circuit breaker opened after ${ this . failures } failures ` ) ;
}
throw error ;
}
}
reset ( ) {
this . failures = 0 ;
this . state = 'CLOSED' ;
}
}
const circuitBreaker = new CircuitBreaker ( ) ;
// Request timeout wrapper
function withTimeout ( promise , timeoutMs = REQUEST _TIMEOUT _MS ) {
return Promise . race ( [
promise ,
new Promise ( ( _ , reject ) =>
setTimeout ( ( ) => reject ( new Error ( 'Request timeout' ) ) , timeoutMs )
) ,
] ) ;
}
// Concurrent request limiter
let activeRequests = 0 ;
function checkConcurrency ( ) {
if ( activeRequests >= MAX _CONCURRENT _REQUESTS ) {
throw new Error ( 'Server at capacity. Please try again later.' ) ;
}
}
// Memory monitoring
let memoryWarning = false ;
function checkMemory ( ) {
const usage = process . memoryUsage ( ) ;
const heapUsedMB = usage . heapUsed / 1024 / 1024 ;
if ( heapUsedMB > MEMORY _THRESHOLD _MB ) {
if ( ! memoryWarning ) {
console . error ( ` High memory usage: ${ heapUsedMB . toFixed ( 2 ) } MB ` ) ;
memoryWarning = true ;
}
throw new Error ( 'Server under high load. Please try again later.' ) ;
} else if ( memoryWarning && heapUsedMB < MEMORY _THRESHOLD _MB * 0.8 ) {
memoryWarning = false ;
}
}
setInterval ( ( ) => {
const usage = process . memoryUsage ( ) ;
const heapUsedMB = usage . heapUsed / 1024 / 1024 ;
console . error ( ` Memory: ${ heapUsedMB . toFixed ( 2 ) } MB, Active requests: ${ activeRequests } ` ) ;
} , 30000 ) ;
// Helper function to strip HTML tags
function stripHtml ( html ) {
return html
. replace ( /<[^>]*>/g , '' )
. replace ( / /g , ' ' )
. replace ( /</g , '<' )
. replace ( />/g , '>' )
. replace ( /&/g , '&' )
. replace ( /"/g , '"' )
. trim ( ) ;
}
// Helper to format episode for display
function formatEpisode ( episode , includeNotes = false ) {
const host = dataLoader . getHost ( episode . hostid ) ;
const seriesInfo = episode . series !== 0 ? dataLoader . getSeries ( episode . series ) : null ;
let result = ` # HPR ${ String ( episode . id ) . padStart ( 4 , '0' ) } : ${ episode . title }
* * Date : * * $ { episode . date }
* * Host : * * $ { host ? . host || 'Unknown' } ( ID : $ { episode . hostid } )
* * Duration : * * $ { Math . floor ( episode . duration / 60 ) } : $ { String ( episode . duration % 60 ) . padStart ( 2 , '0' ) }
* * Tags : * * $ { episode . tags }
* * License : * * $ { episode . license }
* * Downloads : * * $ { episode . downloads }
# # Summary
$ { episode . summary } ` ;
if ( seriesInfo ) {
2025-10-26 14:09:01 +00:00
result += `
# # Series
2025-10-26 10:57:39 +00:00
* * $ { seriesInfo . name } * * : $ { stripHtml ( seriesInfo . description ) } ` ;
}
if ( includeNotes && episode . notes ) {
2025-10-26 14:09:01 +00:00
result += `
# # Host Notes
$ { stripHtml ( episode . notes ) } ` ;
2025-10-26 10:57:39 +00:00
}
return result ;
}
2025-10-26 14:46:24 +00:00
function formatTranscriptSearchResults ( results , args ) {
if ( results . length === 0 ) {
return '' ;
}
const descriptorParts = [ ] ;
if ( args . query ) {
descriptorParts . push ( ` phrase=" ${ args . query } " ` ) ;
}
if ( Array . isArray ( args . terms ) && args . terms . length > 0 ) {
descriptorParts . push ( ` terms=[ ${ args . terms . join ( ', ' ) } ] ` ) ;
}
if ( descriptorParts . length === 0 ) {
descriptorParts . push ( '"no explicit query provided"' ) ;
}
const firstSummary = results [ 0 ] ? . matchSummary || { } ;
const matchMode = firstSummary . matchMode || 'phrase' ;
const contextLines = args . contextLines ? ? 3 ;
const caseSensitive = args . caseSensitive ? 'yes' : 'no' ;
const wholeWord = args . wholeWord ? 'yes' : 'no' ;
const maxMatches = args . maxMatchesPerEpisode ? ? 5 ;
const hostFilters = [ ] ;
if ( args . hostId ) hostFilters . push ( ` ID ${ args . hostId } ` ) ;
if ( args . hostName ) hostFilters . push ( ` name " ${ args . hostName } " ` ) ;
let text = ` # Transcript Search Results ( ${ results . length } episodes) \n \n ` ;
text += ` Searching for: ${ descriptorParts . join ( ' | ' ) } \n ` ;
text += ` Match mode: ${ matchMode } | Context lines: ${ contextLines } | Case sensitive: ${ caseSensitive } | Whole word: ${ wholeWord } \n ` ;
text += ` Maximum matches per episode: ${ maxMatches } \n ` ;
if ( hostFilters . length > 0 ) {
text += ` Host filter: ${ hostFilters . join ( ' & ' ) } \n ` ;
}
text += '\n## Summary\n' ;
text += results . map ( result => {
const host = dataLoader . getHost ( result . episode . hostid ) ;
const matchedTerms = result . matchSummary . matchedTerms . length > 0
? result . matchSummary . matchedTerms . join ( ', ' )
: 'N/A' ;
const termCounts = Object . entries ( result . matchSummary . termHitCounts || { } ) ;
const termCountText = termCounts . length > 0
? termCounts . map ( ( [ term , count ] ) => ` ${ term } : ${ count } ` ) . join ( ', ' )
: null ;
const truncatedNote = result . matchSummary . truncated ? ' (truncated)' : '' ;
let line = ` - HPR ${ String ( result . episode . id ) . padStart ( 4 , '0' ) } : ${ result . episode . title } — ${ result . matchSummary . totalMatches } match ${ result . matchSummary . totalMatches === 1 ? '' : 'es' } ${ truncatedNote } ; terms: ${ matchedTerms } ` ;
if ( termCountText ) {
line += ` ( ${ termCountText } ) ` ;
}
line += ` | Host: ${ host ? . host || 'Unknown' } ( ${ result . episode . date } ) ` ;
return line ;
} ) . join ( '\n' ) ;
text += '\n\n' ;
results . forEach ( result => {
const host = dataLoader . getHost ( result . episode . hostid ) ;
const matchedTerms = result . matchSummary . matchedTerms . length > 0
? result . matchSummary . matchedTerms . join ( ', ' )
: 'N/A' ;
const termCounts = Object . entries ( result . matchSummary . termHitCounts || { } ) ;
const termCountText = termCounts . length > 0
? termCounts . map ( ( [ term , count ] ) => ` ${ term } : ${ count } ` ) . join ( ', ' )
: null ;
text += ` ## HPR ${ String ( result . episode . id ) . padStart ( 4 , '0' ) } : ${ result . episode . title }
* * Host : * * $ { host ? . host || 'Unknown' } | * * Date : * * $ { result . episode . date }
* * Matched terms : * * $ { matchedTerms }
* * Matches captured : * * $ { result . matchSummary . totalMatches } $ { result . matchSummary . truncated ? ' (additional matches omitted after reaching limit)' : '' }
` ;
if ( termCountText ) {
text += ` **Term counts:** ${ termCountText } \n ` ;
}
text += '\n' ;
result . matches . forEach ( ( match , index ) => {
const termInfo = match . terms && match . terms . length > 0
? ` | terms: ${ match . terms . join ( ', ' ) } `
: '' ;
text += ` ### Match ${ index + 1 } (line ${ match . lineNumber } ${ termInfo } )
\ ` \` \`
$ { match . context }
\ ` \` \`
` ;
} ) ;
} ) ;
return text ;
}
2025-10-26 10:57:39 +00:00
// Create MCP server factory
function createMCPServer ( ) {
const server = new Server (
{
name : 'hpr-knowledge-base' ,
version : '1.0.0' ,
} ,
{
capabilities : {
tools : { } ,
resources : { } ,
} ,
}
) ;
// List available resources
server . setRequestHandler ( ListResourcesRequestSchema , async ( ) => {
return {
resources : [
{
uri : 'hpr://stats' ,
mimeType : 'text/plain' ,
name : 'HPR Statistics' ,
description : 'Overall statistics about the HPR knowledge base' ,
} ,
{
uri : 'hpr://episodes/recent' ,
mimeType : 'text/plain' ,
name : 'Recent Episodes' ,
description : 'List of 50 most recent HPR episodes' ,
} ,
{
uri : 'hpr://hosts/all' ,
mimeType : 'text/plain' ,
name : 'All Hosts' ,
description : 'List of all HPR hosts' ,
} ,
{
uri : 'hpr://series/all' ,
mimeType : 'text/plain' ,
name : 'All Series' ,
description : 'List of all HPR series' ,
} ,
] ,
} ;
} ) ;
// Read a resource
server . setRequestHandler ( ReadResourceRequestSchema , async ( request ) => {
const uri = request . params . uri ;
if ( uri === 'hpr://stats' ) {
const stats = dataLoader . getStats ( ) ;
return {
contents : [
{
uri ,
mimeType : 'text/plain' ,
text : ` # Hacker Public Radio Statistics
* * Total Episodes : * * $ { stats . totalEpisodes }
* * Total Hosts : * * $ { stats . totalHosts }
* * Total Comments : * * $ { stats . totalComments }
* * Total Series : * * $ { stats . totalSeries }
* * Transcripts Available : * * $ { stats . totalTranscripts }
* * Date Range : * * $ { stats . dateRange . earliest } to $ { stats . dateRange . latest }
Hacker Public Radio is a community - driven podcast released under Creative Commons licenses .
All content is contributed by the community , for the community . ` ,
} ,
] ,
} ;
}
if ( uri === 'hpr://episodes/recent' ) {
const recent = dataLoader . searchEpisodes ( '' , { limit : 50 } ) ;
const text = recent . map ( ep => {
const host = dataLoader . getHost ( ep . hostid ) ;
return ` **HPR ${ String ( ep . id ) . padStart ( 4 , '0' ) } ** ( ${ ep . date } ) - ${ ep . title } by ${ host ? . host || 'Unknown' } ` ;
} ) . join ( '\n' ) ;
return {
contents : [
{
uri ,
mimeType : 'text/plain' ,
text : ` # Recent Episodes \n \n ${ text } ` ,
} ,
] ,
} ;
}
if ( uri === 'hpr://hosts/all' ) {
const hosts = dataLoader . hosts
. filter ( h => h . valid === 1 )
. map ( h => {
const episodeCount = dataLoader . getEpisodesByHost ( h . hostid ) . length ;
return ` ** ${ h . host } ** (ID: ${ h . hostid } ) - ${ episodeCount } episodes ` ;
} )
. join ( '\n' ) ;
return {
contents : [
{
uri ,
mimeType : 'text/plain' ,
text : ` # All HPR Hosts \n \n ${ hosts } ` ,
} ,
] ,
} ;
}
if ( uri === 'hpr://series/all' ) {
const series = dataLoader . series
. filter ( s => s . valid === 1 && s . private === 0 )
. map ( s => {
const episodeCount = dataLoader . getEpisodesInSeries ( s . id ) . length ;
return ` ** ${ s . name } ** (ID: ${ s . id } ) - ${ episodeCount } episodes \n ${ stripHtml ( s . description ) } ` ;
} )
. join ( '\n\n' ) ;
return {
contents : [
{
uri ,
mimeType : 'text/plain' ,
text : ` # All HPR Series \n \n ${ series } ` ,
} ,
] ,
} ;
}
throw new Error ( ` Unknown resource: ${ uri } ` ) ;
} ) ;
// List available tools
server . setRequestHandler ( ListToolsRequestSchema , async ( ) => {
return {
tools : [
{
name : 'search_episodes' ,
description : 'Search HPR episodes by keywords in title, summary, tags, or host notes. Can filter by host, series, tags, and date range.' ,
2025-10-27 14:45:11 +00:00
annotations : {
readOnlyHint : true ,
openWorldHint : true
} ,
2025-10-26 10:57:39 +00:00
inputSchema : {
type : 'object' ,
properties : {
query : {
type : 'string' ,
description : 'Search query (searches title, summary, tags, and notes)' ,
} ,
limit : {
type : 'number' ,
description : 'Maximum number of results to return (default: 20)' ,
} ,
hostId : {
type : 'number' ,
description : 'Filter by host ID' ,
} ,
seriesId : {
type : 'number' ,
description : 'Filter by series ID' ,
} ,
tag : {
type : 'string' ,
description : 'Filter by tag' ,
} ,
fromDate : {
type : 'string' ,
description : 'Filter episodes from this date (YYYY-MM-DD)' ,
} ,
toDate : {
type : 'string' ,
description : 'Filter episodes to this date (YYYY-MM-DD)' ,
} ,
} ,
required : [ ] ,
} ,
} ,
{
name : 'get_episode' ,
description : 'Get detailed information about a specific HPR episode including transcript if available' ,
2025-10-27 14:45:11 +00:00
annotations : {
readOnlyHint : true ,
openWorldHint : true
} ,
2025-10-26 10:57:39 +00:00
inputSchema : {
type : 'object' ,
properties : {
episodeId : {
type : 'number' ,
description : 'Episode ID number' ,
} ,
includeTranscript : {
type : 'boolean' ,
description : 'Include full transcript if available (default: true)' ,
} ,
includeComments : {
type : 'boolean' ,
description : 'Include community comments (default: true)' ,
} ,
} ,
required : [ 'episodeId' ] ,
} ,
} ,
{
name : 'search_transcripts' ,
2025-10-26 14:46:24 +00:00
description : 'Search through episode transcripts using phrases or multiple terms with AND/OR matching and optional host filters' ,
2025-10-27 14:45:11 +00:00
annotations : {
readOnlyHint : true ,
openWorldHint : true
} ,
2025-10-26 10:57:39 +00:00
inputSchema : {
type : 'object' ,
properties : {
query : {
type : 'string' ,
2025-10-26 14:46:24 +00:00
description : 'Search phrase to find in transcripts. Combine with terms/matchMode for advanced searches.' ,
} ,
terms : {
type : 'array' ,
items : { type : 'string' } ,
description : 'Explicit list of terms to search for; useful when pairing with matchMode "any" or "all".' ,
} ,
matchMode : {
type : 'string' ,
enum : [ 'any' , 'all' , 'phrase' ] ,
description : 'How to interpret the query/terms. "phrase" (default) matches the phrase exactly, "any" matches if any term is present, "all" requires every term.' ,
2025-10-26 10:57:39 +00:00
} ,
limit : {
type : 'number' ,
description : 'Maximum number of episodes to return (default: 20)' ,
} ,
contextLines : {
type : 'number' ,
description : 'Number of lines of context around matches (default: 3)' ,
} ,
2025-10-26 14:46:24 +00:00
hostId : {
type : 'number' ,
description : 'Restrict matches to a given host ID.' ,
} ,
hostName : {
type : 'string' ,
description : 'Restrict matches to hosts whose name contains this value.' ,
} ,
caseSensitive : {
type : 'boolean' ,
description : 'Perform a case-sensitive search (default: false).' ,
} ,
wholeWord : {
type : 'boolean' ,
description : 'Match whole words only (default: false).' ,
} ,
maxMatchesPerEpisode : {
type : 'number' ,
description : 'Maximum number of excerpt matches to include per episode (default: 5).' ,
} ,
2025-10-26 10:57:39 +00:00
} ,
2025-10-26 14:46:24 +00:00
required : [ ] ,
2025-10-26 10:57:39 +00:00
} ,
} ,
{
name : 'get_host_info' ,
description : 'Get information about an HPR host including all their episodes' ,
2025-10-27 14:45:11 +00:00
annotations : {
readOnlyHint : true ,
openWorldHint : true
} ,
2025-10-26 10:57:39 +00:00
inputSchema : {
type : 'object' ,
properties : {
hostId : {
type : 'number' ,
description : 'Host ID number' ,
} ,
hostName : {
type : 'string' ,
description : 'Host name (will search if hostId not provided)' ,
} ,
includeEpisodes : {
type : 'boolean' ,
description : 'Include list of all episodes by this host (default: true)' ,
} ,
} ,
required : [ ] ,
} ,
} ,
{
name : 'get_series_info' ,
description : 'Get information about an HPR series including all episodes in the series' ,
2025-10-27 14:45:11 +00:00
annotations : {
readOnlyHint : true ,
openWorldHint : true
} ,
2025-10-26 10:57:39 +00:00
inputSchema : {
type : 'object' ,
properties : {
seriesId : {
type : 'number' ,
description : 'Series ID number' ,
} ,
} ,
required : [ 'seriesId' ] ,
} ,
} ,
] ,
} ;
} ) ;
// Handle tool calls
server . setRequestHandler ( CallToolRequestSchema , async ( request ) => {
const { name , arguments : args } = request . params ;
try {
if ( name === 'search_episodes' ) {
const results = dataLoader . searchEpisodes ( args . query || '' , {
limit : args . limit || 20 ,
hostId : args . hostId ,
seriesId : args . seriesId ,
tag : args . tag ,
fromDate : args . fromDate ,
toDate : args . toDate ,
} ) ;
const text = results . length > 0
? results . map ( ep => formatEpisode ( ep , false ) ) . join ( '\n\n---\n\n' )
: 'No episodes found matching your search criteria.' ;
return {
content : [
{
type : 'text' ,
text : ` # Search Results ( ${ results . length } episodes found) \n \n ${ text } ` ,
} ,
] ,
} ;
}
if ( name === 'get_episode' ) {
const episode = dataLoader . getEpisode ( args . episodeId ) ;
if ( ! episode ) {
return {
content : [
{
type : 'text' ,
text : ` Episode ${ args . episodeId } not found. ` ,
} ,
] ,
} ;
}
let text = formatEpisode ( episode , true ) ;
// Add transcript if requested and available
if ( args . includeTranscript !== false ) {
const transcript = dataLoader . getTranscript ( args . episodeId ) ;
if ( transcript ) {
text += ` \n \n ## Transcript \n \n ${ transcript } ` ;
} else {
text += ` \n \n ## Transcript \n \n *No transcript available for this episode.* ` ;
}
}
// Add comments if requested
if ( args . includeComments !== false ) {
const comments = dataLoader . getCommentsForEpisode ( args . episodeId ) ;
if ( comments . length > 0 ) {
text += ` \n \n ## Comments ( ${ comments . length } ) \n \n ` ;
text += comments . map ( c =>
` ** ${ c . comment _author _name } ** ( ${ c . comment _timestamp } ) ${ c . comment _title ? ` - ${ c . comment _title } ` : '' } \n ${ c . comment _text } `
) . join ( '\n\n---\n\n' ) ;
}
}
return {
content : [
{
type : 'text' ,
text ,
} ,
] ,
} ;
}
if ( name === 'search_transcripts' ) {
2025-10-26 14:46:24 +00:00
const searchOptions = {
2025-10-26 10:57:39 +00:00
limit : args . limit || 20 ,
2025-10-26 14:46:24 +00:00
contextLines : args . contextLines ? ? 3 ,
terms : args . terms ,
matchMode : args . matchMode ,
hostId : args . hostId ,
hostName : args . hostName ,
caseSensitive : args . caseSensitive ,
wholeWord : args . wholeWord ,
maxMatchesPerEpisode : args . maxMatchesPerEpisode ? ? 5 ,
} ;
const results = dataLoader . searchTranscripts ( args . query || '' , searchOptions ) ;
2025-10-26 10:57:39 +00:00
if ( results . length === 0 ) {
2025-10-26 14:46:24 +00:00
const descriptorParts = [ ] ;
if ( args . query ) descriptorParts . push ( ` phrase " ${ args . query } " ` ) ;
if ( Array . isArray ( args . terms ) && args . terms . length > 0 ) descriptorParts . push ( ` terms [ ${ args . terms . join ( ', ' ) } ] ` ) ;
if ( args . hostId || args . hostName ) descriptorParts . push ( 'host filter applied' ) ;
const description = descriptorParts . length > 0 ? descriptorParts . join ( ', ' ) : 'the provided criteria' ;
2025-10-26 10:57:39 +00:00
return {
content : [
{
type : 'text' ,
2025-10-26 14:46:24 +00:00
text : ` No transcripts found matching ${ description } . ` ,
2025-10-26 10:57:39 +00:00
} ,
] ,
} ;
}
2025-10-26 14:46:24 +00:00
const formatArgs = {
... args ,
contextLines : searchOptions . contextLines ,
maxMatchesPerEpisode : searchOptions . maxMatchesPerEpisode ,
} ;
2025-10-26 10:57:39 +00:00
2025-10-26 14:46:24 +00:00
const text = formatTranscriptSearchResults ( results , formatArgs ) ;
2025-10-26 10:57:39 +00:00
return {
content : [
{
type : 'text' ,
2025-10-26 14:46:24 +00:00
text ,
2025-10-26 10:57:39 +00:00
} ,
] ,
} ;
}
if ( name === 'get_host_info' ) {
let host ;
if ( args . hostId ) {
host = dataLoader . getHost ( args . hostId ) ;
} else if ( args . hostName ) {
const hosts = dataLoader . searchHosts ( args . hostName ) ;
host = hosts [ 0 ] ;
}
if ( ! host ) {
return {
content : [
{
type : 'text' ,
text : 'Host not found.' ,
} ,
] ,
} ;
}
let text = ` # ${ host . host }
* * Host ID : * * $ { host . hostid }
* * Email : * * $ { host . email }
* * License : * * $ { host . license }
* * Profile : * * $ { stripHtml ( host . profile ) }
` ;
if ( args . includeEpisodes !== false ) {
const episodes = dataLoader . getEpisodesByHost ( host . hostid ) ;
text += ` \n **Total Episodes:** ${ episodes . length } \n \n ## Episodes \n \n ` ;
// Sort by date (newest first)
episodes . sort ( ( a , b ) => b . date . localeCompare ( a . date ) ) ;
text += episodes . map ( ep =>
` **HPR ${ String ( ep . id ) . padStart ( 4 , '0' ) } ** ( ${ ep . date } ) - ${ ep . title } \n ${ ep . summary } `
) . join ( '\n\n' ) ;
}
return {
content : [
{
type : 'text' ,
text ,
} ,
] ,
} ;
}
if ( name === 'get_series_info' ) {
const series = dataLoader . getSeries ( args . seriesId ) ;
if ( ! series ) {
return {
content : [
{
type : 'text' ,
text : ` Series ${ args . seriesId } not found. ` ,
} ,
] ,
} ;
}
const episodes = dataLoader . getEpisodesInSeries ( args . seriesId ) ;
let text = ` # ${ series . name }
* * Series ID : * * $ { series . id }
* * Description : * * $ { stripHtml ( series . description ) }
* * Total Episodes : * * $ { episodes . length }
# # Episodes in Series
` ;
// Sort by date
episodes . sort ( ( a , b ) => a . date . localeCompare ( b . date ) ) ;
text += episodes . map ( ( ep , index ) => {
const host = dataLoader . getHost ( ep . hostid ) ;
return ` ${ index + 1 } . **HPR ${ String ( ep . id ) . padStart ( 4 , '0' ) } ** ( ${ ep . date } ) - ${ ep . title } by ${ host ? . host || 'Unknown' } \n ${ ep . summary } ` ;
} ) . join ( '\n\n' ) ;
return {
content : [
{
type : 'text' ,
text ,
} ,
] ,
} ;
}
throw new Error ( ` Unknown tool: ${ name } ` ) ;
} catch ( error ) {
return {
content : [
{
type : 'text' ,
text : ` Error: ${ error . message } ` ,
} ,
] ,
isError : true ,
} ;
}
} ) ;
return server ;
}
// Create Express app
const app = express ( ) ;
2025-10-26 14:09:01 +00:00
// Create a single MCP server instance
const mcpServer = createMCPServer ( ) ;
// Trust first proxy hop (Render/Heroku) without allowing arbitrary spoofing
app . set ( 'trust proxy' , 1 ) ;
2025-10-26 11:32:42 +00:00
2025-10-26 10:57:39 +00:00
// Enable CORS
app . use ( cors ( ) ) ;
// Enable compression
app . use ( compression ( ) ) ;
2025-10-26 14:09:01 +00:00
// Apply JSON body parsing globally for the SDK to read POST bodies.
2025-10-26 13:11:41 +00:00
app . use ( express . json ( ) ) ;
2025-10-26 10:57:39 +00:00
// Rate limiting
const limiter = rateLimit ( {
windowMs : RATE _LIMIT _WINDOW _MS ,
max : RATE _LIMIT _MAX _REQUESTS ,
message : 'Too many requests from this IP, please try again later.' ,
standardHeaders : true ,
legacyHeaders : false ,
} ) ;
app . use ( limiter ) ;
// Health check endpoint
app . get ( '/health' , ( req , res ) => {
const usage = process . memoryUsage ( ) ;
const heapUsedMB = usage . heapUsed / 1024 / 1024 ;
res . json ( {
status : 'ok' ,
memory : {
used : ` ${ heapUsedMB . toFixed ( 2 ) } MB ` ,
threshold : ` ${ MEMORY _THRESHOLD _MB } MB ` ,
} ,
activeRequests ,
circuitBreaker : circuitBreaker . state ,
} ) ;
} ) ;
2025-10-26 14:09:01 +00:00
// ⭐ NEW ENDPOINT: Circuit breaker reset
app . post ( '/reset' , ( req , res ) => {
if ( circuitBreaker . state === 'OPEN' ) {
circuitBreaker . reset ( ) ;
console . error ( 'Circuit breaker manually reset.' ) ;
res . json ( { status : 'ok' , message : 'Circuit breaker reset to CLOSED.' } ) ;
} else {
res . json ( { status : 'ok' , message : 'Circuit breaker already CLOSED.' } ) ;
}
} ) ;
2025-10-26 10:57:39 +00:00
// SSE endpoint for MCP
app . get ( '/sse' , async ( req , res ) => {
2025-10-26 13:06:03 +00:00
let pingInterval = null ;
2025-10-26 14:09:01 +00:00
let transport ;
2025-10-26 12:50:18 +00:00
2025-10-26 10:57:39 +00:00
try {
2025-10-26 13:11:41 +00:00
// Check system health
2025-10-26 10:57:39 +00:00
checkMemory ( ) ;
checkConcurrency ( ) ;
activeRequests ++ ;
console . error ( ` New SSE connection. Active requests: ${ activeRequests } ` ) ;
2025-10-26 14:09:01 +00:00
// Create SSE transport, specifying the POST message path
transport = new SSEServerTransport ( '/message' , res ) ;
activeSseTransports . set ( transport . sessionId , transport ) ;
// Connect server with timeout and circuit breaker
// This calls transport.start() internally, which sets up headers and sends the endpoint event.
await circuitBreaker . execute ( ( ) => mcpServer . connect ( transport ) ) ;
2025-10-26 12:50:18 +00:00
2025-10-26 14:09:01 +00:00
// 2. Start the heartbeat/ping interval (after transport.start() has set up res.write)
2025-10-26 12:50:18 +00:00
pingInterval = setInterval ( ( ) => {
2025-10-26 13:11:41 +00:00
// Send a comment line every 20s to keep the proxy alive
2025-10-26 12:50:18 +00:00
res . write ( ':\n' ) ;
} , SSE _HEARTBEAT _INTERVAL _MS ) ;
2025-10-26 14:09:01 +00:00
// Handle connection close (will execute when client closes the connection)
2025-10-26 10:57:39 +00:00
req . on ( 'close' , ( ) => {
activeRequests -- ;
2025-10-26 12:50:18 +00:00
if ( pingInterval ) {
2025-10-26 13:11:41 +00:00
clearInterval ( pingInterval ) ;
2025-10-26 12:50:18 +00:00
}
2025-10-26 14:09:01 +00:00
if ( transport ) {
activeSseTransports . delete ( transport . sessionId ) ;
}
2025-10-26 10:57:39 +00:00
console . error ( ` SSE connection closed. Active requests: ${ activeRequests } ` ) ;
2025-10-26 14:09:01 +00:00
// Ensure the server stream is ended gracefully if it hasn't already
if ( ! res . writableEnded ) {
res . end ( ) ;
}
2025-10-26 10:57:39 +00:00
} ) ;
} catch ( error ) {
2025-10-26 14:09:01 +00:00
// Handle error during connection establishment or connection timeout
2025-10-26 10:57:39 +00:00
activeRequests -- ;
2025-10-26 12:50:18 +00:00
if ( pingInterval ) {
2025-10-26 13:11:41 +00:00
clearInterval ( pingInterval ) ;
2025-10-26 12:50:18 +00:00
}
2025-10-26 14:09:01 +00:00
if ( transport ) {
activeSseTransports . delete ( transport . sessionId ) ;
}
2025-10-26 10:57:39 +00:00
console . error ( 'SSE connection error:' , error . message ) ;
2025-10-26 14:09:01 +00:00
if ( ! res . headersSent ) {
2025-10-26 13:06:03 +00:00
// Case 1: Error before SSE headers were flushed (e.g., checkMemory failed)
2025-10-26 14:09:01 +00:00
// We can still set the status code.
2025-10-26 10:57:39 +00:00
res . status ( 503 ) . json ( {
error : error . message ,
circuitBreaker : circuitBreaker . state ,
} ) ;
2025-10-26 13:06:03 +00:00
} else {
// Case 2: Error after SSE headers were flushed (stream is open)
2025-10-26 13:11:41 +00:00
// Send an SSE 'error' event and end the connection.
2025-10-26 13:06:03 +00:00
const errorData = JSON . stringify ( {
message : error . message ,
circuitBreaker : circuitBreaker . state
} ) ;
res . write ( ` event: error \n data: ${ errorData } \n \n ` ) ;
2025-10-26 13:11:41 +00:00
res . end ( ) ;
2025-10-26 10:57:39 +00:00
}
}
} ) ;
2025-10-26 14:09:01 +00:00
// POST endpoint for MCP messages
app . post ( '/message' , async ( req , res ) => {
2025-10-26 14:24:12 +00:00
const headerConnectionId = req . headers [ 'x-connection-id' ] ;
const queryConnectionId = req . query . sessionId ;
const connectionId = headerConnectionId || queryConnectionId ;
2025-10-26 14:09:01 +00:00
const transport = activeSseTransports . get ( connectionId ) ;
2025-10-26 13:11:41 +00:00
2025-10-26 14:09:01 +00:00
if ( transport ) {
try {
await transport . handlePostMessage ( req , res , req . body ) ;
} catch ( error ) {
console . error ( 'Error processing MCP message via POST:' , error ) ;
res . status ( 400 ) . json ( { error : 'Bad Request' , message : error . message } ) ;
}
} else {
res . status ( 404 ) . json ( { error : 'Not Found' , message : 'No active SSE connection for this ID.' } ) ;
}
} ) ;
2025-10-26 10:57:39 +00:00
// Error handling middleware
app . use ( ( err , req , res , next ) => {
console . error ( 'Express error:' , err ) ;
res . status ( 500 ) . json ( {
error : 'Internal server error' ,
message : err . message ,
} ) ;
} ) ;
// Start server
app . listen ( PORT , ( ) => {
console . error ( ` HPR Knowledge Base MCP Server running on http://localhost: ${ PORT } ` ) ;
console . error ( ` SSE endpoint: http://localhost: ${ PORT } /sse ` ) ;
console . error ( ` Health check: http://localhost: ${ PORT } /health ` ) ;
console . error ( ` Configuration: ` ) ;
console . error ( ` - Max concurrent requests: ${ MAX _CONCURRENT _REQUESTS } ` ) ;
console . error ( ` - Request timeout: ${ REQUEST _TIMEOUT _MS } ms ` ) ;
console . error ( ` - Rate limit: ${ RATE _LIMIT _MAX _REQUESTS } requests per ${ RATE _LIMIT _WINDOW _MS / 1000 } s ` ) ;
console . error ( ` - Memory threshold: ${ MEMORY _THRESHOLD _MB } MB ` ) ;
2025-10-26 12:50:18 +00:00
console . error ( ` - SSE Heartbeat: ${ SSE _HEARTBEAT _INTERVAL _MS / 1000 } s ` ) ;
2025-10-26 10:57:39 +00:00
} ) ;
// Graceful shutdown
process . on ( 'SIGTERM' , ( ) => {
console . error ( 'SIGTERM received, shutting down gracefully...' ) ;
process . exit ( 0 ) ;
} ) ;
process . on ( 'SIGINT' , ( ) => {
console . error ( 'SIGINT received, shutting down gracefully...' ) ;
process . exit ( 0 ) ;
2025-10-26 14:09:01 +00:00
} ) ;