Skip to content

Commit f81b149

Browse files
committed
Implement agent and MCP configuration endpoints, enhance install script with environment variable extraction, and update workspace actions for agent and MCP installation.
1 parent ffcde32 commit f81b149

File tree

7 files changed

+341
-30
lines changed

7 files changed

+341
-30
lines changed

app/routes/actions.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from fastapi import APIRouter
1+
from fastapi import APIRouter, HTTPException, Body
22
from app.models.actions import ActionsResponse, Agent, Rule, MCP
33
from app.services.actions_loader import actions_loader
4-
from typing import List
4+
from app.services.mcp_installer import get_agent_content, create_mcp_config
5+
from typing import List, Dict, Any
56

67
router = APIRouter(prefix="/api/actions", tags=["actions"])
78

@@ -27,4 +28,41 @@ async def get_rules():
2728
@router.get("/mcps", response_model=List[MCP])
2829
async def get_mcps():
2930
"""Get all available MCPs"""
30-
return actions_loader.get_mcps()
31+
return actions_loader.get_mcps()
32+
33+
@router.get("/agent-content/{agent_name}")
34+
async def get_agent_content_endpoint(agent_name: str):
35+
"""Get agent content for virtual workspace"""
36+
agents = actions_loader.get_agents()
37+
agent = next((a for a in agents if a.name == agent_name), None)
38+
39+
if not agent:
40+
raise HTTPException(status_code=404, detail="Agent not found")
41+
42+
content = get_agent_content(agent.filename)
43+
if not content:
44+
raise HTTPException(status_code=500, detail="Failed to read agent file")
45+
46+
return {
47+
"filename": agent.filename,
48+
"content": content,
49+
"path": f".claude/agents/{agent.filename}"
50+
}
51+
52+
@router.post("/mcp-config/{mcp_name}")
53+
async def get_mcp_config_endpoint(mcp_name: str, current_config: Dict[str, Any] = Body(default={})):
54+
"""Get updated MCP config for virtual workspace"""
55+
mcps = actions_loader.get_mcps()
56+
mcp = next((m for m in mcps if m.name == mcp_name), None)
57+
58+
if not mcp:
59+
raise HTTPException(status_code=404, detail="MCP not found")
60+
61+
updated_config, was_removed = create_mcp_config(current_config, mcp.name, mcp.config)
62+
63+
return {
64+
"filename": ".mcp.json",
65+
"content": updated_config,
66+
"path": ".mcp.json",
67+
"was_removed": was_removed
68+
}

app/routes/install.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from fastapi.templating import Jinja2Templates
44
from pydantic import BaseModel
55
import hashlib
6-
from typing import Dict
6+
import re
7+
from typing import Dict, Set
78
from datetime import datetime
89

910
router = APIRouter()
@@ -15,6 +16,15 @@
1516
class InstallCreate(BaseModel):
1617
files: Dict[str, str]
1718

19+
def extract_env_vars_from_files(files: Dict[str, str]) -> Set[str]:
20+
"""Extract environment variables from file contents"""
21+
env_vars = set()
22+
for content in files.values():
23+
# Find ${VAR_NAME} patterns
24+
matches = re.findall(r'\$\{([^}]+)\}', content)
25+
env_vars.update(matches)
26+
return env_vars
27+
1828
@router.post("/api/install")
1929
async def create_install(request: Request, install: InstallCreate):
2030
"""Generate install script from files and store by hash"""
@@ -26,11 +36,15 @@ async def create_install(request: Request, install: InstallCreate):
2636
for i in range(1, len(parts)):
2737
directories.add('/'.join(parts[:i]))
2838

39+
# Extract environment variables from all files
40+
env_vars = extract_env_vars_from_files(install.files)
41+
2942
# Generate script using Jinja2 template
3043
script_content = templates.get_template("install.sh.j2").render(
3144
timestamp=datetime.now().isoformat(),
3245
files=install.files,
33-
directories=sorted(directories)
46+
directories=sorted(directories),
47+
env_vars=sorted(env_vars) if env_vars else None
3448
)
3549

3650
# Hash the script content

app/services/mcp_installer.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import json
2+
import re
3+
from pathlib import Path
4+
from typing import Dict, Any, Set, Tuple
5+
6+
def get_agent_content(agent_filename: str) -> str:
7+
"""Get agent content from actions/agents directory"""
8+
source_path = Path(__file__).parent.parent / "actions" / "agents" / agent_filename
9+
if source_path.exists():
10+
with open(source_path, 'r') as f:
11+
return f.read()
12+
return ""
13+
14+
def get_current_mcp_config() -> Dict[str, Any]:
15+
"""Get current .mcp.json config from virtual workspace or create new"""
16+
# This would be called from frontend with workspace content
17+
# For now, return default structure
18+
return {"mcpServers": {}}
19+
20+
def create_mcp_config(existing_config: Dict[str, Any], mcp_name: str, mcp_config: Dict[str, Any]) -> Tuple[str, bool]:
21+
"""Create updated .mcp.json content, returns (content, was_removed)"""
22+
if not isinstance(existing_config, dict) or "mcpServers" not in existing_config:
23+
config = {"mcpServers": {}}
24+
else:
25+
config = existing_config.copy()
26+
27+
# Toggle behavior: if exists, remove it; if not, add it
28+
was_removed = False
29+
if mcp_name in config["mcpServers"]:
30+
del config["mcpServers"][mcp_name]
31+
was_removed = True
32+
else:
33+
config["mcpServers"][mcp_name] = mcp_config
34+
35+
return json.dumps(config, indent=2), was_removed
36+
37+
def extract_env_vars_from_config(config: Dict[str, Any]) -> Set[str]:
38+
"""Extract environment variable names from MCP config"""
39+
env_vars = set()
40+
41+
def find_env_vars(obj):
42+
if isinstance(obj, str):
43+
matches = re.findall(r'\$\{([^}]+)\}', obj)
44+
env_vars.update(matches)
45+
elif isinstance(obj, dict):
46+
for value in obj.values():
47+
find_env_vars(value)
48+
elif isinstance(obj, list):
49+
for item in obj:
50+
find_env_vars(item)
51+
52+
find_env_vars(config)
53+
return env_vars

app/static/js/auto_share.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,13 @@ class AutoShareManager {
145145
collectWorkspaceData() {
146146
const allFiles = {};
147147

148-
// Helper function to collect files from tree structure
148+
// Get files directly from workspace manager state
149+
const state = window.workspaceManager?.getState();
150+
if (state?.files) {
151+
return { ...state.files };
152+
}
153+
154+
// Fallback: collect files from tree structure
149155
function collectFilesFromTree(nodes, collected) {
150156
nodes.forEach(node => {
151157
if (node.type === 'file') {
@@ -159,7 +165,7 @@ class AutoShareManager {
159165
});
160166
}
161167

162-
// Collect files from dynamic tree
168+
// Collect files from dynamic tree if available
163169
if (window.generateFileTreeData) {
164170
const fileTreeData = window.generateFileTreeData();
165171
collectFilesFromTree(fileTreeData, allFiles);

app/templates/components/quick_actions.html

Lines changed: 149 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -206,27 +206,63 @@ <h3 class="text-sm font-black text-black mb-2 flex items-center gap-1">
206206
createNewContextFromScratch();
207207
}
208208

209-
// Create a new context and apply a template to it
210-
function createNewContextAndApplyTemplate(templateName) {
209+
210+
// Create a new empty context from scratch
211+
function createNewContextFromScratch() {
212+
if (!window.workspaceManager) {
213+
console.error('WorkspaceManager not available');
214+
return;
215+
}
216+
217+
// Generate unique context ID
218+
const timestamp = Date.now();
219+
const contextId = `scratch-${timestamp}`;
220+
const contextName = `From Scratch - ${new Date().toLocaleString()}`;
221+
222+
// Create new context
223+
if (workspaceManager.createContext(contextId, contextName)) {
224+
// Switch to new context (it will be empty by default)
225+
workspaceManager.switchContext(contextId);
226+
227+
// Show workspace
228+
showWorkspace();
229+
} else {
230+
console.error('Failed to create new context');
231+
}
232+
}
233+
234+
// Action functions that use backend data
235+
function addAgent(name) {
236+
createNewContextAndInstallAgent(name);
237+
}
238+
239+
function addRule(name) {
240+
createNewContextAndInsertRule(name);
241+
}
242+
243+
function addMCP(name) {
244+
createNewContextAndInstallMCP(name);
245+
}
246+
247+
// New context creation functions that use the same API as workspace
248+
function createNewContextAndInstallAgent(agentName) {
211249
if (!window.workspaceManager) {
212250
console.error('WorkspaceManager not available');
213251
return;
214252
}
215253

216254
// Generate unique context ID
217255
const timestamp = Date.now();
218-
const contextId = `context-${timestamp}`;
219-
const contextName = `${templateName} - ${new Date().toLocaleString()}`;
256+
const contextId = `agent-${timestamp}`;
257+
const contextName = `${agentName} - ${new Date().toLocaleString()}`;
220258

221259
// Create new context
222260
if (workspaceManager.createContext(contextId, contextName)) {
223261
// Switch to new context
224262
workspaceManager.switchContext(contextId);
225263

226-
// Apply template to new context
227-
if (window.includeTemplate) {
228-
window.includeTemplate(templateName);
229-
}
264+
// Install agent using the same function as workspace sidebar
265+
installAgentInContext(agentName);
230266

231267
// Show workspace
232268
showWorkspace();
@@ -235,41 +271,134 @@ <h3 class="text-sm font-black text-black mb-2 flex items-center gap-1">
235271
}
236272
}
237273

238-
// Create a new empty context from scratch
239-
function createNewContextFromScratch() {
274+
function createNewContextAndInstallMCP(mcpName) {
240275
if (!window.workspaceManager) {
241276
console.error('WorkspaceManager not available');
242277
return;
243278
}
244279

245280
// Generate unique context ID
246281
const timestamp = Date.now();
247-
const contextId = `scratch-${timestamp}`;
248-
const contextName = `From Scratch - ${new Date().toLocaleString()}`;
282+
const contextId = `mcp-${timestamp}`;
283+
const contextName = `${mcpName} - ${new Date().toLocaleString()}`;
249284

250285
// Create new context
251286
if (workspaceManager.createContext(contextId, contextName)) {
252-
// Switch to new context (it will be empty by default)
287+
// Switch to new context
253288
workspaceManager.switchContext(contextId);
254289

290+
// Install MCP using the same function as workspace sidebar
291+
installMCPInContext(mcpName);
292+
255293
// Show workspace
256294
showWorkspace();
257295
} else {
258296
console.error('Failed to create new context');
259297
}
260298
}
261299

262-
// Action functions that use backend data
263-
function addAgent(name) {
264-
createNewContextAndApplyTemplate(name);
300+
function createNewContextAndInsertRule(ruleName) {
301+
if (!window.workspaceManager) {
302+
console.error('WorkspaceManager not available');
303+
return;
304+
}
305+
306+
// Generate unique context ID
307+
const timestamp = Date.now();
308+
const contextId = `rule-${timestamp}`;
309+
const contextName = `${ruleName} - ${new Date().toLocaleString()}`;
310+
311+
// Create new context
312+
if (workspaceManager.createContext(contextId, contextName)) {
313+
// Switch to new context
314+
workspaceManager.switchContext(contextId);
315+
316+
// Insert rule text directly into editor
317+
insertRuleInContext(ruleName);
318+
319+
// Show workspace
320+
showWorkspace();
321+
} else {
322+
console.error('Failed to create new context');
323+
}
265324
}
266325

267-
function addRule(name) {
268-
createNewContextAndApplyTemplate(name);
326+
// Helper functions that use the same API as workspace sidebar
327+
async function installAgentInContext(agentName) {
328+
try {
329+
const response = await fetch(`/api/actions/agent-content/${encodeURIComponent(agentName)}`);
330+
331+
if (response.ok) {
332+
const result = await response.json();
333+
334+
// Add to virtual workspace
335+
if (window.workspaceManager && window.workspaceManager.currentState) {
336+
window.workspaceManager.currentState.addFile(result.path, result.content);
337+
window.workspaceManager.saveState(window.workspaceManager.currentContextId);
338+
window.workspaceManager.render();
339+
}
340+
} else {
341+
const error = await response.json();
342+
console.error(`Error: ${error.detail}`);
343+
}
344+
} catch (error) {
345+
console.error(`Error installing agent: ${error.message}`);
346+
}
269347
}
270348

271-
function addMCP(name) {
272-
createNewContextAndApplyTemplate(name + ' MCP');
349+
async function installMCPInContext(mcpName) {
350+
try {
351+
// Get current .mcp.json content from workspace
352+
let currentConfig = {};
353+
if (window.workspaceManager && window.workspaceManager.getState()) {
354+
const mcpFile = window.workspaceManager.getState().files['.mcp.json'];
355+
if (mcpFile) {
356+
try {
357+
currentConfig = JSON.parse(mcpFile);
358+
} catch (e) {
359+
currentConfig = {};
360+
}
361+
}
362+
}
363+
364+
const response = await fetch(`/api/actions/mcp-config/${encodeURIComponent(mcpName)}`, {
365+
method: 'POST',
366+
headers: {
367+
'Content-Type': 'application/json'
368+
},
369+
body: JSON.stringify(currentConfig)
370+
});
371+
372+
if (response.ok) {
373+
const result = await response.json();
374+
375+
// Add to virtual workspace
376+
if (window.workspaceManager && window.workspaceManager.currentState) {
377+
window.workspaceManager.currentState.addFile(result.path, result.content);
378+
window.workspaceManager.saveState(window.workspaceManager.currentContextId);
379+
window.workspaceManager.render();
380+
}
381+
} else {
382+
const error = await response.json();
383+
console.error(`Error: ${error.detail}`);
384+
}
385+
} catch (error) {
386+
console.error(`Error installing MCP: ${error.message}`);
387+
}
388+
}
389+
390+
function insertRuleInContext(ruleName) {
391+
// For rules, we just insert the text directly into the editor
392+
// This matches the workspace sidebar behavior for rules
393+
setTimeout(() => {
394+
if (window.insertTextAtCursor) {
395+
window.insertTextAtCursor(ruleName);
396+
} else if (window.workspaceMonacoEditor) {
397+
// Fallback: set editor value directly
398+
window.workspaceMonacoEditor.setValue(ruleName);
399+
window.workspaceMonacoEditor.focus();
400+
}
401+
}, 100);
273402
}
274403

275404
// Show workspace with fade transition

0 commit comments

Comments
 (0)