Skip to content

Commit 1108c6f

Browse files
Natalia VendittoNatalia Venditto
authored andcommitted
feat: external MCP config support and remove all any types
- Add mergeConfigServers() to merge DA config-declared MCP servers with repo-discovered servers (config wins on ID conflicts) - Parse configServers query param in /mcp-discovery and /mcp-tools - Replace all `any` types in discovery.ts and loader.ts with proper typed alternatives to fix CI lint errors Made-with: Cursor
1 parent f3c343c commit 1108c6f

3 files changed

Lines changed: 112 additions & 22 deletions

File tree

src/mcp/discovery.ts

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,12 @@ async function listGitHubDir(
5858
try {
5959
const resp = await ghFetch(url, token);
6060
if (!resp.ok) return [];
61-
const data = await resp.json() as any;
61+
const data: unknown = await resp.json();
6262
if (!Array.isArray(data)) return [];
63-
return data.map((item: any) => ({
64-
name: item.name,
65-
path: item.path,
66-
type: item.type === 'dir' ? 'dir' : 'file',
63+
return data.map((item: Record<string, unknown>) => ({
64+
name: String(item.name ?? ''),
65+
path: String(item.path ?? ''),
66+
type: (item.type === 'dir' ? 'dir' : 'file') as 'file' | 'dir',
6767
}));
6868
} catch {
6969
return [];
@@ -81,8 +81,8 @@ async function readGitHubFile(
8181
try {
8282
const resp = await ghFetch(url, token);
8383
if (!resp.ok) return null;
84-
const data = await resp.json() as any;
85-
if (data.encoding === 'base64' && data.content) {
84+
const data = await resp.json() as Record<string, unknown>;
85+
if (data.encoding === 'base64' && typeof data.content === 'string') {
8686
return atob(data.content.replace(/\n/g, ''));
8787
}
8888
if (typeof data.content === 'string') return data.content;
@@ -139,14 +139,14 @@ function validateConfig(raw: unknown): { config: MCPServerConfig | null; error:
139139
}
140140

141141
function inferConfigFromPackageJson(pkgRaw: string): { config: MCPServerConfig | null; error: string | null } {
142-
let parsed: any;
142+
let parsed: Record<string, unknown>;
143143
try {
144-
parsed = JSON.parse(pkgRaw);
144+
parsed = JSON.parse(pkgRaw) as Record<string, unknown>;
145145
} catch {
146146
return { config: null, error: 'package.json is not valid JSON' };
147147
}
148148

149-
const scripts = parsed?.scripts;
149+
const scripts = parsed.scripts as Record<string, unknown> | undefined;
150150
if (!scripts || typeof scripts !== 'object') {
151151
return { config: null, error: 'package.json has no scripts' };
152152
}
@@ -201,7 +201,7 @@ async function getPackageDescription(
201201
const pkgRaw = await readGitHubFile(owner, repo, `${serverPath}/package.json`, branch, ghToken);
202202
if (!pkgRaw) return undefined;
203203
try {
204-
const pkg = JSON.parse(pkgRaw);
204+
const pkg = JSON.parse(pkgRaw) as Record<string, unknown>;
205205
return typeof pkg.description === 'string' ? pkg.description : undefined;
206206
} catch {
207207
return undefined;
@@ -222,8 +222,9 @@ async function probeRemoteEndpoint(url: string): Promise<{ reachable: boolean; d
222222
return { reachable: true, detail: `Responded ${resp.status}` };
223223
}
224224
return { reachable: false, detail: `HTTP ${resp.status} ${resp.statusText}` };
225-
} catch (e: any) {
226-
const msg = e?.name === 'AbortError' ? 'Timeout (5s)' : (e?.message || 'Connection failed');
225+
} catch (e: unknown) {
226+
const err = e instanceof Error ? e : null;
227+
const msg = err?.name === 'AbortError' ? 'Timeout (5s)' : (err?.message || 'Connection failed');
227228
return { reachable: false, detail: msg };
228229
}
229230
}
@@ -418,6 +419,75 @@ export function loadEffectiveMCPConfig(
418419
};
419420
}
420421

422+
// ---------------------------------------------------------------------------
423+
// Merge config-declared (external) MCP servers
424+
// ---------------------------------------------------------------------------
425+
426+
/**
427+
* Merge externally declared MCP servers (from DA config `mcp-servers` key)
428+
* into a discovery result. Config servers override repo-discovered servers
429+
* on ID conflict. Remote servers are probed for reachability.
430+
*/
431+
export async function mergeConfigServers(
432+
discovery: DiscoveredMCP,
433+
configServers: Record<string, unknown>,
434+
): Promise<DiscoveredMCP> {
435+
const result: DiscoveredMCP = {
436+
...discovery,
437+
mcpServers: { ...discovery.mcpServers },
438+
warnings: [...discovery.warnings],
439+
servers: [...discovery.servers],
440+
};
441+
442+
for (const [id, raw] of Object.entries(configServers)) {
443+
const idError = validateServerId(id);
444+
if (idError) {
445+
result.warnings.push({ serverId: id, message: idError });
446+
result.servers.push({ id, sourcePath: 'da-config', status: 'error', statusDetail: idError });
447+
continue;
448+
}
449+
450+
const { config, error } = validateConfig(raw);
451+
if (!config || error) {
452+
const msg = error ?? 'Invalid MCP server config';
453+
result.warnings.push({ serverId: id, message: msg });
454+
result.servers.push({ id, sourcePath: 'da-config', status: 'error', statusDetail: msg });
455+
continue;
456+
}
457+
458+
// Remove any repo-discovered entry with the same ID (config wins)
459+
result.servers = result.servers.filter((s) => s.id !== id);
460+
461+
if (isRemoteConfig(config)) {
462+
const probe = await probeRemoteEndpoint(config.url);
463+
result.mcpServers[id] = config;
464+
result.servers.push({
465+
id,
466+
sourcePath: 'da-config',
467+
status: probe.reachable ? 'reachable' : 'unreachable',
468+
transport: config.type,
469+
endpoint: config.url,
470+
statusDetail: probe.detail,
471+
});
472+
} else if (isStdioConfig(config)) {
473+
result.mcpServers[id] = config;
474+
result.servers.push({
475+
id,
476+
sourcePath: 'da-config',
477+
status: 'ok',
478+
transport: 'stdio',
479+
endpoint: `${config.command} ${(config.args ?? []).join(' ')}`.trim(),
480+
statusDetail: 'Configured via DA config (stdio — requires local runtime)',
481+
});
482+
} else {
483+
result.mcpServers[id] = config;
484+
result.servers.push({ id, sourcePath: 'da-config', status: 'ok' });
485+
}
486+
}
487+
488+
return result;
489+
}
490+
421491
// ---------------------------------------------------------------------------
422492
// Cache I/O via DAAdminClient
423493
// ---------------------------------------------------------------------------
@@ -431,7 +501,7 @@ export async function readDiscoveryCache(
431501
): Promise<DiscoveredMCP | null> {
432502
try {
433503
const source = await client.getSource(org, repo, CACHE_PATH);
434-
const raw = typeof source === 'string' ? source : (source as any).content;
504+
const raw = typeof source === 'string' ? source : (source as unknown as { content: string }).content;
435505
return JSON.parse(raw) as DiscoveredMCP;
436506
} catch {
437507
return null;

src/server.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { EDSAdminClient } from './eds-admin/client.js';
1010
import { createDATools, createEDSTools } from './tools/tools.js';
1111
import { ensureHtmlExtension } from './tools/utils.js';
1212
import { createCollabClient } from './collab-client.js';
13-
import { scanRepoMCPServers, readDiscoveryCache, loadEffectiveMCPConfig } from './mcp/discovery.js';
13+
import { scanRepoMCPServers, readDiscoveryCache, loadEffectiveMCPConfig, mergeConfigServers } from './mcp/discovery.js';
1414
import type { MCPServerConfig } from './mcp/types.js';
1515
import { loadSkillsIndex, loadSkillContent } from './skills/loader.js';
1616
import type { SkillsIndex } from './skills/loader.js';
@@ -228,6 +228,15 @@ function expandUserSelectionContextForModel(messages: any[]): any[] {
228228
});
229229
}
230230

231+
async function parseConfigServers(raw: string | null): Promise<Record<string, unknown>> {
232+
if (!raw) return {};
233+
try {
234+
const parsed = JSON.parse(decodeURIComponent(raw));
235+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) return parsed;
236+
} catch { /* invalid JSON — ignore */ }
237+
return {};
238+
}
239+
231240
async function handleMcpDiscovery(
232241
request: Request,
233242
env: Env,
@@ -236,12 +245,17 @@ async function handleMcpDiscovery(
236245
): Promise<Response> {
237246
const url = new URL(request.url);
238247
const mcpPath = url.searchParams.get('mcpPath') || undefined;
239-
const result = await scanRepoMCPServers(org, site, {
248+
let result = await scanRepoMCPServers(org, site, {
240249
branch: 'main',
241250
githubToken: env.GITHUB_TOKEN,
242251
mcpPath,
243252
});
244253

254+
const configServers = await parseConfigServers(url.searchParams.get('configServers'));
255+
if (Object.keys(configServers).length > 0) {
256+
result = await mergeConfigServers(result, configServers);
257+
}
258+
245259
return new Response(JSON.stringify(result), {
246260
status: 200,
247261
headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' },
@@ -261,12 +275,17 @@ async function handleMcpToolsList(
261275
const url = new URL(request.url);
262276
const mcpPath = url.searchParams.get('mcpPath') || undefined;
263277

264-
const discovery = await scanRepoMCPServers(org, site, {
278+
let discovery = await scanRepoMCPServers(org, site, {
265279
branch: 'main',
266280
githubToken: env.GITHUB_TOKEN,
267281
mcpPath,
268282
});
269283

284+
const configServers = await parseConfigServers(url.searchParams.get('configServers'));
285+
if (Object.keys(configServers).length > 0) {
286+
discovery = await mergeConfigServers(discovery, configServers);
287+
}
288+
270289
const serverTools: Array<{
271290
id: string;
272291
description?: string;

src/skills/loader.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export async function loadSkillsIndex(
8888
const subPath = source === 'org' ? `skills/${item.name}${item.ext && !item.name.endsWith('.md') ? `.${item.ext}` : ''}` : pathSegment;
8989

9090
const content = await client.getSource(org, target, subPath);
91-
const body = typeof content === 'string' ? content : (content as any)?.content ?? '';
91+
const body = typeof content === 'string' ? content : (content as unknown as { content?: string })?.content ?? '';
9292
return { id, title: extractTitle(body) };
9393
} catch {
9494
return { id, title: id };
@@ -114,7 +114,7 @@ export async function loadSkillContent(
114114
// Site-level
115115
try {
116116
const content = await client.getSource(org, site, `${SKILLS_PATH}/${filename}`);
117-
const body = typeof content === 'string' ? content : (content as any)?.content ?? '';
117+
const body = typeof content === 'string' ? content : (content as unknown as { content?: string })?.content ?? '';
118118
if (body) return body;
119119
} catch {
120120
// fall through to org-level
@@ -123,7 +123,7 @@ export async function loadSkillContent(
123123
// Org-level
124124
try {
125125
const content = await client.getSource(org, '.da', `skills/${filename}`);
126-
const body = typeof content === 'string' ? content : (content as any)?.content ?? '';
126+
const body = typeof content === 'string' ? content : (content as unknown as { content?: string })?.content ?? '';
127127
if (body) return body;
128128
} catch {
129129
// not found
@@ -146,7 +146,8 @@ export async function saveSkillContent(
146146
try {
147147
await client.createSource(org, site, `${SKILLS_PATH}/${filename}`, content, 'text/markdown');
148148
return { success: true };
149-
} catch (e: any) {
150-
return { success: false, error: e?.message ?? String(e) };
149+
} catch (e: unknown) {
150+
const msg = e instanceof Error ? e.message : String(e);
151+
return { success: false, error: msg };
151152
}
152153
}

0 commit comments

Comments
 (0)