mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-16 09:26:07 +00:00
feat(mcp): support mcp resources
This commit is contained in:
@@ -45,6 +45,8 @@ import MCPReadme from '@/app/home/mcp/components/mcp-form/MCPReadme';
|
||||
import {
|
||||
MCPServerRuntimeInfo,
|
||||
MCPTool,
|
||||
MCPResource,
|
||||
MCPResourceContent,
|
||||
MCPServer,
|
||||
MCPSessionStatus,
|
||||
MCPServerExtraArgsRemote,
|
||||
@@ -244,13 +246,105 @@ function ToolsList({ tools, t }: { tools: MCPTool[]; t: TFunction }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Resources list component
|
||||
function ResourcesList({
|
||||
resources,
|
||||
serverName,
|
||||
t,
|
||||
}: {
|
||||
resources: MCPResource[];
|
||||
serverName: string;
|
||||
t: (key: string) => string;
|
||||
}) {
|
||||
const [expandedUri, setExpandedUri] = React.useState<string | null>(null);
|
||||
const [resourceContent, setResourceContent] =
|
||||
React.useState<MCPResourceContent | null>(null);
|
||||
const [loadingContent, setLoadingContent] = React.useState(false);
|
||||
|
||||
const handleToggleResource = async (uri: string) => {
|
||||
if (expandedUri === uri) {
|
||||
setExpandedUri(null);
|
||||
setResourceContent(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setExpandedUri(uri);
|
||||
setResourceContent(null);
|
||||
setLoadingContent(true);
|
||||
|
||||
try {
|
||||
const resp = await httpClient.readMCPServerResource(serverName, uri);
|
||||
if (resp.contents && resp.contents.length > 0) {
|
||||
setResourceContent(resp.contents[0]);
|
||||
}
|
||||
} catch {
|
||||
setResourceContent(null);
|
||||
} finally {
|
||||
setLoadingContent(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2 max-h-[400px] overflow-y-auto">
|
||||
{resources.map((resource, index) => (
|
||||
<Card key={index} className="py-3 shadow-none">
|
||||
<CardHeader
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleToggleResource(resource.uri)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm">{resource.name}</CardTitle>
|
||||
{resource.mime_type && (
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{resource.mime_type}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{resource.description && (
|
||||
<CardDescription className="text-xs">
|
||||
{resource.description}
|
||||
</CardDescription>
|
||||
)}
|
||||
<div className="text-xs text-muted-foreground font-mono break-all">
|
||||
{resource.uri}
|
||||
</div>
|
||||
</CardHeader>
|
||||
{expandedUri === resource.uri && (
|
||||
<CardContent>
|
||||
{loadingContent ? (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('mcp.loading')}
|
||||
</div>
|
||||
) : resourceContent?.type === 'text' && resourceContent.text ? (
|
||||
<pre className="text-xs bg-muted p-3 rounded-md overflow-auto max-h-[200px] whitespace-pre-wrap break-all">
|
||||
{resourceContent.text}
|
||||
</pre>
|
||||
) : resourceContent?.type === 'blob' ? (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('mcp.resourceBinaryContent')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('mcp.resourceReadFailed')}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RuntimePanel({
|
||||
mcpTesting,
|
||||
runtimeInfo,
|
||||
serverName,
|
||||
t,
|
||||
}: {
|
||||
mcpTesting: boolean;
|
||||
runtimeInfo: MCPServerRuntimeInfo | null;
|
||||
serverName: string;
|
||||
t: TFunction;
|
||||
}) {
|
||||
// Show tools whenever we have runtime info — either an edit-mode server or a
|
||||
@@ -267,6 +361,7 @@ function RuntimePanel({
|
||||
const isConnected =
|
||||
!mcpTesting && runtimeInfo.status === MCPSessionStatus.CONNECTED;
|
||||
const tools = runtimeInfo.tools || [];
|
||||
const resources = runtimeInfo.resources || [];
|
||||
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
@@ -278,7 +373,16 @@ function RuntimePanel({
|
||||
|
||||
{isConnected && tools.length > 0 && <ToolsList tools={tools} t={t} />}
|
||||
|
||||
{isConnected && tools.length === 0 && (
|
||||
{isConnected && resources.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">
|
||||
{t('mcp.resourceCount', { count: resources.length })}
|
||||
</div>
|
||||
<ResourcesList resources={resources} serverName={serverName} t={t} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isConnected && tools.length === 0 && resources.length === 0 && (
|
||||
<div className="flex min-h-[220px] items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground">
|
||||
{t('mcp.noToolsFound')}
|
||||
</div>
|
||||
@@ -732,6 +836,8 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
||||
error_message: errorMsg,
|
||||
tool_count: 0,
|
||||
tools: [],
|
||||
resource_count: 0,
|
||||
resources: [],
|
||||
});
|
||||
} else {
|
||||
if (isEditMode) {
|
||||
@@ -1026,7 +1132,12 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
||||
);
|
||||
|
||||
const runtimePanel = (
|
||||
<RuntimePanel mcpTesting={mcpTesting} runtimeInfo={runtimeInfo} t={t} />
|
||||
<RuntimePanel
|
||||
mcpTesting={mcpTesting}
|
||||
runtimeInfo={runtimeInfo}
|
||||
serverName={form.getValues('name')}
|
||||
t={t}
|
||||
/>
|
||||
);
|
||||
|
||||
// In edit mode the right side shows a tablist switching between the live
|
||||
|
||||
@@ -561,6 +561,8 @@ export interface MCPServerRuntimeInfo {
|
||||
* server runs inside Box. Absent when Box is unavailable. */
|
||||
box_session_id?: string;
|
||||
box_enabled?: boolean;
|
||||
resource_count: number;
|
||||
resources: MCPResource[];
|
||||
}
|
||||
|
||||
export type MCPServer =
|
||||
@@ -615,6 +617,29 @@ export interface MCPTool {
|
||||
parameters?: object;
|
||||
}
|
||||
|
||||
export interface MCPResource {
|
||||
uri: string;
|
||||
name: string;
|
||||
description: string;
|
||||
mime_type: string;
|
||||
}
|
||||
|
||||
export interface MCPResourceContent {
|
||||
uri: string;
|
||||
mime_type: string;
|
||||
type: 'text' | 'blob';
|
||||
text?: string;
|
||||
blob?: string;
|
||||
}
|
||||
|
||||
export interface ApiRespMCPResources {
|
||||
resources: MCPResource[];
|
||||
}
|
||||
|
||||
export interface ApiRespMCPResourceContents {
|
||||
contents: MCPResourceContent[];
|
||||
}
|
||||
|
||||
export interface PluginTool {
|
||||
name: string;
|
||||
description: string;
|
||||
|
||||
@@ -40,6 +40,8 @@ import {
|
||||
BoxSessionInfo,
|
||||
ApiRespMCPServers,
|
||||
ApiRespMCPServer,
|
||||
ApiRespMCPResources,
|
||||
ApiRespMCPResourceContents,
|
||||
MCPServer,
|
||||
ApiRespModelProviders,
|
||||
ApiRespModelProvider,
|
||||
@@ -905,6 +907,21 @@ export class BackendClient extends BaseHttpClient {
|
||||
return this.post('/api/v1/mcp/servers', { source });
|
||||
}
|
||||
|
||||
public getMCPServerResources(
|
||||
serverName: string,
|
||||
): Promise<ApiRespMCPResources> {
|
||||
return this.get(`/api/v1/mcp/servers/${serverName}/resources`);
|
||||
}
|
||||
|
||||
public readMCPServerResource(
|
||||
serverName: string,
|
||||
uri: string,
|
||||
): Promise<ApiRespMCPResourceContents> {
|
||||
return this.post(`/api/v1/mcp/servers/${serverName}/resources/read`, {
|
||||
uri,
|
||||
});
|
||||
}
|
||||
|
||||
// ============ System API ============
|
||||
public getSystemInfo(): Promise<ApiRespSystemInfo> {
|
||||
return this.get('/api/v1/system/info');
|
||||
|
||||
@@ -824,6 +824,9 @@ const enUS = {
|
||||
toolCount: 'Tools: {{count}}',
|
||||
parameterCount: 'Parameters: {{count}}',
|
||||
noParameters: 'No parameters',
|
||||
resourceCount: 'Resources: {{count}}',
|
||||
resourceBinaryContent: 'Binary content (cannot be displayed)',
|
||||
resourceReadFailed: 'Failed to read resource content',
|
||||
statusConnected: 'Connected',
|
||||
statusDisconnected: 'Disconnected',
|
||||
statusError: 'Connection Error',
|
||||
|
||||
@@ -838,6 +838,9 @@ const esES = {
|
||||
toolCount: 'Herramientas: {{count}}',
|
||||
parameterCount: 'Parámetros: {{count}}',
|
||||
noParameters: 'Sin parámetros',
|
||||
resourceCount: 'Recursos: {{count}}',
|
||||
resourceBinaryContent: 'Contenido binario (no se puede mostrar)',
|
||||
resourceReadFailed: 'Error al leer el contenido del recurso',
|
||||
statusConnected: 'Conectado',
|
||||
statusDisconnected: 'Desconectado',
|
||||
statusError: 'Error de conexión',
|
||||
|
||||
@@ -830,6 +830,9 @@ const jaJP = {
|
||||
toolCount: 'ツール:{{count}}',
|
||||
parameterCount: 'パラメータ:{{count}}',
|
||||
noParameters: 'パラメータなし',
|
||||
resourceCount: 'リソース:{{count}}',
|
||||
resourceBinaryContent: 'バイナリコンテンツ(表示できません)',
|
||||
resourceReadFailed: 'リソースの読み込みに失敗しました',
|
||||
statusConnected: '接続済み',
|
||||
statusDisconnected: '未接続',
|
||||
statusError: '接続エラー',
|
||||
|
||||
@@ -835,6 +835,9 @@ const ruRU = {
|
||||
toolCount: 'Инструменты: {{count}}',
|
||||
parameterCount: 'Параметры: {{count}}',
|
||||
noParameters: 'Нет параметров',
|
||||
resourceCount: 'Ресурсы: {{count}}',
|
||||
resourceBinaryContent: 'Двоичное содержимое (невозможно отобразить)',
|
||||
resourceReadFailed: 'Не удалось прочитать содержимое ресурса',
|
||||
statusConnected: 'Подключён',
|
||||
statusDisconnected: 'Отключён',
|
||||
statusError: 'Ошибка подключения',
|
||||
|
||||
@@ -813,6 +813,9 @@ const thTH = {
|
||||
toolCount: 'เครื่องมือ: {{count}}',
|
||||
parameterCount: 'พารามิเตอร์: {{count}}',
|
||||
noParameters: 'ไม่มีพารามิเตอร์',
|
||||
resourceCount: 'ทรัพยากร: {{count}}',
|
||||
resourceBinaryContent: 'เนื้อหาไบนารี (ไม่สามารถแสดงได้)',
|
||||
resourceReadFailed: 'ไม่สามารถอ่านเนื้อหาทรัพยากรได้',
|
||||
statusConnected: 'เชื่อมต่อแล้ว',
|
||||
statusDisconnected: 'ไม่ได้เชื่อมต่อ',
|
||||
statusError: 'ข้อผิดพลาดการเชื่อมต่อ',
|
||||
|
||||
@@ -828,6 +828,9 @@ const viVN = {
|
||||
toolCount: 'Công cụ: {{count}}',
|
||||
parameterCount: 'Tham số: {{count}}',
|
||||
noParameters: 'Không có tham số',
|
||||
resourceCount: 'Tài nguyên: {{count}}',
|
||||
resourceBinaryContent: 'Nội dung nhị phân (không thể hiển thị)',
|
||||
resourceReadFailed: 'Không thể đọc nội dung tài nguyên',
|
||||
statusConnected: 'Đã kết nối',
|
||||
statusDisconnected: 'Đã ngắt kết nối',
|
||||
statusError: 'Lỗi kết nối',
|
||||
|
||||
@@ -791,6 +791,9 @@ const zhHans = {
|
||||
toolCount: '工具:{{count}}',
|
||||
parameterCount: '参数:{{count}}',
|
||||
noParameters: '无参数',
|
||||
resourceCount: '资源:{{count}}',
|
||||
resourceBinaryContent: '二进制内容(无法显示)',
|
||||
resourceReadFailed: '读取资源内容失败',
|
||||
statusConnected: '已连接',
|
||||
statusDisconnected: '未连接',
|
||||
statusError: '连接错误',
|
||||
|
||||
@@ -790,6 +790,9 @@ const zhHant = {
|
||||
toolCount: '工具:{{count}}',
|
||||
parameterCount: '參數:{{count}}',
|
||||
noParameters: '無參數',
|
||||
resourceCount: '資源:{{count}}',
|
||||
resourceBinaryContent: '二進位內容(無法顯示)',
|
||||
resourceReadFailed: '讀取資源內容失敗',
|
||||
statusConnected: '已連線',
|
||||
statusDisconnected: '未連線',
|
||||
statusError: '連接錯誤',
|
||||
|
||||
Reference in New Issue
Block a user