本篇是作者上一篇文章针对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的网站上有这样一套的集成逻辑:

  1. 点击 "New Agent" 按钮(不是寻找现有的Agent卡片)
  2. 进入Agent配置页面(URL会是 /agents/new/[agentId]
  3. 选择 "Manual" 选项卡(不是Agent Builder)
  4. 向下滚动找到手风琴菜单
  5. 点击展开 "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的潜质。

就写到这里,接下来要作者会进行更多的工程搭建工作。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐