diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index abf18a6..d0bf9b6 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -1335,6 +1335,11 @@ export async function buildApp(params: { env: Env; store: RegistryStore }) { : { mcpServers: {} }; const claudeDesktopConfig = JSON.parse(JSON.stringify(genericConfig)); + const claudeDesktopNote = primaryRemote + ? 'Claude Desktop remote MCP servers may need to be added via Settings > Connectors. Remote servers might not connect when configured only in claude_desktop_config.json.' + : stdioCommand + ? 'Claude Desktop local stdio MCP servers can be configured in claude_desktop_config.json.' + : ''; return { serverName: entry.server.name, @@ -1384,7 +1389,8 @@ export async function buildApp(params: { env: Env; store: RegistryStore }) { genericMcpHostConfig: { object: genericConfig, json: JSON.stringify(genericConfig, null, 2) - } + }, + claudeDesktopNote }; }); @@ -1439,6 +1445,7 @@ export async function buildApp(params: { env: Env; store: RegistryStore }) { totalLatestServers, countRagScoreGte1, countRagScoreGte25, + reachabilityPolicy: params.env.reachabilityPolicy, reachabilityCandidates, reachabilityKnown, reachabilityTrue, diff --git a/apps/api/tests/api.test.ts b/apps/api/tests/api.test.ts index d55e5e6..ca80d86 100644 --- a/apps/api/tests/api.test.ts +++ b/apps/api/tests/api.test.ts @@ -809,6 +809,9 @@ test('rag install returns copy-ready config object', async () => { assert.equal(body.serverName, 'example/installable'); assert.equal(body.version, '1.2.3'); assert.equal(body.transport.hasStdio, true); + assert.equal(body.primaryRemoteType, null); + assert.equal(typeof body.claudeDesktopNote, 'string'); + assert.equal(body.claudeDesktopNote.includes('claude_desktop_config.json'), true); assert.equal(typeof body.genericMcpHostConfig?.json, 'string'); assert.equal(body.genericMcpHostConfig.json.includes('mcpServers'), true); await app.close(); @@ -841,6 +844,8 @@ test('rag install emits SSE transport and endpoint list for SSE-only servers', a assert.equal(body.remoteEndpoints[0].type, 'sse'); assert.equal(body.remoteEndpoints[0].url, 'https://example.com/mcp'); assert.equal(body.genericMcpHostConfig.object?.mcpServers?.['example_sse-installable']?.transport, 'sse'); + assert.equal(typeof body.claudeDesktopNote, 'string'); + assert.equal(body.claudeDesktopNote.includes('Connectors'), true); await app.close(); }); @@ -912,6 +917,7 @@ test('rag stats returns freshness and coverage fields', async () => { assert.equal(typeof body.totalLatestServers, 'number'); assert.equal(typeof body.countRagScoreGte1, 'number'); assert.equal(typeof body.countRagScoreGte25, 'number'); + assert.equal(body.reachabilityPolicy, env.reachabilityPolicy); assert.equal(body.reachabilityCandidates, 2); assert.equal(body.reachabilityKnown, 1); assert.equal(body.reachabilityTrue, 1); diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 2dfb2c3..1e4277e 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -99,6 +99,7 @@ For repository workflows that call `/internal/*` routes: - `sse`: short `GET` with `Accept: text/event-stream`, then immediate body cancel so checks do not hang on streaming responses. - `/rag/install` now emits remote configs for both `streamable-http` and `sse` endpoints. Note: SSE support depends on the MCP host/client; Ragmap only emits the correct transport config. +- `/rag/install` also emits `claudeDesktopNote` so UIs can clarify that Claude Desktop remote MCP servers may need to be added via the Connectors UI. This applies to both scheduled ingest (`/internal/ingest/run`) and scheduled reachability refresh (`/internal/reachability/run`). diff --git a/docs/hosting/api/browse/index.html b/docs/hosting/api/browse/index.html index 4512c17..bfa0650 100644 --- a/docs/hosting/api/browse/index.html +++ b/docs/hosting/api/browse/index.html @@ -53,6 +53,15 @@ .card button:hover { background: var(--bg); } .card button.primary { background: var(--accent); color: white; border-color: var(--accent); } .card button.primary:hover { opacity: 0.92; } + .install-info { margin-top: 10px; border: 1px solid var(--border); border-radius: 10px; padding: 10px; background: #faf8f4; font-size: 12px; color: var(--muted); } + .install-info:empty { display: none; } + .install-info .row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; } + .install-info .row:last-child { margin-bottom: 0; } + .transport-badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 999px; font-size: 11px; border: 1px solid var(--border); color: var(--ink); background: #fff; } + .transport-remote { border-color: #9ec3ff; background: #eef5ff; } + .transport-local { border-color: #b8dcbf; background: #eefaf0; } + .install-info details { margin-top: 6px; } + .install-info summary { cursor: pointer; color: var(--ink); } .empty { color: var(--muted); padding: 24px; text-align: center; } .top-cta { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 12px 16px; margin-bottom: 20px; font-size: 13px; } .top-cta strong { display: block; margin-bottom: 6px; } @@ -166,6 +175,30 @@

RAG servers

document.getElementById('copy-ragmap-npx').onclick = () => copyText(npxRagmap, 'npx command copied'); function esc(s) { if (s == null) return ''; return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } + function transportLabel(installData) { + const t = installData && installData.primaryRemoteType; + if (t === 'sse') return { text: 'Remote (SSE)', cls: 'transport-badge transport-remote' }; + if (t === 'streamable-http') return { text: 'Remote (Streamable HTTP)', cls: 'transport-badge transport-remote' }; + if (installData && installData.transport && installData.transport.hasStdio) { + return { text: 'Local (stdio)', cls: 'transport-badge transport-local' }; + } + return { text: 'Transport unknown', cls: 'transport-badge' }; + } + function renderInstallInfo(index, installData) { + const host = document.getElementById('install-info-' + index); + if (!host) return; + if (!installData) { host.innerHTML = ''; return; } + const label = transportLabel(installData); + const remoteUrl = installData.remote && installData.remote.url ? installData.remote.url : ''; + const note = installData.claudeDesktopNote || ''; + host.innerHTML = + '
' + esc(label.text) + '' + + (remoteUrl ? 'URL: ' + esc(remoteUrl) + '' : '') + + '
' + + (note + ? '
Claude Desktop note
' + esc(note) + '
' + : ''); + } let lastResults = []; function render(results) { lastResults = results; @@ -191,7 +224,8 @@

RAG servers

'' + (desc ? '
' + esc(desc) + '
' : '') + (r.categories && r.categories.length ? '
' + r.categories.map(c => '' + esc(c) + '').join('') + '
' : '') + - '
' + actions + '
'; + '
' + actions + '
' + + '
'; }).join(''); el.querySelectorAll('.copy-name').forEach(b => b.addEventListener('click', () => { const r = lastResults[parseInt(b.dataset.idx, 10)]; if (r) copyText(r.name || '', 'Name copied'); })); el.querySelectorAll('.copy-install').forEach(b => b.addEventListener('click', () => { const r = lastResults[parseInt(b.dataset.idx, 10)]; const line = r && installLine(r.server); if (line) copyText(line, 'Install copied'); })); @@ -201,6 +235,7 @@

RAG servers

fetch('/rag/install?name=' + encodeURIComponent(r.name), { headers: { accept: 'application/json' } }) .then(res => res.ok ? res.json() : Promise.reject(new Error('install config unavailable'))) .then(data => { + renderInstallInfo(parseInt(b.dataset.idx, 10), data); const text = (data && data.genericMcpHostConfig && data.genericMcpHostConfig.json) || ''; if (!text) throw new Error('install config unavailable'); copyText(text, 'Config copied');