Skip to main content
Glama
capability-aggregator.ts8.6 kB
import type { LoadedServer, ServerCapabilities } from '../types/server.js' import type { ListResourcesResult, ListToolsResult, ToolDefinition, ResourceDefinition, PromptDefinition } from '../types/mcp.js' import type { AuthHeaders } from '../types/auth.js' import { Logger } from '../utils/logger.js' export interface AggregatorOptions { prefixStrategy?: 'serverId' | 'none' // base path for discovery relative to server endpoint capabilitiesEndpoint?: string // default '/capabilities' toolsEndpoint?: string // default '/mcp/tools/list' resourcesEndpoint?: string // default '/mcp/resources/list' } export interface CapabilityMapEntry { serverId: string originalName: string } export class CapabilityAggregator { private readonly options: Required<AggregatorOptions> private toolMap = new Map<string, CapabilityMapEntry>() private resourceMap = new Map<string, CapabilityMapEntry>() constructor(options?: AggregatorOptions) { this.options = { prefixStrategy: options?.prefixStrategy ?? 'serverId', capabilitiesEndpoint: options?.capabilitiesEndpoint ?? '/capabilities', toolsEndpoint: options?.toolsEndpoint ?? '/mcp/tools/list', resourcesEndpoint: options?.resourcesEndpoint ?? '/mcp/resources/list', } } reset(): void { this.toolMap.clear() this.resourceMap.clear() } getMappingForTool(aggregatedName: string): CapabilityMapEntry | undefined { return this.toolMap.get(aggregatedName) } getMappingForResource(aggregatedUri: string): CapabilityMapEntry | undefined { return this.resourceMap.get(aggregatedUri) } async discoverCapabilities( servers: Map<string, LoadedServer>, clientToken?: string, getAuthHeaders?: (serverId: string, clientToken?: string) => Promise<AuthHeaders | undefined> ): Promise<void> { Logger.info('Discovering capabilities', { servers: Array.from(servers.entries()) }) this.reset() const fallbackHeaders: AuthHeaders = {} if (clientToken) fallbackHeaders['Authorization'] = `Bearer ${clientToken}` await Promise.all( Array.from(servers.values()).map(async (server) => { if (!server.endpoint || server.endpoint === 'unknown') { // For STDIO servers, we need a different approach if (server.type === 'stdio' && server.config.url?.startsWith('file://')) { try { Logger.info('Discovering capabilities for STDIO server', { serverId: server.id }) // Import the STDIO capability discovery module const { StdioCapabilityDiscovery } = await import('./stdio-capability-discovery.js') const stdioDiscovery = new StdioCapabilityDiscovery() const filePath = server.config.url.replace('file://', '') const caps = await stdioDiscovery.discoverCapabilities(server.id, filePath) Logger.info('Fetched capabilities for STDIO server', { serverId: server.id, caps }) server.capabilities = caps this.index(server.id, caps) Logger.logServerEvent('capabilities_discovered', server.id, { tools: caps.tools.length, resources: caps.resources.length, prompts: caps.prompts?.length ?? 0, }) } catch (err) { Logger.warn(`Failed capability discovery for STDIO server ${server.id}`, err) } } else { Logger.warn(`Skipping server with unknown endpoint`, { serverId: server.id, type: server.type }) } return } try { Logger.info('Fetching capabilities for server', { serverId: server.id, endpoint: server.endpoint }) const headers = (await getAuthHeaders?.(server.id, clientToken)) ?? fallbackHeaders const caps = await this.fetchCapabilities(server.endpoint, headers) Logger.info('Fetched capabilities for server', { serverId: server.id, caps }) server.capabilities = caps this.index(server.id, caps) Logger.logServerEvent('capabilities_discovered', server.id, { tools: caps.tools.length, resources: caps.resources.length, prompts: caps.prompts?.length ?? 0, }) } catch (err) { Logger.warn(`Failed capability discovery for ${server.id}`, err) } }) ) } getAllTools(servers: Map<string, LoadedServer>): ToolDefinition[] { const result: ToolDefinition[] = [] for (const server of servers.values()) { const tools = server.capabilities?.tools ?? [] for (const t of tools) { const name = this.aggregateName(server.id, t.name) result.push({ ...t, name }) } } return result } getAllResources(servers: Map<string, LoadedServer>): ResourceDefinition[] { const result: ResourceDefinition[] = [] for (const server of servers.values()) { const resources = server.capabilities?.resources ?? [] for (const r of resources) { const uri = this.aggregateName(server.id, r.uri) result.push({ ...r, uri }) } } return result } aggregate(servers: LoadedServer[]): ServerCapabilities { const tools = servers.flatMap((s) => (s.capabilities?.tools ?? []).map((t) => ({ ...t, name: this.aggregateName(s.id, t.name) }))) const resources = servers.flatMap((s) => (s.capabilities?.resources ?? []).map((r) => ({ ...r, uri: this.aggregateName(s.id, r.uri) }))) const prompts = servers.flatMap((s) => s.capabilities?.prompts ?? []) return { tools, resources, prompts: prompts.length ? prompts : undefined } } // --- internals --- private index(serverId: string, caps: ServerCapabilities): void { for (const t of caps.tools) this.toolMap.set(this.aggregateName(serverId, t.name), { serverId, originalName: t.name }) for (const r of caps.resources) this.resourceMap.set(this.aggregateName(serverId, r.uri), { serverId, originalName: r.uri }) } private aggregateName(serverId: string, name: string): string { if (this.options.prefixStrategy === 'none') return name return `${serverId}.${name}` } private ensureTrailingSlash(endpoint: string): string { return endpoint.endsWith('/') ? endpoint : `${endpoint}/` } private async fetchCapabilities(endpoint: string, headers: AuthHeaders): Promise<ServerCapabilities> { const urlCap = new URL(this.options.capabilitiesEndpoint, this.ensureTrailingSlash(endpoint)).toString() Logger.info('Fetching capabilities from endpoint', { urlCap, headers }) try { const res = await fetch(urlCap, { headers }) if (res.ok) { const json = (await res.json()) as any // Try to coerce shapes const tools: ToolDefinition[] = Array.isArray(json.tools) ? json.tools : (json.capabilities?.tools ?? []) const resources: ResourceDefinition[] = Array.isArray(json.resources) ? json.resources : (json.capabilities?.resources ?? []) const prompts: PromptDefinition[] | undefined = Array.isArray(json.prompts) ? json.prompts : (json.capabilities?.prompts ?? undefined) Logger.info('Fetched capabilities', { tools, resources, prompts }) return { tools, resources, prompts } } } catch (err) { Logger.debug('Direct capabilities endpoint failed, trying fallbacks', err) } // Fallback: fetch tools and resources separately const [tools, resources] = await Promise.all([this.fetchTools(endpoint, headers), this.fetchResources(endpoint, headers)]) Logger.info('Fetched capabilities using fallbacks', { tools, resources }) return { tools, resources } } private async fetchTools(endpoint: string, headers: AuthHeaders): Promise<ToolDefinition[]> { const url = new URL(this.options.toolsEndpoint, this.ensureTrailingSlash(endpoint)).toString() try { const res = await fetch(url, { headers }) if (res.ok) { const json = (await res.json()) as ListToolsResult return json.tools ?? [] } } catch (err) { Logger.warn('fetchTools failed', err) } return [] } private async fetchResources(endpoint: string, headers: AuthHeaders): Promise<ResourceDefinition[]> { const url = new URL(this.options.resourcesEndpoint, this.ensureTrailingSlash(endpoint)).toString() try { const res = await fetch(url, { headers }) if (res.ok) { const json = (await res.json()) as ListResourcesResult return json.resources ?? [] } } catch (err) { Logger.warn('fetchResources failed', err) } return [] } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Jakedismo/master-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server