SUNA 二次开发日志 (从日志端显示到网页端显示,上下文对话管理,创建agent,接入mcp工具)
本篇是作者上一篇文章的续篇。显然,这不是一个令人满意的结果,尤其是考虑到该产品未来要实际落地,不可能让用户提一次问看一次日志,所以我们需要继续修改。分析:目前网页存在问题,就要从网页入手,按f12打开开发者工具:发现错误仍然很多,有protocol error, 有404,有custom agents is not enabled,还有406。
本篇是作者上一篇文章针对suna本地部署时遇到“Cannot connect to backend server. ...” 问题的解决方案_unable to connect to anthropic services failed to -CSDN博客的续篇。
前文讲到,我在linux系统上本地部署了suna,然而此时的suna依旧是一个半成品,具体表现为:
- 可以进入localhost:3000且无报错。
- 在对话框内输入文本,点击回车,不报错,但是也不会有任何显示。
- 在整个localhost:3000的页面上,只有文本框和回车是可以互动的,其余按钮均失效。
- 对于用户提问的回答,可以在Linux上通过输入指令->查看日志的形式获得,大概长这样:
显然,这不是一个令人满意的结果,尤其是考虑到该产品未来要实际落地,不可能让用户提一次问看一次日志,所以我们需要继续修改。
分析:目前网页存在问题,就要从网页入手,按f12打开开发者工具:
发现错误仍然很多,有protocol error, 有404,有custom agents is not enabled,还有406。需要一个一个地解决,都解决之后,就会出现如下的工作状态,也是本文的最终目标:
- 输入文字,自动命名创建新的agent
- agent的回复和思考过程,工具调用结果都能体现在网页上
- agent允许在一个聊天里重复对话,且能记住用户之前的内容
首先我要解决custom agent is not enabled 的问题,因为404和406,error 是连接层面的问题,而not enabled 是配置层面的问题,涉及到agent的基本功能。如果这个问题不解决,就算连接成功了,也无法创建自己的agent,所以这个是目前的重点。
问题诊断一:
对于custom agent is not enabled,初步的分析是前端和后端的检查逻辑不同步。进一步检查日志发现:前端通过Feature Flags API检查Custom Agents状态,但API返回空配置。
那为什么api会返回空配置呢?输入:
cat ~/suna/backend/flags/flags.py
查看后端中关于feature flags的代码,可以发现:
Feature Flags系统依赖Redis存储,但Redis中没有存储任何Custom Agents的feature flag
从代码可以看到:
-
list_flags()
从Redis的feature_flags:list
集合中读取所有flag -
如果Redis中没有数据,返回空字典
{}
-
前端收到空的flags,检查Custom Agents失败
解决方案一:
办法是初始化Feature Flag:
尝试以下两种方法:
# 在 backend/main.py 或 api.py 的启动代码中添加
async def initialize_feature_flags( ):
from flags.flags import enable_flag
await enable_flag("custom_agents", "Enable custom agents functionality")
# 进入Redis容器
docker exec -it suna-redis-1 redis-cli
# 设置Custom Agents flag
HSET feature_flag:custom_agents enabled true description "Enable custom agents functionality" updated_at "2025-01-07T10:00:00Z"
SADD feature_flags:list custom_agents
应用此方法后,打开localhost:3000,发现了一些变化:
- 开发者工具中不再报错not enabled
- 页面上新出现了Agent Playground和自选agent的文字选项(虽然点开之后还是只能选suna)
问题诊断二:
接下来我们要处理前文提到的404和406报错,对于这一系列问题,解决的过程比较曲折和复杂。
解决方案二:
首先去检查了前端和后端的api端点,以及查看日志,初步发现了导致406报错的原因:前端试图加载一个不存在的对话线程!
日志中显示
GET https://onltalhduxrmshfzffrd.supabase.co/rest/v1/threads?select=*&thread_id=eq.d458c04d-ec46-4d21-9847-a0eee769f4b4 406 (Not Acceptable)
说明前端在启动时试图恢复一个旧的对话线程, id是".......9847-a0eee769f4b4",但该线程在数据库中不存在,导致.single()
查询失败。
对于这个问题我一开始的解法是创建新的对话线程,但是问题并没有解决,linux端返回的结果也是{"detail":"Not Found"}
# 测试创建新线程的API
curl -X POST "http://192.168.1.136:8003/api/threads" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsImtpZCI6IkpuSXFpNlhvVWViUXhnQkwiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL29ubHRhbGhkdXhybXNoZnpmZnJkLnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiIwNzNjMmU0MS1kNTI3LTRlZmUtYmE1MS1lZmU0YWEyMTM1NTEiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzUxOTU3ODg1LCJpYXQiOjE3NTE5NTQyODUsImVtYWlsIjoiMzA5NTY1OTgyM0BxcS5jb20iLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7ImVtYWlsIjoiMzA5NTY1OTgyM0BxcS5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwicGhvbmVfdmVyaWZpZWQiOmZhbHNlLCJzdWIiOiIwNzNjMmU0MS1kNTI3LTRlZmUtYmE1MS1lZmU0YWEyMTM1NTUxIn0sInJvbGUiOiJhdXRoZW50aWNhdGVkIiwiYWFsIjoiYWFsMSIsImFtciI6W3sibWV0aG9kIjoicGFzc3dvcmQiLCJ0aW1lc3RhbXAiOjE3NTEwMjAyMTB9XSwic2Vzc2lvbl9pZCI6IjNmYjk2YTM4LWU4MTctNDU0Zi1hZmNmLWEzMTRmYzk2MGI2ZCIsImlzX2Fub255bW91cyI6ZmFsc2V9.K13hlO9MvAeul1c4TJPDUjNpAz5-LgwQ5oUBssxyD9A" \
-H "Content-Type: application/json" \
-d '{}'
值得一提的是,上面的指令中第二行里,Bearer后的一长串字符是suna在运行时生成的特定jwt token,在接下来的很多场景中都会有应用。获取方法为:在页面发送请求时,打开开发者工具,点击“网络”选项,就会显示一堆请求,通常随便点开一个就可以在“请求标头”部分看到,如下图所示:
话说回来,由于第一个方法失败了,需要进一步检测问题,检查完后,发现前端依然在试图使用一个不存在的thread_id:d458c04d-ec46-4d21-9847-a0eee769f4b4,
这和之前的问题一样,说明问题是我们的改动没有起效果。
改动没有其效果的原因在于,我们的请求方式有问题,API 期望的是Form Data 而不是JSON,另外,API需要的是prompt字段而不是message字段,根据以上信息,再次修改我们的请求指令:
# 使用 -F 发送form data,而不是 -d 发送JSON
curl -X POST "http://192.168.1.136:8003/api/agent/initiate" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsImtpZCI6IkpuSXFpNlhvVWViUXhnQkwiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL29ubHRhbGhkdXhybXNoZnpmZnJkLnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiIwNzNjMmU0MS1kNTI3LTRlZmUtYmE1MS1lZmU0YWEyMTM1NTEiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzUxOTU3ODg1LCJpYXQiOjE3NTE5NTQyODUsImVtYWlsIjoiMzA5NTY1OTgyM0BxcS5jb20iLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7ImVtYWlsIjoiMzA5NTY1OTgyM0BxcS5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwicGhvbmVfdmVyaWZpZWQiOmZhbHNlLCJzdWIiOiIwNzNjMmU0MS1kNTI3LTRlZmUtYmE1MS1lZmU0YWEyMTM1NTUxIn0sInJvbGUiOiJhdXRoZW50aWNhdGVkIiwiYWFsIjoiYWFsMSIsImFtciI6W3sibWV0aG9kIjoicGFzc3dvcmQiLCJ0aW1lc3RhbXAiOjE3NTEwMjAyMTB9XSwic2Vzc2lvbl9pZCI6IjNmYjk2YTM4LWU4MTctNDU0Zi1hZmNmLWEzMTRmYzk2MGI2ZCIsImlzX2Fub255bW91cyI6ZmFsc2V9.K13hlO9MvAeul1c4TJPDUjNpAz5-LgwQ5oUBssxyD9A" \
-F "prompt=你好"
读者记得把Bearer 后面的token换成自己环境下的。
应用该指令后,我在linux端得到了这样的回复:
{"thread_id":"a8139125-a6be-4edb-9fc5-66b4610998b2","agent_run_id":"8a33016f-ed66-48ed-a30b-192c2e2d41e3"}
这说明我们成功创建了新的thread,而且后端api也在正常工作!这就解决了之前提到的,“前端试图加载不存在的线程”的问题。
但是,这只是解决该问题的第一步,打开开发者工具后依然可以看到各种报错,此处贴上copilot的总结:
我判断,核心问题出在JWT Token的权限上,如果JWT Token的签名过期或者无效,那么所有相关的需要认证的API都会调用失败,所以我需要重新获得有效的token。
一开始的操作是:
先在开发者工具的控制台中运行:
// 获取当前session的token
const session = JSON.parse(localStorage.getItem('sb-onltalhduxrmshfzffrd-auth-token'));
console.log('Current token:', session?.access_token);
然后去linux上:
curl -H "Authorization: Bearer [新的TOKEN]" \
"http://192.168.1.136:8003/api/agent-run/8a33016f-ed66-48ed-a30b-192c2e2d41e3"
结果第一步就显示Current token: undefined undefined,使用在开发者工具界面找到的token去测试,也出现了{"detail":"Not authorized to access this thread"} 的报错。
这就说明,后端的权限检查认为我没有权限访问这个thread,即使是我自己创建的!
同时,在检查中还发现了两个问题:
- 数据库迁移问题,Supabase数据库中没有threads表。
"relation \"graphql_public.threads\" does not exist"
- FormData的append方法使用不正确。
422 (Unprocessable Entity) - Field required
接下来一一列出关于他们的解决方法:
第一个,权限问题,我重新启动了一下suna服务,进入后去开发者工具获取了新的token, 然后将新的token插入进如下的测试指令:
# 直接查询Supabase中的thread信息
curl -H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9ubHRhbGhkdXhybXNoZnpmZnJkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTA3Mjg5NTAsImV4cCI6MjA2NjMwNDk1MH0.bUO7kltXgFmM3BeB960PUv0uN0v4Fdp1erm1nkdyTxY" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsImtpZCI6IkpuSXFpNlhvVWViUXhnQkwiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL29ubHRhbGhkdXhybXNoZnpmZnJkLnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiIwNzNjMmU0MS1kNTI3LTRlZmUtYmE1MS1lZmU0YWEyMTM1NTEiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzUxOTYxNjQxLCJpYXQiOjE3NTE5NTgwNDEsImVtYWlsIjoiMzA5NTY1OTgyM0BxcS5jb20iLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7ImVtYWlsIjoiMzA5NTY1OTgyM0BxcS5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwicGhvbmVfdmVyaWZpZWQiOmZhbHNlLCJzdWIiOiIwNzNjMmU0MS1kNTI3LTRlZmUtYmE1MS1lZmU0YWEyMTM1NTEifSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc1MTAyMDIxMH1dLCJzZXNzaW9uX2lkIjoiM2ZiOTZhMzgtZTgxNy00NTRmLWFmY2YtYTMxNGZjOTYwYjZkIiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.uwA33409iNOjtpUrtLrAdMtczIPyP3VrzFgqMpGlyTc" \
"https://onltalhduxrmshfzffrd.supabase.co/rest/v1/threads?select=*&thread_id=eq.a8139125-a6be-4edb-9fc5-66b4610998b2"
得到了如下的信息:
{"status":"ok","timestamp":"2025-07-08T09:04:32.957334+00:00","instance_id":"single"}
这就说明新的token是有效的,但是数据库仍然是问题。我们需要手动迁徙数据库。
首先远程在supabase中创建threads表:
# 使用Supabase SQL编辑器API直接执行SQL
curl -X POST "https://onltalhduxrmshfzffrd.supabase.co/rest/v1/rpc/exec_sql" \
-H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9ubHRhbGhkdXhybXNoZnpmZnJkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTA3Mjg5NTAsImV4cCI6MjA2NjMwNDk1MH0.bUO7kltXgFmM3BeB960PUv0uN0v4Fdp1erm1nkdyTxY" \
-H "Authorization: Bearer [您的最新TOKEN]" \
-H "Content-Type: application/json" \
-d '{
"sql": "CREATE TABLE IF NOT EXISTS threads (thread_id UUID PRIMARY KEY DEFAULT gen_random_uuid( ), account_id UUID, project_id UUID, is_public BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('"'"'utc'"'"'::text, NOW()) NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('"'"'utc'"'"'::text, NOW()) NOT NULL);"
}'
然后创建threads表依赖的project表:
# 先创建projects表
curl -X POST "https://onltalhduxrmshfzffrd.supabase.co/rest/v1/rpc/exec_sql" \
-H "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9ubHRhbGhkdXhybXNoZnpmZnJkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTA3Mjg5NTAsImV4cCI6MjA2NjMwNDk1MH0.bUO7kltXgFmM3BeB960PUv0uN0v4Fdp1erm1nkdyTxY" \
-H "Authorization: Bearer [您的最新TOKEN]" \
-H "Content-Type: application/json" \
-d '{
"sql": "CREATE TABLE IF NOT EXISTS projects (project_id UUID PRIMARY KEY DEFAULT gen_random_uuid( ), account_id UUID, name TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('"'"'utc'"'"'::text, NOW()) NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT TIMEZONE('"'"'utc'"'"'::text, NOW()) NOT NULL);"
}'
之后问题成功解决。
问题诊断三:
目前可以在suna上进行对话了,但是此时的问题是,每一个新创建的对话里,只能进行一次对话。当suna在这次对话中生成完回复后,回复内容会自动消失,再次尝试对话则会出现如下报错:
分析问题,主要出现在两个方面:数据表结构缺失,RLS策略不匹配。
后端日志中显示:
'column threads.agent_id does not exist', 'code': '42703'
说明 threads表缺少agent_id
列,导致后端无法启动agent。
日志的另一处显示:
new row violates row-level security policy for table "messages"
说明,在messages表的RLS策略只允许@kortix.ai
邮箱用户读取,而我使用是另一个邮箱,无法插入消息,此外,还缺少INSERT策略。
解决方案三:
为了解决这个问题,需要到supbase的SQL编辑器上进行操作:
步骤一,修复threads表结构:
-- 添加缺失的agent_id列
ALTER TABLE public.threads ADD COLUMN agent_id TEXT;
步骤二,修复messages表RLS策略:
-- 删除限制性的读取策略
DROP POLICY "Give read only access to internal users" ON public.messages;
-- 创建适用于所有认证用户的策略
CREATE POLICY "Allow authenticated users to read own messages" ON public.messages
FOR SELECT USING (auth.uid() IS NOT NULL);
CREATE POLICY "Allow authenticated users to insert own messages" ON public.messages
FOR INSERT WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "Allow authenticated users to update own messages" ON public.messages
FOR UPDATE USING (auth.uid() IS NOT NULL);
这样问题就解决了。
问题诊断四:
通过观察suna前端设计的代码结构,有一个好消息是,suna的开源版在设计的时候已经自带了集成mcp工具的逻辑,也就是说,用户不需要做任何代码上的修改,只需要点一点鼠标就可以把自定义或者公共的mcp工具接入到智能体平台当中。
代码部分展示如下:
# frontend/src/app/(dashboard)/agents/page.tsx
'use client';
import React, { useState, useMemo } from 'react';
import { Plus, AlertCircle, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { UpdateAgentDialog } from './_components/update-agent-dialog';
import { useAgents, useUpdateAgent, useDeleteAgent, useOptimisticAgentUpdate, useCreateAgent } from '@/hooks/react-query/agents/use-agents';
import { SearchAndFilters } from './_components/search-and-filters';
import { ResultsInfo } from './_components/results-info';
import { EmptyState } from './_components/empty-state';
import { AgentsGrid } from './_components/agents-grid';
import { AgentsList } from './_components/agents-list';
import { LoadingState } from './_components/loading-state';
import { Pagination } from './_components/pagination';
import { useRouter } from 'next/navigation';
import { DEFAULT_AGENTPRESS_TOOLS } from './_data/tools';
import { AgentsParams } from '@/hooks/react-query/agents/utils';
import { useFeatureFlags } from '@/lib/feature-flags';
type ViewMode = 'grid' | 'list';
type SortOption = 'name' | 'created_at' | 'updated_at' | 'tools_count';
type SortOrder = 'asc' | 'desc';
interface FilterOptions {
hasDefaultAgent: boolean;
hasMcpTools: boolean;
hasAgentpressTools: boolean;
selectedTools: string[];
}
export default function AgentsPage() {
const router = useRouter();
const [editDialogOpen, setEditDialogOpen] = useState(false);
const [editingAgentId, setEditingAgentId] = useState<string | null>(null);
const [viewMode, setViewMode] = useState<ViewMode>('grid');
// Server-side parameters
const [page, setPage] = useState(1);
const [searchQuery, setSearchQuery] = useState('');
const [sortBy, setSortBy] = useState<SortOption>('created_at');
const [sortOrder, setSortOrder] = useState<SortOrder>('desc');
const [filters, setFilters] = useState<FilterOptions>({
hasDefaultAgent: false,
hasMcpTools: false,
hasAgentpressTools: false,
selectedTools: []
});
// Build query parameters
const queryParams: AgentsParams = useMemo(() => {
const params: AgentsParams = {
page,
limit: 20,
search: searchQuery || undefined,
sort_by: sortBy,
sort_order: sortOrder,
};
if (filters.hasDefaultAgent) {
params.has_default = true;
}
if (filters.hasMcpTools) {
params.has_mcp_tools = true;
}
if (filters.hasAgentpressTools) {
params.has_agentpress_tools = true;
}
if (filters.selectedTools.length > 0) {
params.tools = filters.selectedTools.join(',');
}
return params;
}, [page, searchQuery, sortBy, sortOrder, filters]);
const {
data: agentsResponse,
isLoading,
error,
refetch: loadAgents
} = useAgents(queryParams);
const updateAgentMutation = useUpdateAgent();
const deleteAgentMutation = useDeleteAgent();
const createAgentMutation = useCreateAgent();
const { optimisticallyUpdateAgent, revertOptimisticUpdate } = useOptimisticAgentUpdate();
const agents = agentsResponse?.agents || [];
const pagination = agentsResponse?.pagination;
// Get all tools for filter options (we'll need to fetch this separately or compute from current page)
const allTools = useMemo(() => {
const toolsSet = new Set<string>();
agents.forEach(agent => {
agent.configured_mcps?.forEach(mcp => toolsSet.add(`mcp:${mcp.name}`));
Object.entries(agent.agentpress_tools || {}).forEach(([tool, toolData]) => {
if (toolData && typeof toolData === 'object' && 'enabled' in toolData && toolData.enabled) {
toolsSet.add(`agentpress:${tool}`);
}
});
});
return Array.from(toolsSet).sort();
}, [agents]);
const activeFiltersCount = useMemo(() => {
let count = 0;
if (filters.hasDefaultAgent) count++;
if (filters.hasMcpTools) count++;
if (filters.hasAgentpressTools) count++;
count += filters.selectedTools.length;
return count;
}, [filters]);
const clearFilters = () => {
setSearchQuery('');
setFilters({
hasDefaultAgent: false,
hasMcpTools: false,
hasAgentpressTools: false,
selectedTools: []
});
setPage(1);
};
// Reset page when search or filters change
React.useEffect(() => {
setPage(1);
}, [searchQuery, sortBy, sortOrder, filters]);
const handleDeleteAgent = async (agentId: string) => {
try {
await deleteAgentMutation.mutateAsync(agentId);
} catch (error) {
console.error('Error deleting agent:', error);
}
};
const handleToggleDefault = async (agentId: string, currentDefault: boolean) => {
optimisticallyUpdateAgent(agentId, { is_default: !currentDefault });
try {
await updateAgentMutation.mutateAsync({
agentId,
is_default: !currentDefault
});
} catch (error) {
revertOptimisticUpdate(agentId);
console.error('Error updating agent:', error);
}
};
const handleEditAgent = (agentId: string) => {
setEditingAgentId(agentId);
setEditDialogOpen(true);
};
const handleCreateNewAgent = async () => {
try {
const defaultAgentData = {
name: 'New Agent',
description: 'A newly created agent',
system_prompt: 'You are a helpful assistant. Provide clear, accurate, and helpful responses to user queries.',
configured_mcps: [],
agentpress_tools: Object.fromEntries(
Object.entries(DEFAULT_AGENTPRESS_TOOLS).map(([key, value]) => [
key,
{ enabled: value.enabled, description: value.description }
])
),
is_default: false,
};
const newAgent = await createAgentMutation.mutateAsync(defaultAgentData);
router.push(`/agents/new/${newAgent.agent_id}`);
} catch (error) {
console.error('Error creating agent:', error);
}
};
if (error) {
return (
<div className="container mx-auto max-w-7xl px-4 py-8">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
{error instanceof Error ? error.message : 'An error occurred loading agents'}
</AlertDescription>
</Alert>
</div>
);
}
return (
<div className="container mx-auto max-w-7xl px-4 py-8">
<div className="space-y-8">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight text-foreground">
Your Agents
</h1>
<p className="text-md text-muted-foreground max-w-2xl">
Create and manage your AI agents with custom instructions and tools
</p>
</div>
<Button
onClick={handleCreateNewAgent}
disabled={createAgentMutation.isPending}
className="self-start sm:self-center"
>
{createAgentMutation.isPending ? (
<>
<Loader2 className="h-5 w-5 animate-spin" />
Creating...
</>
) : (
<>
<Plus className="h-5 w-5" />
New Agent
</>
)}
</Button>
</div>
<SearchAndFilters
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
sortBy={sortBy}
setSortBy={setSortBy}
sortOrder={sortOrder}
setSortOrder={setSortOrder}
filters={filters}
setFilters={setFilters}
activeFiltersCount={activeFiltersCount}
clearFilters={clearFilters}
viewMode={viewMode}
setViewMode={setViewMode}
allTools={allTools}
/>
<ResultsInfo
isLoading={isLoading}
totalAgents={pagination?.total || 0}
filteredCount={agents.length}
searchQuery={searchQuery}
activeFiltersCount={activeFiltersCount}
clearFilters={clearFilters}
currentPage={pagination?.page || 1}
totalPages={pagination?.pages || 1}
/>
{isLoading ? (
<LoadingState viewMode={viewMode} />
) : agents.length === 0 ? (
<EmptyState
hasAgents={(pagination?.total || 0) > 0}
onCreateAgent={handleCreateNewAgent}
onClearFilters={clearFilters}
/>
) : (
<AgentsGrid
agents={agents}
onEditAgent={handleEditAgent}
onDeleteAgent={handleDeleteAgent}
onToggleDefault={handleToggleDefault}
deleteAgentMutation={deleteAgentMutation}
/>
)}
{pagination && pagination.pages > 1 && (
<Pagination
currentPage={pagination.page}
totalPages={pagination.pages}
onPageChange={setPage}
isLoading={isLoading}
/>
)}
<UpdateAgentDialog
agentId={editingAgentId}
isOpen={editDialogOpen}
onOpenChange={(open) => {
setEditDialogOpen(open);
if (!open) setEditingAgentId(null);
}}
onAgentUpdated={loadAgents}
/>
</div>
</div>
);
}
#frontend/src/app/(dashboard)/agents/_components/agent-mcp-configuration.tsx
import React from 'react';
import { Sparkles } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { MCPConfigurationNew } from './mcp/mcp-configuration-new';
interface AgentMCPConfigurationProps {
mcps: Array<{ name: string; qualifiedName: string; config: any; enabledTools?: string[]; isCustom?: boolean; customType?: 'http' | 'sse' }>;
customMcps?: Array<{ name: string; type: 'http' | 'sse'; config: any; enabledTools: string[] }>;
onMCPsChange: (mcps: Array<{ name: string; qualifiedName: string; config: any; enabledTools?: string[]; isCustom?: boolean; customType?: 'http' | 'sse' }>) => void;
onCustomMCPsChange?: (customMcps: Array<{ name: string; type: 'http' | 'sse'; config: any; enabledTools: string[] }>) => void;
onBatchMCPChange?: (updates: { configured_mcps: any[]; custom_mcps: any[] }) => void;
}
export const AgentMCPConfiguration = ({ mcps, customMcps = [], onMCPsChange, onCustomMCPsChange, onBatchMCPChange }: AgentMCPConfigurationProps) => {
const allMcps = React.useMemo(() => {
const combined = [...mcps];
customMcps.forEach(customMcp => {
combined.push({
name: customMcp.name,
qualifiedName: `custom_${customMcp.type}_${customMcp.name.replace(' ', '_').toLowerCase()}`,
config: customMcp.config,
enabledTools: customMcp.enabledTools,
isCustom: true,
customType: customMcp.type as 'http' | 'sse'
});
});
return combined;
}, [mcps, customMcps]);
const handleConfigurationChange = (updatedMcps: Array<{ name: string; qualifiedName: string; config: any; enabledTools?: string[]; isCustom?: boolean; customType?: 'http' | 'sse' }>) => {
const standardMcps = updatedMcps.filter(mcp => !mcp.isCustom);
const customMcpsList = updatedMcps.filter(mcp => mcp.isCustom);
const transformedCustomMcps = customMcpsList.map(mcp => ({
name: mcp.name,
type: (mcp.customType || 'http') as 'http' | 'sse',
config: mcp.config,
enabledTools: mcp.enabledTools || []
}));
if (onBatchMCPChange) {
onBatchMCPChange({
configured_mcps: standardMcps,
custom_mcps: transformedCustomMcps
});
} else {
onMCPsChange(standardMcps);
if (onCustomMCPsChange) {
onCustomMCPsChange(transformedCustomMcps);
根据这些代码,理论上来讲,suna的网站上有这样一套的集成逻辑:
- 点击 "New Agent" 按钮(不是寻找现有的Agent卡片)
- 进入Agent配置页面(URL会是
/agents/new/[agentId]
) - 选择 "Manual" 选项卡(不是Agent Builder)
- 向下滚动找到手风琴菜单
- 点击展开 "Integrations (via MCP)" (带星星图标和"New"标签)
然而,目前我的界面却没有找到任何可以集成mcp工具的UI,也根本找不到哪里可以创建new Agents。在浏览器页面里输入localhost:3000/agents,就会自动跳转到localhost:3000/dashboard,而在dashboard界面,只能和suna对话,然后让suna调用自己本地注册的工具,和mcp没有关系。
解决方案四:
问题的核心在于怎样创建agent,而不是在dashboard界面点击小加号然后对话。点击小加号对话创建的是project,无论点击多少次加号,新建了多少个project,背后和用户对话的agent依然是初始的suna,这个suna是无法挂在新的公共mcp工具的。只有新建了自定义的agent (此处回扣到我们一开始解决的“custom agent is not enabled”的问题),我们才有能力去自定义设置,接入我们想要的mcp工具。
Agent创建方法:
# 创建一个真正的agent
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Test MCP Agent",
"description": "Agent for MCP testing",
"system_prompt": "You are a helpful assistant with MCP tools.",
"configured_mcps": [],
"agentpress_tools": {},
"is_default": false
}' \
http://192.168.1.136:8003/api/agents
然后,输入这堆指令后会在linux上看到返回来的agent id,接下来,访问
http://localhost:3000/agents/new/[真实的agent_id]
就能进入我们在理论分析中想要看到的界面了:
可以看到,这里允许用户去自己搜索mcp工具,点击一下,接入,就可以了。在这之前,只需要配置好smithery.ai的api key即可。目前为止,suna用到的所有公共mcp工具都可以让smithery.ai这个网站来提供。具体配置过程这里就略过了。
suna很好地应用了以Exa Search为首的搜索和网页自动化工具,并给出了不错的工作反馈,举例来说, “Drake 和 Kendrick之间怎么了?”
与此同时,Qwen的回复是(没开网络搜索):
或者询问一家大学教授和学生合伙建立的初创公司:
对于这一问题,Qwen即便打开了网页搜索功能后依然无法得到正确回复:
说明接入了更多工具的智能体平台已经展现出了超越普通LLM的潜质。
就写到这里,接下来要作者会进行更多的工程搭建工作。
更多推荐
所有评论(0)