feat: Add support for MCP Resources (#13178)

Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
This commit is contained in:
Alex Gavrilescu
2025-12-09 03:43:12 +01:00
committed by GitHub
parent 720b31cb8b
commit 560550f5df
20 changed files with 1146 additions and 80 deletions
+75 -2
View File
@@ -9,6 +9,7 @@ import type { Config, FileSearch } from '@google/gemini-cli-core';
import { FileSearchFactory, escapePath } from '@google/gemini-cli-core';
import type { Suggestion } from '../components/SuggestionsDisplay.js';
import { MAX_SUGGESTIONS_TO_SHOW } from '../components/SuggestionsDisplay.js';
import { AsyncFzf } from 'fzf';
export enum AtCompletionStatus {
IDLE = 'idle',
@@ -97,6 +98,61 @@ export interface UseAtCompletionProps {
setIsLoadingSuggestions: (isLoading: boolean) => void;
}
interface ResourceSuggestionCandidate {
searchKey: string;
suggestion: Suggestion;
}
function buildResourceCandidates(
config?: Config,
): ResourceSuggestionCandidate[] {
const registry = config?.getResourceRegistry?.();
if (!registry) {
return [];
}
const resources = registry.getAllResources().map((resource) => {
// Use serverName:uri format to disambiguate resources from different MCP servers
const prefixedUri = `${resource.serverName}:${resource.uri}`;
return {
// Include prefixedUri in searchKey so users can search by the displayed format
searchKey: `${prefixedUri} ${resource.name ?? ''}`.toLowerCase(),
suggestion: {
label: prefixedUri,
value: prefixedUri,
},
} satisfies ResourceSuggestionCandidate;
});
return resources;
}
async function searchResourceCandidates(
pattern: string,
candidates: ResourceSuggestionCandidate[],
): Promise<Suggestion[]> {
if (candidates.length === 0) {
return [];
}
const normalizedPattern = pattern.toLowerCase();
if (!normalizedPattern) {
return candidates
.slice(0, MAX_SUGGESTIONS_TO_SHOW)
.map((candidate) => candidate.suggestion);
}
const fzf = new AsyncFzf(candidates, {
selector: (candidate: ResourceSuggestionCandidate) => candidate.searchKey,
});
const results = await fzf.find(normalizedPattern, {
limit: MAX_SUGGESTIONS_TO_SHOW * 3,
});
return results.map(
(result: { item: ResourceSuggestionCandidate }) => result.item.suggestion,
);
}
export function useAtCompletion(props: UseAtCompletionProps): void {
const {
enabled,
@@ -210,11 +266,28 @@ export function useAtCompletion(props: UseAtCompletionProps): void {
return;
}
const suggestions = results.map((p) => ({
const fileSuggestions = results.map((p) => ({
label: p,
value: escapePath(p),
}));
dispatch({ type: 'SEARCH_SUCCESS', payload: suggestions });
const resourceCandidates = buildResourceCandidates(config);
const resourceSuggestions = (
await searchResourceCandidates(
state.pattern ?? '',
resourceCandidates,
)
).map((suggestion) => ({
...suggestion,
label: suggestion.label.replace(/^@/, ''),
value: suggestion.value.replace(/^@/, ''),
}));
const combinedSuggestions = [
...fileSuggestions,
...resourceSuggestions,
];
dispatch({ type: 'SEARCH_SUCCESS', payload: combinedSuggestions });
} catch (error) {
if (!(error instanceof Error && error.name === 'AbortError')) {
dispatch({ type: 'ERROR' });