Skip to content

Commit a8c34af

Browse files
committed
added mcp client
1 parent 6db22df commit a8c34af

6 files changed

Lines changed: 14215 additions & 577 deletions

File tree

bin/mcp-kit.js

Lines changed: 172 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,30 @@ const { spawn } = require('child_process');
77
const { PostHog } = require('posthog-node');
88

99
const 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
1436
const PERSISTENCE_FILE = path.join(os.homedir(), '.mcpkit', 'agents.json');
@@ -45,7 +67,15 @@ function savePersistedAgents(agents) {
4567
}
4668

4769
async 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

129159
function 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

134164
function 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+
570684
function 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

Comments
 (0)