Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 142 additions & 42 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,12 @@ const App = () => {
selectedTaskRef.current = selectedTask;
}, [selectedTask]);

const listToolsRef = useRef<any>(null);
const listResourcesRef = useRef<any>(null);
const listResourceTemplatesRef = useRef<any>(null);
const listPromptsRef = useRef<any>(null);
const listTasksRef = useRef<any>(null);

const {
connectionStatus,
serverCapabilities,
Expand Down Expand Up @@ -402,7 +408,20 @@ const App = () => {
setNotifications((prev) => [...prev, notification as ServerNotification]);

if (notification.method === "notifications/tasks/list_changed") {
void listTasks();
void listTasksRef.current?.();
}

if (notification.method === "notifications/tools/list_changed") {
void listToolsRef.current?.(false);
}

if (notification.method === "notifications/resources/list_changed") {
void listResourcesRef.current?.(false);
void listResourceTemplatesRef.current?.(false);
}

if (notification.method === "notifications/prompts/list_changed") {
void listPromptsRef.current?.(false);
}

if (notification.method === "notifications/tasks/status") {
Expand Down Expand Up @@ -858,33 +877,52 @@ const App = () => {
}
};

const listResources = async () => {
const listResources = async (loadMore: boolean = false) => {
const cursor = loadMore ? nextResourceCursor : undefined;
const response = await sendMCPRequest(
{
method: "resources/list" as const,
params: nextResourceCursor ? { cursor: nextResourceCursor } : {},
params: cursor ? { cursor } : {},
},
ListResourcesResultSchema,
"resources",
);
setResources(resources.concat(response.resources ?? []));
if (loadMore) {
setResources((prev) => prev.concat(response.resources ?? []));
} else {
const newResources = response.resources ?? [];
setResources(newResources);

// Preserve selection if it still exists
if (selectedResource) {
const stillExists = newResources.some(
(r) => r.uri === selectedResource.uri,
);
if (!stillExists) {
setSelectedResource(null);
}
}
}
setNextResourceCursor(response.nextCursor);
};

const listResourceTemplates = async () => {
const listResourceTemplates = async (loadMore: boolean = false) => {
const cursor = loadMore ? nextResourceTemplateCursor : undefined;
const response = await sendMCPRequest(
{
method: "resources/templates/list" as const,
params: nextResourceTemplateCursor
? { cursor: nextResourceTemplateCursor }
: {},
params: cursor ? { cursor } : {},
},
ListResourceTemplatesResultSchema,
"resources",
);
setResourceTemplates(
resourceTemplates.concat(response.resourceTemplates ?? []),
);
if (loadMore) {
setResourceTemplates((prev) =>
prev.concat(response.resourceTemplates ?? []),
);
} else {
setResourceTemplates(response.resourceTemplates ?? []);
}
setNextResourceTemplateCursor(response.nextCursor);
};

Expand Down Expand Up @@ -979,31 +1017,67 @@ const App = () => {
}
};

const listPrompts = async () => {
const listPrompts = async (loadMore: boolean = false) => {
const cursor = loadMore ? nextPromptCursor : undefined;
const response = await sendMCPRequest(
{
method: "prompts/list" as const,
params: nextPromptCursor ? { cursor: nextPromptCursor } : {},
params: cursor ? { cursor } : {},
},
ListPromptsResultSchema,
"prompts",
);
setPrompts(response.prompts);
if (loadMore) {
setPrompts((prev) => prev.concat(response.prompts ?? []));
} else {
const newPrompts = response.prompts ?? [];
setPrompts(newPrompts);

// Preserve selection if it still exists
if (selectedPrompt) {
const stillExists = newPrompts.some(
(p) => p.name === selectedPrompt.name,
);
if (!stillExists) {
setSelectedPrompt(null);
setPromptContent("");
}
}
}
setNextPromptCursor(response.nextCursor);
};

const listTools = async () => {
const listTools = async (loadMore: boolean = false) => {
const cursor = loadMore ? nextToolCursor : undefined;
const response = await sendMCPRequest(
{
method: "tools/list" as const,
params: nextToolCursor ? { cursor: nextToolCursor } : {},
params: cursor ? { cursor } : {},
},
ListToolsResultSchema,
"tools",
);
setTools(response.tools);
if (loadMore) {
setTools((prev) => {
const nextTools = prev.concat(response.tools ?? []);
cacheToolOutputSchemas(nextTools);
return nextTools;
});
} else {
const newTools = response.tools ?? [];
setTools(newTools);
cacheToolOutputSchemas(newTools);

// Preserve selection if it still exists
if (selectedTool) {
const stillExists = newTools.some((t) => t.name === selectedTool.name);
if (!stillExists) {
setSelectedTool(null);
setToolResult(null);
}
}
}
setNextToolCursor(response.nextCursor);
cacheToolOutputSchemas(response.tools);
};

const callTool = async (
Expand Down Expand Up @@ -1216,20 +1290,46 @@ const App = () => {
}
};

const listTasks = useCallback(async () => {
try {
const response = await listMcpTasks(nextTaskCursor);
setTasks(response.tasks);
setNextTaskCursor(response.nextCursor);
// Inline error clear to avoid extra dependency on clearError
setErrors((prev) => ({ ...prev, tasks: null }));
} catch (e) {
setErrors((prev) => ({
...prev,
tasks: (e as Error).message ?? String(e),
}));
}
}, [listMcpTasks, nextTaskCursor]);
useEffect(() => {
listToolsRef.current = listTools;
listResourcesRef.current = listResources;
listResourceTemplatesRef.current = listResourceTemplates;
listPromptsRef.current = listPrompts;
listTasksRef.current = listTasks;
});

const listTasks = useCallback(
async (loadMore: boolean = false) => {
try {
const cursor = loadMore ? nextTaskCursor : undefined;
const response = await listMcpTasks(cursor);
if (loadMore) {
setTasks((prev) => prev.concat(response.tasks));
} else {
setTasks(response.tasks);
// Selection is handled via ref in TasksTab, but we should clear selectedTask
// if it's no longer in the list after a full refresh
if (selectedTaskRef.current) {
const stillExists = response.tasks.some(
(t) => t.taskId === selectedTaskRef.current?.taskId,
);
if (!stillExists) {
setSelectedTask(null);
}
}
}
setNextTaskCursor(response.nextCursor);
// Inline error clear to avoid extra dependency on clearError
setErrors((prev) => ({ ...prev, tasks: null }));
} catch (e) {
setErrors((prev) => ({
...prev,
tasks: (e as Error).message ?? String(e),
}));
}
},
[listMcpTasks, nextTaskCursor],
);

const cancelTask = async (taskId: string) => {
try {
Expand Down Expand Up @@ -1466,17 +1566,17 @@ const App = () => {
<ResourcesTab
resources={resources}
resourceTemplates={resourceTemplates}
listResources={() => {
listResources={(loadMore) => {
clearError("resources");
listResources();
listResources(loadMore);
}}
clearResources={() => {
setResources([]);
setNextResourceCursor(undefined);
}}
listResourceTemplates={() => {
listResourceTemplates={(loadMore) => {
clearError("resources");
listResourceTemplates();
listResourceTemplates(loadMore);
}}
clearResourceTemplates={() => {
setResourceTemplates([]);
Expand Down Expand Up @@ -1512,9 +1612,9 @@ const App = () => {
/>
<PromptsTab
prompts={prompts}
listPrompts={() => {
listPrompts={(loadMore) => {
clearError("prompts");
listPrompts();
listPrompts(loadMore);
}}
clearPrompts={() => {
setPrompts([]);
Expand All @@ -1541,9 +1641,9 @@ const App = () => {
!!serverCapabilities?.tasks?.requests?.tools?.call
}
tools={tools}
listTools={() => {
listTools={(loadMore) => {
clearError("tools");
listTools();
listTools(loadMore);
}}
clearTools={() => {
setTools([]);
Expand Down Expand Up @@ -1617,9 +1717,9 @@ const App = () => {
<AppsTab
sandboxPath={`${getMCPProxyAddress(config)}/sandbox`}
tools={tools}
listTools={() => {
listTools={(loadMore) => {
clearError("tools");
listTools();
listTools(loadMore);
}}
callTool={async (
name: string,
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/AppsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import {
interface AppsTabProps {
sandboxPath: string;
tools: Tool[];
listTools: () => void;
listTools: (loadMore?: boolean) => void;
callTool: (
name: string,
params: Record<string, unknown>,
Expand Down
14 changes: 10 additions & 4 deletions client/src/components/PromptsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const PromptsTab = ({
error,
}: {
prompts: Prompt[];
listPrompts: () => void;
listPrompts: (loadMore?: boolean) => void;
clearPrompts: () => void;
getPrompt: (name: string, args: Record<string, string>) => void;
selectedPrompt: Prompt | null;
Expand Down Expand Up @@ -105,7 +105,7 @@ const PromptsTab = ({
<div className="grid grid-cols-2 gap-4">
<ListPane
items={prompts}
listItems={listPrompts}
listItems={() => listPrompts(!!nextCursor)}
clearItems={() => {
clearPrompts();
setSelectedPrompt(null);
Expand All @@ -129,8 +129,14 @@ const PromptsTab = ({
</div>
)}
title="Prompts"
buttonText={nextCursor ? "List More Prompts" : "List Prompts"}
isButtonDisabled={!nextCursor && prompts.length > 0}
buttonText={
nextCursor
? "List More Prompts"
: prompts.length > 0
? "Refresh Prompts"
: "List Prompts"
}
isButtonDisabled={false}
/>

<div className="bg-card border border-border rounded-lg shadow">
Expand Down
26 changes: 18 additions & 8 deletions client/src/components/ResourcesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ const ResourcesTab = ({
}: {
resources: Resource[];
resourceTemplates: ResourceTemplate[];
listResources: () => void;
listResources: (loadMore?: boolean) => void;
clearResources: () => void;
listResourceTemplates: () => void;
listResourceTemplates: (loadMore?: boolean) => void;
clearResourceTemplates: () => void;
readResource: (uri: string) => void;
selectedResource: Resource | null;
Expand Down Expand Up @@ -115,7 +115,7 @@ const ResourcesTab = ({
<div className="grid grid-cols-3 gap-4">
<ListPane
items={resources}
listItems={listResources}
listItems={() => listResources(!!nextCursor)}
clearItems={() => {
clearResources();
// Condition to check if selected resource is not resource template's resource
Expand All @@ -141,13 +141,19 @@ const ResourcesTab = ({
</div>
)}
title="Resources"
buttonText={nextCursor ? "List More Resources" : "List Resources"}
isButtonDisabled={!nextCursor && resources.length > 0}
buttonText={
nextCursor
? "List More Resources"
: resources.length > 0
? "Refresh Resources"
: "List Resources"
}
isButtonDisabled={false}
/>

<ListPane
items={resourceTemplates}
listItems={listResourceTemplates}
listItems={() => listResourceTemplates(!!nextTemplateCursor)}
clearItems={() => {
clearResourceTemplates();
// Condition to check if selected resource is resource template's resource
Expand Down Expand Up @@ -175,9 +181,13 @@ const ResourcesTab = ({
)}
title="Resource Templates"
buttonText={
nextTemplateCursor ? "List More Templates" : "List Templates"
nextTemplateCursor
? "List More Templates"
: resourceTemplates.length > 0
? "Refresh Templates"
: "List Templates"
}
isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0}
isButtonDisabled={false}
/>

<div className="bg-card border border-border rounded-lg shadow">
Expand Down
Loading