@@ -7,8 +7,30 @@ const { spawn } = require('child_process');
77const { PostHog } = require ( 'posthog-node' ) ;
88
99const POSTHOG_KEY = process . env . MCPKIT_POSTHOG_KEY || '' ;
10+ const POSTHOG_HOST = process . env . MCPKIT_POSTHOG_HOST || 'https://us.i.posthog.com' ;
11+ const POSTHOG_PUBLIC_KEY = process . env . MCPKIT_POSTHOG_PUBLIC_KEY || POSTHOG_KEY ; // allow same key by default
1012
11- const ph = POSTHOG_KEY ? new PostHog ( POSTHOG_KEY , { host : process . env . MCPKIT_POSTHOG_HOST || 'https://us.i.posthog.com' } ) : null ;
13+ const ph = POSTHOG_KEY ? new PostHog ( POSTHOG_KEY , { host : POSTHOG_HOST } ) : null ;
14+
15+ // Stable anonymous ID persisted locally for anonymous telemetry
16+ const ANON_ID_FILE = path . join ( os . homedir ( ) , '.mcpkit' , 'anonymous_id' ) ;
17+ function getOrCreateAnonymousId ( ) {
18+ try {
19+ const dir = path . dirname ( ANON_ID_FILE ) ;
20+ if ( ! fs . existsSync ( dir ) ) fs . mkdirSync ( dir , { recursive : true } ) ;
21+ if ( fs . existsSync ( ANON_ID_FILE ) ) {
22+ const val = fs . readFileSync ( ANON_ID_FILE , 'utf8' ) . trim ( ) ;
23+ if ( val ) return val ;
24+ }
25+ const newId = 'anon_' + Math . random ( ) . toString ( 36 ) . slice ( 2 ) + Date . now ( ) . toString ( 36 ) ;
26+ fs . writeFileSync ( ANON_ID_FILE , newId ) ;
27+ return newId ;
28+ } catch {
29+ // Fallback to volatile ID if filesystem fails
30+ return 'anon_' + Math . random ( ) . toString ( 36 ) . slice ( 2 ) ;
31+ }
32+ }
33+ const ANONYMOUS_ID = getOrCreateAnonymousId ( ) ;
1234
1335// Persistence file for manually added agents
1436const PERSISTENCE_FILE = path . join ( os . homedir ( ) , '.mcpkit' , 'agents.json' ) ;
@@ -45,7 +67,15 @@ function savePersistedAgents(agents) {
4567}
4668
4769async function loadMcpRegistry ( ) {
48- // Try to fetch from GitHub first, but silently fall back to local if it fails
70+ // Try official registry first
71+ try {
72+ const official = await fetchFromOfficialRegistry ( ) ;
73+ if ( official && Array . isArray ( official . mcps ) ) {
74+ saveMcpRegistry ( official ) ;
75+ return official ;
76+ }
77+ } catch { }
78+ // Then try GitHub fallback
4979 try {
5080 const githubRegistry = await fetchCatalogFromGitHub ( ) ;
5181 if ( githubRegistry && githubRegistry . mcps ) {
@@ -128,7 +158,7 @@ function updateMcpInstallationStatus(mcpId, agentId, installed = true) {
128158
129159function track ( event , properties = { } ) {
130160 if ( ! ph ) return ;
131- try { ph . capture ( { distinctId : os . userInfo ( ) . username || 'unknown' , event, properties } ) ; } catch { }
161+ try { ph . capture ( { distinctId : ANONYMOUS_ID , event, properties } ) ; } catch { }
132162}
133163
134164function resolveCursorMcpConfig ( ) {
@@ -567,6 +597,90 @@ async function fetchCatalogFromGitHub() {
567597 }
568598}
569599
600+ // Fetch from official MCP registry with pagination and transform to internal format
601+ async function fetchFromOfficialRegistry ( ) {
602+ const BASE = 'https://registry.modelcontextprotocol.io' ;
603+ const perPage = 100 ;
604+ let cursor = null ;
605+ let all = [ ] ;
606+ while ( true ) {
607+ const url = `${ BASE } /v0/servers?limit=${ perPage } ` + ( cursor ? `&cursor=${ encodeURIComponent ( cursor ) } ` : '' ) ;
608+ const res = await fetch ( url ) ;
609+ if ( ! res . ok ) break ;
610+ const data = await res . json ( ) ;
611+ const items = Array . isArray ( data . servers ) ? data . servers : [ ] ;
612+ if ( ! items . length ) break ;
613+ all = all . concat ( items ) ;
614+ const nextCursor = data . metadata && data . metadata . next_cursor ;
615+ if ( ! nextCursor ) break ;
616+ cursor = nextCursor ;
617+ // safety cap to avoid infinite loop
618+ if ( all . length > 5000 ) break ;
619+ }
620+ const mcps = all
621+ . filter ( s => ( s . status || 'active' ) === 'active' )
622+ . map ( transformOfficialServerToInternal )
623+ . filter ( Boolean ) ;
624+ return { mcps } ;
625+ }
626+
627+ function transformOfficialServerToInternal ( server ) {
628+ try {
629+ const officialMeta = server . _meta && server . _meta [ 'io.modelcontextprotocol.registry/official' ] ;
630+ const id = ( officialMeta && officialMeta . id ) || server . id || server . uuid || server . name || '' ;
631+ const name = server . name || server . display_name || server . title || id ;
632+ const description = server . description || '' ;
633+ const version = server . version || 'latest' ;
634+ const documentation = server . documentation || server . homepage || ( server . repository && server . repository . url ) || '' ;
635+ const npmPkg = ( server . packages || [ ] ) . find ( p => p . registry_type === 'npm' ) ;
636+ const identifier = npmPkg ?. identifier || null ;
637+ const command = identifier ? `npx ${ identifier } @${ version || 'latest' } ` : null ;
638+ const category = 'Community' ;
639+ return {
640+ id : String ( id ) ,
641+ name,
642+ category,
643+ description,
644+ version,
645+ npm : identifier ,
646+ command,
647+ env : { } ,
648+ setup_instructions : [ ] ,
649+ uninstall_steps : [ ] ,
650+ documentation,
651+ repository : server . repository && server . repository . url ? server . repository . url : null ,
652+ remotes : Array . isArray ( server . remotes ) ? server . remotes : [ ] ,
653+ packages : Array . isArray ( server . packages ) ? server . packages : [ ] ,
654+ installed : false ,
655+ installation_date : null ,
656+ installed_agents : [ ]
657+ } ;
658+ } catch {
659+ return null ;
660+ }
661+ }
662+
663+ // Detailed transform retains env var metadata for display
664+ function transformOfficialServerDetail ( server ) {
665+ const base = transformOfficialServerToInternal ( server ) || { } ;
666+ // Build env map if npm package defines environment_variables
667+ const env = { } ;
668+ ( server . packages || [ ] ) . forEach ( p => {
669+ if ( p . environment_variables ) {
670+ p . environment_variables . forEach ( ev => {
671+ env [ ev . name ] = {
672+ required : ! ! ev . is_required ,
673+ description : ev . description || '' ,
674+ placeholder : '' ,
675+ help : '' ,
676+ secret : ! ! ev . is_secret
677+ } ;
678+ } ) ;
679+ }
680+ } ) ;
681+ return { ...base , env } ;
682+ }
683+
570684function ensureMcpServersInConfig ( config , agentId ) {
571685 // Different platforms use different config formats
572686 if ( ! config || typeof config !== 'object' ) {
@@ -646,12 +760,57 @@ async function main() {
646760 const app = express ( ) ;
647761 app . use ( express . json ( ) ) ;
648762
763+ // Public telemetry config for frontend initialization
764+ app . get ( '/api/telemetry-config' , ( req , res ) => {
765+ // Only expose public key/host; do not expose server secret
766+ res . json ( {
767+ enabled : ! ! POSTHOG_PUBLIC_KEY ,
768+ publicKey : POSTHOG_PUBLIC_KEY || null ,
769+ host : POSTHOG_HOST ,
770+ } ) ;
771+ } ) ;
772+
649773 app . get ( '/api/agents' , ( req , res ) => {
650774 const agents = detectAgents ( ) ;
651775 track ( 'agents_listed' , { count : agents . length } ) ;
652776 res . json ( { agents } ) ;
653777 } ) ;
654778
779+ // Proxy list from official registry with cursor
780+ app . get ( '/api/official/servers' , async ( req , res ) => {
781+ try {
782+ const limit = Math . min ( Number ( req . query . limit ) || 50 , 100 ) ;
783+ const cursor = req . query . cursor ? String ( req . query . cursor ) : null ;
784+ const BASE = 'https://registry.modelcontextprotocol.io' ;
785+ const url = `${ BASE } /v0/servers?limit=${ limit } ` + ( cursor ? `&cursor=${ encodeURIComponent ( cursor ) } ` : '' ) ;
786+ const r = await fetch ( url ) ;
787+ if ( ! r . ok ) return res . status ( r . status ) . json ( { error : 'registry_error' } ) ;
788+ const data = await r . json ( ) ;
789+ const servers = ( data . servers || [ ] ) . filter ( s => ( s . status || 'active' ) === 'active' ) ;
790+ res . json ( {
791+ servers : servers . map ( s => transformOfficialServerToInternal ( s ) ) ,
792+ next_cursor : data . metadata && data . metadata . next_cursor
793+ } ) ;
794+ } catch ( e ) {
795+ res . status ( 500 ) . json ( { error : e . message } ) ;
796+ }
797+ } ) ;
798+
799+ // Proxy detail fetch for a server by ID
800+ app . get ( '/api/official/servers/:id' , async ( req , res ) => {
801+ try {
802+ const id = req . params . id ;
803+ const BASE = 'https://registry.modelcontextprotocol.io' ;
804+ const url = `${ BASE } /v0/servers/${ encodeURIComponent ( id ) } ` ;
805+ const r = await fetch ( url ) ;
806+ if ( ! r . ok ) return res . status ( r . status ) . json ( { error : 'registry_error' } ) ;
807+ const data = await r . json ( ) ;
808+ res . json ( { server : transformOfficialServerDetail ( data ) } ) ;
809+ } catch ( e ) {
810+ res . status ( 500 ) . json ( { error : e . message } ) ;
811+ }
812+ } ) ;
813+
655814 // News API endpoint for tech headlines - using free proxy
656815 app . get ( '/api/news' , async ( req , res ) => {
657816 try {
@@ -870,14 +1029,14 @@ async function main() {
8701029 // Manual registry refresh endpoint
8711030 app . post ( '/api/refresh-registry' , async ( req , res ) => {
8721031 try {
873- const githubRegistry = await fetchCatalogFromGitHub ( ) ;
874- if ( githubRegistry && githubRegistry . mcps ) {
875- saveMcpRegistry ( githubRegistry ) ;
876- track ( 'registry_refreshed' , { source : 'manual ' , count : githubRegistry . mcps . length } ) ;
1032+ const official = await fetchFromOfficialRegistry ( ) ;
1033+ if ( official && official . mcps ) {
1034+ saveMcpRegistry ( official ) ;
1035+ track ( 'registry_refreshed' , { source : 'official ' , count : official . mcps . length } ) ;
8771036 res . json ( {
8781037 success : true ,
8791038 message : 'Registry updated successfully' ,
880- count : githubRegistry . mcps . length
1039+ count : official . mcps . length
8811040 } ) ;
8821041 } else {
8831042 // If GitHub fetch fails, return success with local registry info
@@ -1129,11 +1288,11 @@ async function main() {
11291288 // Set up periodic registry updates (every 30 minutes)
11301289 setInterval ( async ( ) => {
11311290 try {
1132- const githubRegistry = await fetchCatalogFromGitHub ( ) ;
1133- if ( githubRegistry && githubRegistry . mcps ) {
1134- saveMcpRegistry ( githubRegistry ) ;
1135- console . log ( `Registry updated automatically - ${ githubRegistry . mcps . length } MCPs available` ) ;
1136- track ( 'registry_refreshed' , { source : 'automatic ' , count : githubRegistry . mcps . length } ) ;
1291+ const official = await fetchFromOfficialRegistry ( ) ;
1292+ if ( official && official . mcps ) {
1293+ saveMcpRegistry ( official ) ;
1294+ console . log ( `Registry updated automatically - ${ official . mcps . length } MCPs available` ) ;
1295+ track ( 'registry_refreshed' , { source : 'automatic_official ' , count : official . mcps . length } ) ;
11371296 }
11381297 } catch ( e ) {
11391298 // Silently fail - don't log errors for automatic updates to avoid noise
0 commit comments