feat: implement JSON agent export/import with improved UX

- Add backend API endpoints for agent export/import
  - GET /agents/{agent_id}/export - exports agent configuration as JSON
  - POST /agents/import - imports agent from JSON (new agents only)
  - Add AgentExportData and AgentImportRequest Pydantic models

- Integrate export functionality in agent config header
  - Add always-visible 3-dots menu left of "Prompt to Build" tab
  - Include export option in dropdown menu for all agent types
  - Handle loading states during export process

- Add subtle import functionality to new agent dialog
  - Replace mode selection with "or import from JSON" link
  - Support file upload and validation for agent JSON
  - Streamline UX for creating agents from templates

- Create React Query hooks for export/import operations
  - useExportAgent: handles JSON download with proper error handling
  - useImportAgent: creates new agent and invalidates cache

- Remove update existing agent option (import creates new agents only)
- Fix API integration to use direct fetch calls with backend URL
- Clean up unused components and improve code organization

This enables users to share agent configurations across instances
and create agents from templates with an intuitive interface.
This commit is contained in:
marko-kraemer 2025-08-03 02:01:31 +02:00
parent 286e7edeef
commit adf9326f61
6 changed files with 673 additions and 46 deletions

View File

@ -146,6 +146,27 @@ class ThreadAgentResponse(BaseModel):
source: str # "thread", "default", "none", "missing"
message: str
class AgentExportData(BaseModel):
"""Exportable agent configuration data"""
name: str
description: Optional[str] = None
system_prompt: str
agentpress_tools: Dict[str, Any]
configured_mcps: List[Dict[str, Any]]
custom_mcps: List[Dict[str, Any]]
avatar: Optional[str] = None
avatar_color: Optional[str] = None
tags: Optional[List[str]] = []
metadata: Optional[Dict[str, Any]] = None
export_version: str = "1.0"
exported_at: str
exported_by: Optional[str] = None
class AgentImportRequest(BaseModel):
"""Request to import an agent from JSON"""
import_data: AgentExportData
import_as_new: bool = True # Always true, only creating new agents is supported
def initialize(
_db: DBConnection,
_instance_id: Optional[str] = None
@ -1589,6 +1610,140 @@ async def get_agent(agent_id: str, user_id: str = Depends(get_current_user_id_fr
logger.error(f"Error fetching agent {agent_id} for user {user_id}: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to fetch agent: {str(e)}")
@router.get("/agents/{agent_id}/export", response_model=AgentExportData)
async def export_agent(agent_id: str, user_id: str = Depends(get_current_user_id_from_jwt)):
"""Export an agent configuration as JSON"""
logger.info(f"Exporting agent {agent_id} for user: {user_id}")
try:
client = await db.client
# Get agent data
agent_result = await client.table('agents').select('*').eq('agent_id', agent_id).eq('account_id', user_id).execute()
if not agent_result.data:
raise HTTPException(status_code=404, detail="Agent not found")
agent = agent_result.data[0]
# Get current version data if available
current_version = None
if agent.get('current_version_id'):
version_result = await client.table('agent_versions').select('*').eq('version_id', agent['current_version_id']).execute()
if version_result.data:
current_version = version_result.data[0]
# Extract configuration using existing helper
from agent.config_helper import extract_agent_config
config = extract_agent_config(agent, current_version)
# Clean metadata for export (remove instance-specific data)
export_metadata = {}
if agent.get('metadata'):
export_metadata = {k: v for k, v in agent['metadata'].items()
if k not in ['is_suna_default', 'centrally_managed', 'installation_date', 'last_central_update']}
# Create export data
export_data = AgentExportData(
name=config.get('name', ''),
description=config.get('description', ''),
system_prompt=config.get('system_prompt', ''),
agentpress_tools=config.get('agentpress_tools', {}),
configured_mcps=config.get('configured_mcps', []),
custom_mcps=config.get('custom_mcps', []),
avatar=config.get('avatar'),
avatar_color=config.get('avatar_color'),
tags=agent.get('tags', []),
metadata=export_metadata,
exported_at=datetime.utcnow().isoformat(),
exported_by=user_id
)
logger.info(f"Successfully exported agent {agent_id}")
return export_data
except Exception as e:
logger.error(f"Error exporting agent {agent_id}: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to export agent: {str(e)}")
@router.post("/agents/import", response_model=AgentResponse)
async def import_agent(
import_request: AgentImportRequest,
user_id: str = Depends(get_current_user_id_from_jwt)
):
"""Import an agent from JSON configuration"""
logger.info(f"Importing agent for user: {user_id}")
if not await is_enabled("custom_agents"):
raise HTTPException(
status_code=403,
detail="Custom agents currently disabled. This feature is not available at the moment."
)
try:
client = await db.client
import_data = import_request.import_data
# Validate import data
if not import_data.name or not import_data.system_prompt:
raise HTTPException(status_code=400, detail="Agent name and system prompt are required")
# Create new agent
logger.info(f"Creating new agent from import: {import_data.name}")
# Check if user wants to set as default, and if so, unset other defaults
is_default = False # Imported agents are not default by default
insert_data = {
"account_id": user_id,
"name": import_data.name,
"description": import_data.description,
"avatar": import_data.avatar,
"avatar_color": import_data.avatar_color,
"is_default": is_default,
"tags": import_data.tags or [],
"version_count": 1,
"metadata": import_data.metadata or {}
}
new_agent = await client.table('agents').insert(insert_data).execute()
if not new_agent.data:
raise HTTPException(status_code=500, detail="Failed to create agent from import")
agent = new_agent.data[0]
agent_id = agent['agent_id']
# Create initial version
try:
version_service = await _get_version_service()
version = await version_service.create_version(
agent_id=agent_id,
user_id=user_id,
system_prompt=import_data.system_prompt,
configured_mcps=import_data.configured_mcps,
custom_mcps=import_data.custom_mcps,
agentpress_tools=import_data.agentpress_tools,
version_name="v1",
change_description="Initial version from import"
)
logger.info(f"Successfully imported agent {agent_id}")
return await get_agent(agent_id, user_id)
except Exception as e:
# Clean up agent if anything goes wrong
await client.table('agents').delete().eq('agent_id', agent_id).execute()
raise e
except HTTPException:
raise
except Exception as e:
logger.error(f"Error importing agent: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to import agent: {str(e)}")
@router.post("/agents", response_model=AgentResponse)
async def create_agent(
agent_data: AgentCreateRequest,

View File

@ -23,6 +23,7 @@ import { cn } from '@/lib/utils';
import { AgentHeader, VersionAlert, AgentBuilderTab, ConfigurationTab } from '@/components/agents/config';
import { UpcomingRunsDropdown } from '@/components/agents/upcoming-runs-dropdown';
import { useExportAgent } from '@/hooks/react-query/agents/use-agent-export-import';
interface FormData {
name: string;
@ -50,6 +51,7 @@ export default function AgentConfigurationPage() {
const updateAgentMutation = useUpdateAgent();
const createVersionMutation = useCreateAgentVersion();
const activateVersionMutation = useActivateAgentVersion();
const exportMutation = useExportAgent();
const [formData, setFormData] = useState<FormData>({
name: '',
@ -389,6 +391,11 @@ export default function AgentConfigurationPage() {
}
}, [agentId, activateVersionMutation]);
const handleExport = useCallback(() => {
if (!agentId) return;
exportMutation.mutate(agentId);
}, [agentId, exportMutation]);
useEffect(() => {
if (isViewingOldVersion && activeTab === 'agent-builder') {
setActiveTab('configuration');
@ -526,6 +533,8 @@ export default function AgentConfigurationPage() {
onFieldChange={handleFieldChange}
onStyleChange={handleStyleChange}
onTabChange={setActiveTab}
onExport={handleExport}
isExporting={exportMutation.isPending}
agentMetadata={agent?.metadata}
/>
</div>
@ -655,6 +664,8 @@ export default function AgentConfigurationPage() {
onFieldChange={handleFieldChange}
onStyleChange={handleStyleChange}
onTabChange={setActiveTab}
onExport={handleExport}
isExporting={exportMutation.isPending}
agentMetadata={agent?.metadata}
/>
</div>
@ -724,6 +735,8 @@ export default function AgentConfigurationPage() {
</div>
</DrawerContent>
</Drawer>
</div>
</div>
</div>

View File

@ -1,11 +1,18 @@
import React from 'react';
import { Sparkles, Settings } from 'lucide-react';
import { Sparkles, Settings, MoreHorizontal, Download } from 'lucide-react';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { EditableText } from '@/components/ui/editable';
import { StylePicker } from '../style-picker';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
import { KortixLogo } from '@/components/sidebar/kortix-logo';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import { Button } from '@/components/ui/button';
interface AgentHeaderProps {
agentId: string;
@ -22,6 +29,8 @@ interface AgentHeaderProps {
onFieldChange: (field: string, value: any) => void;
onStyleChange: (emoji: string, color: string) => void;
onTabChange: (value: string) => void;
onExport?: () => void;
isExporting?: boolean;
agentMetadata?: {
is_suna_default?: boolean;
restrictions?: {
@ -39,6 +48,8 @@ export function AgentHeader({
onFieldChange,
onStyleChange,
onTabChange,
onExport,
isExporting = false,
agentMetadata,
}: AgentHeaderProps) {
const isSunaAgent = agentMetadata?.is_suna_default || false;
@ -60,7 +71,7 @@ export function AgentHeader({
<div className="flex items-center gap-3">
<div className="relative">
{isSunaAgent ? (
<div className="h-9 w-9 bg-background rounded-lg bg-muted border border flex items-center justify-center">
<div className="h-9 w-9 rounded-lg bg-muted border flex items-center justify-center">
<KortixLogo size={16} />
</div>
) : (
@ -93,30 +104,59 @@ export function AgentHeader({
</div>
</div>
{!isSunaAgent && (
<Tabs value={activeTab} onValueChange={onTabChange}>
<TabsList className="grid grid-cols-2 bg-muted/50 h-9">
<TabsTrigger
value="agent-builder"
disabled={isViewingOldVersion}
className={cn(
"flex items-center gap-2 text-sm data-[state=active]:bg-background data-[state=active]:shadow-sm",
isViewingOldVersion && "opacity-50 cursor-not-allowed"
)}
>
<Sparkles className="h-3 w-3" />
Prompt to Build
</TabsTrigger>
<TabsTrigger
value="configuration"
className="flex items-center gap-2 text-sm data-[state=active]:bg-background data-[state=active]:shadow-sm"
>
<Settings className="h-3 w-3" />
Manual Config
</TabsTrigger>
</TabsList>
</Tabs>
)}
<div className="flex items-center gap-2">
{/* 3-dots menu for actions - always show if onExport is available */}
{onExport && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
disabled={isExporting}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-40">
<DropdownMenuItem
onClick={onExport}
disabled={isExporting}
className="flex items-center gap-2 text-xs"
>
<Download className="h-3 w-3" />
Export agent
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
{/* Only show tabs for non-Suna agents */}
{!isSunaAgent && (
<Tabs value={activeTab} onValueChange={onTabChange}>
<TabsList className="grid grid-cols-2 bg-muted/50 h-9">
<TabsTrigger
value="agent-builder"
disabled={isViewingOldVersion}
className={cn(
"flex items-center gap-2 text-sm data-[state=active]:bg-background data-[state=active]:shadow-sm",
isViewingOldVersion && "opacity-50 cursor-not-allowed"
)}
>
<Sparkles className="h-3 w-3" />
Prompt to Build
</TabsTrigger>
<TabsTrigger
value="configuration"
className="flex items-center gap-2 text-sm data-[state=active]:bg-background data-[state=active]:shadow-sm"
>
<Settings className="h-3 w-3" />
Manual Config
</TabsTrigger>
</TabsList>
</Tabs>
)}
</div>
</div>
);
}

View File

@ -1,7 +1,7 @@
'use client';
import React from 'react';
import { Loader2 } from 'lucide-react';
import React, { useState, useRef } from 'react';
import { Loader2, Upload, Plus, FileJson, CheckCircle2, AlertCircle } from 'lucide-react';
import {
AlertDialog,
AlertDialogAction,
@ -12,7 +12,13 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { useCreateNewAgent } from '@/hooks/react-query/agents/use-agents';
import { useImportAgent, parseAgentImportFile, type AgentExportData } from '@/hooks/react-query/agents/use-agent-export-import';
import { toast } from 'sonner';
interface NewAgentDialogProps {
open: boolean;
@ -21,7 +27,14 @@ interface NewAgentDialogProps {
}
export function NewAgentDialog({ open, onOpenChange, onSuccess }: NewAgentDialogProps) {
const [mode, setMode] = useState<'create' | 'import'>('create');
const [importFile, setImportFile] = useState<File | null>(null);
const [importData, setImportData] = useState<AgentExportData | null>(null);
const [isProcessingFile, setIsProcessingFile] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const createNewAgentMutation = useCreateNewAgent();
const importMutation = useImportAgent();
const handleCreateNewAgent = () => {
createNewAgentMutation.mutate(undefined, {
@ -36,33 +49,188 @@ export function NewAgentDialog({ open, onOpenChange, onSuccess }: NewAgentDialog
});
};
const isLoading = createNewAgentMutation.isPending;
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
if (!file.name.endsWith('.json')) {
toast.error('Please select a JSON file');
return;
}
setIsProcessingFile(true);
try {
const data = await parseAgentImportFile(file);
setImportFile(file);
setImportData(data);
toast.success('Import file loaded successfully');
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to parse import file');
setImportFile(null);
setImportData(null);
} finally {
setIsProcessingFile(false);
}
};
const handleImport = () => {
if (!importData) {
toast.error('No import data available');
return;
}
importMutation.mutate({
import_data: importData,
import_as_new: true
}, {
onSuccess: () => {
onOpenChange(false);
onSuccess?.();
// Reset form
setImportFile(null);
setImportData(null);
setMode('create');
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
});
};
const handleDialogClose = (open: boolean) => {
if (!open) {
// Reset form when closing
setImportFile(null);
setImportData(null);
setMode('create');
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
onOpenChange(open);
};
const isLoading = createNewAgentMutation.isPending || importMutation.isPending;
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialog open={open} onOpenChange={handleDialogClose}>
<AlertDialogContent className="max-w-md">
<AlertDialogHeader>
<AlertDialogTitle>Create New Agent</AlertDialogTitle>
<AlertDialogDescription>
This will create a new agent with a default name and description.
This will create a new agent that you can customize and configure.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="space-y-4">
{/* Content based on mode */}
{mode === 'create' ? (
<div className="space-y-3">
<div className="text-sm text-muted-foreground">
This will create a new agent with a default name and description that you can customize later.
</div>
{/* Subtle import option */}
<div className="text-center">
<button
onClick={() => setMode('import')}
className="text-xs text-muted-foreground hover:text-foreground underline transition-colors"
disabled={isLoading}
>
or import from JSON
</button>
</div>
</div>
) : (
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label htmlFor="import-file">Select Agent JSON File</Label>
<button
onClick={() => setMode('create')}
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
disabled={isLoading}
>
back to create blank
</button>
</div>
<Input
ref={fileInputRef}
id="import-file"
type="file"
accept=".json"
onChange={handleFileChange}
className="mt-1"
disabled={isLoading}
/>
{isProcessingFile && (
<Alert>
<Loader2 className="h-4 w-4 animate-spin" />
<AlertDescription>
Processing import file...
</AlertDescription>
</Alert>
)}
{importData && (
<Alert>
<CheckCircle2 className="h-4 w-4 text-green-600" />
<AlertDescription>
<div className="space-y-1">
<div><strong>Agent:</strong> {importData.name}</div>
{importData.description && (
<div><strong>Description:</strong> {importData.description}</div>
)}
<div><strong>Exported:</strong> {new Date(importData.exported_at).toLocaleString()}</div>
</div>
</AlertDescription>
</Alert>
)}
</div>
)}
</div>
<AlertDialogFooter>
<AlertDialogCancel disabled={isLoading}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleCreateNewAgent}
disabled={isLoading}
className="min-w-[100px]"
>
{isLoading ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Creating...
</>
) : (
'Create'
)}
</AlertDialogAction>
{mode === 'create' ? (
<AlertDialogAction
onClick={handleCreateNewAgent}
disabled={isLoading}
className="min-w-[100px]"
>
{createNewAgentMutation.isPending ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Creating...
</>
) : (
<>
<Plus className="h-4 w-4 mr-2" />
Create Agent
</>
)}
</AlertDialogAction>
) : (
<AlertDialogAction
onClick={handleImport}
disabled={!importData || isLoading}
className="min-w-[100px]"
>
{importMutation.isPending ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Importing...
</>
) : (
<>
<Upload className="h-4 w-4 mr-2" />
Import Agent
</>
)}
</AlertDialogAction>
)}
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

View File

@ -0,0 +1,207 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { createClient } from "@/lib/supabase/client";
const API_URL = process.env.NEXT_PUBLIC_BACKEND_URL || '';
export interface AgentExportData {
name: string;
description?: string;
system_prompt: string;
agentpress_tools: Record<string, any>;
configured_mcps: Array<{
name: string;
config: Record<string, any>;
}>;
custom_mcps: Array<{
name: string;
type: 'json' | 'sse';
config: Record<string, any>;
enabledTools: string[];
}>;
avatar?: string;
avatar_color?: string;
tags?: string[];
metadata?: Record<string, any>;
export_version: string;
exported_at: string;
exported_by?: string;
}
export interface AgentImportRequest {
import_data: AgentExportData;
import_as_new: boolean;
}
// Export agent hook
export const useExportAgent = () => {
return useMutation({
mutationFn: async (agentId: string): Promise<AgentExportData> => {
console.log('Exporting agent:', agentId);
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
throw new Error('You must be logged in to export agents');
}
const response = await fetch(`${API_URL}/agents/${agentId}/export`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}`,
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: 'Unknown error' }));
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log('Export response:', data);
return data;
},
onSuccess: (data, agentId) => {
console.log('Export successful for agent:', agentId, 'Data:', data);
// Create and download JSON file
const jsonString = JSON.stringify(data, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `${data.name.replace(/[^a-zA-Z0-9]/g, '_')}_agent_export.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
toast.success(`Agent "${data.name}" exported successfully`);
},
onError: (error: any) => {
console.error('Export error:', error);
toast.error(error?.message || "Failed to export agent");
},
});
};
// Import agent hook
export const useImportAgent = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (importRequest: AgentImportRequest) => {
console.log('Importing agent:', importRequest);
const supabase = createClient();
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
throw new Error('You must be logged in to import agents');
}
const response = await fetch(`${API_URL}/agents/import`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}`,
},
body: JSON.stringify(importRequest),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: 'Unknown error' }));
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log('Import response:', data);
return data;
},
onSuccess: (data, variables) => {
console.log('Import successful:', data);
// Invalidate agents list to refresh data
queryClient.invalidateQueries({ queryKey: ['agents'] });
const action = variables.import_as_new ? 'imported' : 'updated';
toast.success(`Agent "${variables.import_data.name}" ${action} successfully`);
},
onError: (error: any) => {
console.error('Import error:', error);
toast.error(error?.message || "Failed to import agent");
},
});
};
// Helper function to validate import data
export const validateImportData = (data: any): { isValid: boolean; errors: string[] } => {
const errors: string[] = [];
if (!data) {
errors.push("No data provided");
return { isValid: false, errors };
}
if (!data.name || typeof data.name !== 'string') {
errors.push("Agent name is required");
}
if (!data.system_prompt || typeof data.system_prompt !== 'string') {
errors.push("System prompt is required");
}
if (!data.export_version) {
errors.push("Export version is missing - this may not be a valid agent export file");
}
if (data.agentpress_tools && typeof data.agentpress_tools !== 'object') {
errors.push("Invalid agentpress_tools format");
}
if (data.configured_mcps && !Array.isArray(data.configured_mcps)) {
errors.push("Invalid configured_mcps format");
}
if (data.custom_mcps && !Array.isArray(data.custom_mcps)) {
errors.push("Invalid custom_mcps format");
}
return {
isValid: errors.length === 0,
errors
};
};
// Helper function to read and parse JSON file
export const parseAgentImportFile = (file: File): Promise<AgentExportData> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target?.result as string;
const data = JSON.parse(content);
const validation = validateImportData(data);
if (!validation.isValid) {
reject(new Error(`Invalid import file: ${validation.errors.join(', ')}`));
return;
}
resolve(data as AgentExportData);
} catch (error) {
reject(new Error('Failed to parse JSON file'));
}
};
reader.onerror = () => {
reject(new Error('Failed to read file'));
};
reader.readAsText(file);
});
};

44
sample_agent_export.json Normal file
View File

@ -0,0 +1,44 @@
{
"name": "Research Assistant",
"description": "An AI assistant specialized in conducting research and providing comprehensive analysis",
"system_prompt": "You are a research assistant with expertise in gathering, analyzing, and synthesizing information from various sources. Your approach is thorough and methodical.\n\nKey capabilities:\n- Conduct comprehensive web research\n- Analyze and synthesize information from multiple sources\n- Provide well-structured reports and summaries\n- Fact-check information for accuracy\n- Identify trends and patterns in data\n\nAlways cite your sources and provide evidence-based conclusions.",
"agentpress_tools": {
"web_search": {
"enabled": true,
"description": "Search the web for current information and research"
},
"sb_files": {
"enabled": true,
"description": "Read and write files for research documentation"
},
"code_interpreter": {
"enabled": true,
"description": "Analyze data and create visualizations"
}
},
"configured_mcps": [
{
"name": "filesystem",
"qualifiedName": "@modelcontextprotocol/server-filesystem",
"config": {
"path": "/research-data"
},
"enabledTools": [
"read_file",
"write_file",
"list_directory"
]
}
],
"custom_mcps": [],
"avatar": "🔬",
"avatar_color": "#4F46E5",
"tags": ["research", "analysis", "academic"],
"metadata": {
"category": "productivity",
"use_cases": ["academic research", "market analysis", "fact checking"]
},
"export_version": "1.0",
"exported_at": "2024-01-15T10:30:00.000Z",
"exported_by": "user123"
}