#84 - Projects support in MCP, A2A, API

deci committed Dec 10, 2025 at 16:05 UTC d1be7f25319ffae55d92d47a6c6042810b3403b1
7 files changed +434 -39
docs/connectivity.md
+93
@@ -25,6 +25,7 @@ Send messages to Agent Zero and receive responses. Supports text messages, file
25 * `message` (string, required): The message to send
26 * `attachments` (array, optional): Array of `{filename, base64}` objects
27 * `lifetime_hours` (number, optional): Chat lifetime in hours (default: 24)
28 +* `project` (string, optional): Project name to activate (only on first message)
29
30 **Headers:**
31 * `X-API-KEY` (required)
@@ -169,6 +170,63 @@ async function sendWithAttachment() {
170 sendWithAttachment();
171 ```
172
173 +#### Project Usage Example
174 +
175 +```javascript
176 +// Working with projects
177 +async function sendMessageWithProject() {
178 + try {
179 + // First message - activate project
180 + const response = await fetch('YOUR_AGENT_ZERO_URL/api_message', {
181 + method: 'POST',
182 + headers: {
183 + 'Content-Type': 'application/json',
184 + 'X-API-KEY': 'YOUR_API_KEY'
185 + },
186 + body: JSON.stringify({
187 + message: "Analyze the project structure",
188 + project: "my-web-app" // Activates this project
189 + })
190 + });
191 +
192 + const data = await response.json();
193 +
194 + if (response.ok) {
195 + console.log('✅ Project activated!');
196 + console.log('Context ID:', data.context_id);
197 + console.log('Response:', data.response);
198 +
199 + // Continue conversation - project already set
200 + const followUp = await fetch('YOUR_AGENT_ZERO_URL/api_message', {
201 + method: 'POST',
202 + headers: {
203 + 'Content-Type': 'application/json',
204 + 'X-API-KEY': 'YOUR_API_KEY'
205 + },
206 + body: JSON.stringify({
207 + context_id: data.context_id,
208 + message: "What files are in the project?"
209 + // Do NOT include project field here - already set on first message
210 + })
211 + });
212 +
213 + const followUpData = await followUp.json();
214 + console.log('Follow-up response:', followUpData.response);
215 + return followUpData;
216 + } else {
217 + console.error('❌ Error:', data.error);
218 + return null;
219 + }
220 + } catch (error) {
221 + console.error('❌ Request failed:', error);
222 + return null;
223 + }
224 +}
225 +
226 +// Call the function
227 +sendMessageWithProject();
228 +```
229 +
230 ---
231
232 ## `GET/POST /api_log_get`
@@ -568,6 +626,30 @@ Below is an example of a `mcp.json` configuration file that a client could use t
626 }
627 ```
628
629 +### Project Support in MCP
630 +
631 +You can specify a project for MCP connections by including it in the URL path:
632 +
633 +```json
634 +{
635 + "mcpServers": {
636 + "agent-zero-with-project": {
637 + "type": "sse",
638 + "url": "YOUR_AGENT_ZERO_URL/mcp/t-YOUR_API_TOKEN/p-my-project-name/sse"
639 + },
640 + "agent-zero-http-with-project": {
641 + "type": "streamable-http",
642 + "url": "YOUR_AGENT_ZERO_URL/mcp/t-YOUR_API_TOKEN/p-my-project-name/http/"
643 + }
644 + }
645 +}
646 +```
647 +
648 +When a project is specified in the URL:
649 +- All new chats will be created within that project context
650 +- The agent will have access to project-specific instructions, knowledge, and file structure
651 +- Attempting to use an existing chat_id from a different project will result in an error
652 +
653 ---
654
655 ## A2A (Agent-to-Agent) Connectivity
@@ -583,3 +665,14 @@ To connect another agent to your Agent Zero instance, use the following URL form
665 ```
666 YOUR_AGENT_ZERO_URL/a2a/t-YOUR_API_TOKEN
667 ```
668 +
669 +To connect with a specific project active:
670 +
671 +```
672 +YOUR_AGENT_ZERO_URL/a2a/t-YOUR_API_TOKEN/p-PROJECT_NAME
673 +```
674 +
675 +When a project is specified:
676 +- All A2A conversations will run in the context of that project
677 +- The agent will have access to project-specific resources, instructions, and knowledge
678 +- This enables project-isolated agent-to-agent communication
python/api/api_message.py
+14 -1
@@ -3,7 +3,7 @@ import os
3 from datetime import datetime, timedelta
4 from agent import AgentContext, UserMessage, AgentContextType
5 from python.helpers.api import ApiHandler, Request, Response
6 -from python.helpers import files
6 +from python.helpers import files, projects
7 from python.helpers.print_style import PrintStyle
8 from werkzeug.utils import secure_filename
9 from initialize import initialize_agent
@@ -33,6 +33,7 @@ class ApiMessage(ApiHandler):
33 message = input.get("message", "")
34 attachments = input.get("attachments", [])
35 lifetime_hours = input.get("lifetime_hours", 24) # Default 24 hours
36 + project = input.get("project", None) # Optional project name
37
38 if not message:
39 return Response('{"error": "Message is required"}', status=400, mimetype="application/json")
@@ -71,12 +72,24 @@ class ApiMessage(ApiHandler):
72 context = AgentContext.use(context_id)
73 if not context:
74 return Response('{"error": "Context not found"}', status=404, mimetype="application/json")
75 +
76 + # Validation: if project is provided but context already has different project
77 + existing_project = context.get_data(projects.CONTEXT_DATA_KEY_PROJECT)
78 + if project and existing_project and existing_project != project:
79 + return Response('{"error": "Project can only be set on first message"}', status=400, mimetype="application/json")
80 else:
81 config = initialize_agent()
82 context = AgentContext(config=config, type=AgentContextType.USER)
83 AgentContext.use(context.id)
84 context_id = context.id
85
86 + # Activate project if provided
87 + if project:
88 + try:
89 + projects.activate_project(context_id, project)
90 + except Exception as e:
91 + return Response(f'{{"error": "Failed to activate project: {str(e)}"}}', status=400, mimetype="application/json")
92 +
93 # Update chat lifetime
94 with self._cleanup_lock:
95 self._chat_lifetimes[context_id] = datetime.now() + timedelta(hours=lifetime_hours)
python/helpers/fasta2a_server.py
+29 -1
@@ -5,8 +5,9 @@ import atexit
5 from typing import Any, List
6 import contextlib
7 import threading
8 +import contextvars
9
9 -from python.helpers import settings
10 +from python.helpers import settings, projects
11 from starlette.requests import Request
12
13 # Local imports
@@ -60,6 +61,9 @@ except ImportError: # pragma: no cover – library not installed
61
62 _PRINTER = PrintStyle(italic=True, font_color="purple", padding=False)
63
64 +# Context variable to store project name from URL
65 +_a2a_project_name: contextvars.ContextVar[str | None] = contextvars.ContextVar('a2a_project_name', default=None)
66 +
67
68 class AgentZeroWorker(Worker): # type: ignore[misc]
69 """Agent Zero implementation of FastA2A Worker."""
@@ -84,6 +88,16 @@ class AgentZeroWorker(Worker): # type: ignore[misc]
88 cfg = initialize_agent()
89 context = AgentContext(cfg, type=AgentContextType.BACKGROUND)
90
91 + # Activate project if specified in URL
92 + project_name = _a2a_project_name.get()
93 + if project_name:
94 + try:
95 + projects.activate_project(context.id, project_name)
96 + _PRINTER.print(f"[A2A] Activated project: {project_name}")
97 + except Exception as e:
98 + _PRINTER.print(f"[A2A] Failed to activate project: {e}")
99 + raise Exception(f"Failed to activate project: {str(e)}")
100 +
101 # Log user message so it appears instantly in UI chat window
102 context.log.log(
103 type="user", # type: ignore[arg-type]
@@ -424,6 +438,9 @@ class DynamicA2AProxy:
438 if path.startswith('/a2a'):
439 path = path[4:] # Remove '/a2a' prefix
440
441 + # Initialize project name
442 + project_name = None
443 +
444 # Check if path matches token pattern /t-{token}/
445 if path.startswith('/t-'):
446 # Extract token from path
@@ -431,6 +448,14 @@ class DynamicA2AProxy:
448 path_parts = path[3:].split('/', 1) # Remove '/t-' prefix
449 request_token = path_parts[0]
450 remaining_path = '/' + path_parts[1] if len(path_parts) > 1 else '/'
451 +
452 + # Check for project pattern /p-{project}/
453 + if remaining_path.startswith('/p-'):
454 + project_parts = remaining_path[3:].split('/', 1)
455 + if project_parts[0]:
456 + project_name = project_parts[0]
457 + remaining_path = '/' + project_parts[1] if len(project_parts) > 1 else '/'
458 + _PRINTER.print(f"[A2A] Extracted project from URL: {project_name}")
459 else:
460 request_token = path[3:]
461 remaining_path = '/'
@@ -452,6 +477,9 @@ class DynamicA2AProxy:
477 })
478 return
479
480 + # Set project name in context variable for use by worker
481 + _a2a_project_name.set(project_name)
482 +
483 # Update scope with cleaned path
484 scope = dict(scope)
485 scope['path'] = remaining_path
python/helpers/mcp_server.py
+127 -12
@@ -3,23 +3,31 @@ from typing import Annotated, Literal, Union
3 from urllib.parse import urlparse
4 from openai import BaseModel
5 from pydantic import Field
6 -from fastmcp import FastMCP
6 +from fastmcp import FastMCP # type: ignore
7 +import contextvars
8
9 from agent import AgentContext, AgentContextType, UserMessage
10 from python.helpers.persist_chat import remove_chat
11 from initialize import initialize_agent
12 from python.helpers.print_style import PrintStyle
12 -from python.helpers import settings
13 +from python.helpers import settings, projects
14 from starlette.middleware import Middleware
15 from starlette.middleware.base import BaseHTTPMiddleware
16 from starlette.exceptions import HTTPException as StarletteHTTPException
17 from starlette.types import ASGIApp, Receive, Scope, Send
17 -from fastmcp.server.http import create_sse_app
18 +from fastmcp.server.http import create_sse_app # type: ignore
19 from starlette.requests import Request
20 import threading
21
22 _PRINTER = PrintStyle(italic=True, font_color="green", padding=False)
23
24 +# Context variable to store project name from URL (per-request)
25 +_mcp_project_name: contextvars.ContextVar[str | None] = contextvars.ContextVar('mcp_project_name', default=None)
26 +
27 +# Session storage for project names (persists across SSE tool calls)
28 +# Key: connection identifier, Value: project name
29 +_mcp_project_sessions: dict[str, str | None] = {}
30 +_mcp_session_lock = threading.Lock()
31
32 mcp_server: FastMCP = FastMCP(
33 name="Agent Zero integrated MCP Server",
@@ -127,6 +135,25 @@ async def send_message(
135 description="The response from the remote Agent Zero Instance", title="response"
136 ),
137 ]:
138 + # Get project name from session storage (persists across SSE connection)
139 + # First try context variable (for HTTP requests), then session storage (for SSE tool calls)
140 + project_name = _mcp_project_name.get()
141 +
142 + # If not in context variable, try session storage using current token
143 + if not project_name:
144 + cfg = settings.get_settings()
145 + current_token = cfg.get("mcp_server_token")
146 + _PRINTER.print(f"[MCP] send_message - Looking for project. Token: '{current_token}' (type: {type(current_token).__name__})")
147 + if current_token:
148 + with _mcp_session_lock:
149 + _PRINTER.print(f"[MCP] Session storage keys: {list(_mcp_project_sessions.keys())}")
150 + _PRINTER.print(f"[MCP] Session storage: {_mcp_project_sessions}")
151 + project_name = _mcp_project_sessions.get(current_token)
152 + if project_name:
153 + _PRINTER.print(f"[MCP] Retrieved project from session: {project_name}")
154 + else:
155 + _PRINTER.print(f"[MCP] No project found in session for token: {current_token}")
156 +
157 context: AgentContext | None = None
158 if chat_id:
159 context = AgentContext.get(chat_id)
@@ -137,10 +164,27 @@ async def send_message(
164 # whether we should save the chat or delete it afterwards
165 # If we continue a conversation, it must be persistent
166 persistent_chat = True
167 +
168 + # Validation: if project is in URL but context has different project
169 + if project_name:
170 + existing_project = context.get_data(projects.CONTEXT_DATA_KEY_PROJECT)
171 + if existing_project and existing_project != project_name:
172 + return ToolError(
173 + error=f"Chat belongs to project '{existing_project}' but URL specifies '{project_name}'",
174 + chat_id=chat_id
175 + )
176 else:
177 config = initialize_agent()
178 context = AgentContext(config=config, type=AgentContextType.BACKGROUND)
179
180 + # Activate project if specified in URL
181 + if project_name:
182 + try:
183 + projects.activate_project(context.id, project_name)
184 + _PRINTER.print(f"[MCP] Activated project: {project_name}")
185 + except Exception as e:
186 + return ToolError(error=f"Failed to activate project: {str(e)}", chat_id="")
187 +
188 if not message:
189 return ToolError(
190 error="Message is required", chat_id=context.id if persistent_chat else ""
@@ -325,10 +369,10 @@ class DynamicMcpProxy:
369
370 def _create_custom_http_app(self, streamable_http_path, auth_server_provider, auth_settings, debug, routes):
371 """Create a custom HTTP app that manages the session manager manually."""
328 - from fastmcp.server.http import setup_auth_middleware_and_routes, create_base_app
329 - from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
372 + from fastmcp.server.http import setup_auth_middleware_and_routes, create_base_app # type: ignore
373 + from mcp.server.streamable_http_manager import StreamableHTTPSessionManager # type: ignore
374 from starlette.routing import Mount
331 - from mcp.server.auth.middleware.bearer_auth import RequireAuthMiddleware
375 + from mcp.server.auth.middleware.bearer_auth import RequireAuthMiddleware # type: ignore
376 import anyio
377
378 server_routes = []
@@ -408,12 +452,47 @@ class DynamicMcpProxy:
452 # Route based on path
453 path = scope.get("path", "")
454
411 - if f"/t-{self.token}/sse" in path or f"t-{self.token}/messages" in path:
412 - # Route to SSE app
413 - await sse_app(scope, receive, send)
414 - elif f"/t-{self.token}/http" in path:
415 - # Route to HTTP app
416 - await http_app(scope, receive, send)
455 + # Check for token in path (with or without project segment)
456 + # Patterns: /t-{token}/sse, /t-{token}/p-{project}/sse, etc.
457 + has_token = f"/t-{self.token}/" in path or f"t-{self.token}/" in path
458 +
459 + # Extract project from path BEFORE cleaning (for session storage)
460 + project_name = None
461 + if "/p-" in path:
462 + try:
463 + parts = path.split("/p-")
464 + if len(parts) > 1:
465 + project_part = parts[1].split("/")[0]
466 + if project_part:
467 + project_name = project_part
468 + _PRINTER.print(f"[MCP] Proxy extracted project from URL: {project_name}")
469 + except Exception as e:
470 + _PRINTER.print(f"[MCP] Failed to extract project in proxy: {e}")
471 +
472 + # Store project in session (persists across SSE connection)
473 + if self.token and project_name:
474 + with _mcp_session_lock:
475 + _mcp_project_sessions[self.token] = project_name
476 + _PRINTER.print(f"[MCP] Stored project '{project_name}' for token '{self.token}' (type: {type(self.token).__name__}) in proxy")
477 +
478 + # Strip project segment from path if present (e.g., /p-project_name/)
479 + # This is needed because the underlying MCP apps were configured without project paths
480 + cleaned_path = path
481 + if "/p-" in path:
482 + # Remove /p-{project}/ segment: /t-TOKEN/p-PROJECT/sse -> /t-TOKEN/sse
483 + import re
484 + cleaned_path = re.sub(r'/p-[^/]+/', '/', path)
485 +
486 + # Update scope with cleaned path for the underlying app
487 + modified_scope = dict(scope)
488 + modified_scope['path'] = cleaned_path
489 +
490 + if has_token and ("/sse" in path or "/messages" in path):
491 + # Route to SSE app with cleaned path
492 + await sse_app(modified_scope, receive, send)
493 + elif has_token and "/http" in path:
494 + # Route to HTTP app with cleaned path
495 + await http_app(modified_scope, receive, send)
496 else:
497 raise StarletteHTTPException(
498 status_code=403, detail="MCP forbidden"
@@ -430,4 +509,40 @@ async def mcp_middleware(request: Request, call_next):
509 status_code=403, detail="MCP server is disabled in settings."
510 )
511
512 + # Extract project from URL path if present (pattern: /mcp/t-{token}/p-{project}/...)
513 + path = request.url.path
514 + project_name = None
515 + token = None
516 +
517 + # Extract token from path
518 + if "/t-" in path:
519 + token_parts = path.split("/t-")
520 + if len(token_parts) > 1:
521 + token = token_parts[1].split("/")[0]
522 +
523 + # Extract project if present
524 + if "/p-" in path:
525 + try:
526 + parts = path.split("/p-")
527 + if len(parts) > 1:
528 + project_part = parts[1].split("/")[0]
529 + if project_part:
530 + project_name = project_part
531 + _PRINTER.print(f"[MCP] Extracted project from URL: {project_name}")
532 + except Exception as e:
533 + _PRINTER.print(f"[MCP] Failed to extract project from URL: {e}")
534 +
535 + # Debug logging
536 + _PRINTER.print(f"[MCP] Middleware - Path: {path}, Token: {token}, Project: {project_name}")
537 +
538 + # Store project in session dict ONLY if we found one (don't overwrite with None)
539 + # The proxy already handles project extraction before path cleaning
540 + if token and project_name:
541 + with _mcp_session_lock:
542 + _mcp_project_sessions[token] = project_name
543 + _PRINTER.print(f"[MCP] Middleware stored project '{project_name}' for token session")
544 +
545 + # Also set in context variable for backwards compatibility
546 + _mcp_project_name.set(project_name)
547 +
548 return await call_next(request)
webui/components/settings/a2a/a2a-connection.html
+49 -4
@@ -21,10 +21,21 @@
21 </div>
22
23 <h3>A2A Connection URL</h3>
24 +
25 + <!-- Project selector -->
26 + <div style="margin: 16px 0;">
27 + <label for="a2a-project-select" style="display: block; margin-bottom: 8px; color: var(--color-text-primary);">
28 + Select Project (optional):
29 + </label>
30 + <select id="a2a-project-select" style="width: 100%; padding: 8px; background-color: var(--color-bg-secondary); color: var(--color-text-primary); border: 1px solid var(--color-border); border-radius: 4px;">
31 + <option value="">No project</option>
32 + </select>
33 + </div>
34 +
35 <div id="a2a-connection-example"></div>
36
37 <script>
27 - setTimeout(() => {
38 + setTimeout(async () => {
39 const url = window.location.origin;
40 // Try to get a2a_token first, fallback to mcp_server_token
41 let tokenField = null;
@@ -33,15 +44,49 @@
44 tokenField = allFields.find(f => f.id === 'a2a_token') || allFields.find(f => f.id === 'mcp_server_token');
45 } catch (e) { }
46 const token = tokenField ? tokenField.value : '';
36 - const connectionUrl = `${url}/a2a/t-${token}`;
47
48 + // Fetch and populate projects
49 + const projectSelect = document.getElementById('a2a-project-select');
50 + try {
51 + const response = await fetch('/projects', {
52 + method: 'POST',
53 + headers: { 'Content-Type': 'application/json' },
54 + body: JSON.stringify({ action: 'list' })
55 + });
56 + const data = await response.json();
57 + if (data.ok && data.data) {
58 + data.data.forEach(project => {
59 + const option = document.createElement('option');
60 + option.value = project.name;
61 + option.textContent = project.title || project.name;
62 + projectSelect.appendChild(option);
63 + });
64 + }
65 + } catch (e) {
66 + console.error('Failed to load projects:', e);
67 + }
68 +
69 + // Function to update URL based on selected project
70 + function updateConnectionUrl() {
71 + const selectedProject = projectSelect.value;
72 + let connectionUrl = `${url}/a2a/t-${token}`;
73 + if (selectedProject) {
74 + connectionUrl += `/p-${selectedProject}`;
75 + }
76 + editor.setValue(connectionUrl);
77 + editor.clearSelection();
78 + }
79 +
80 + // Initialize editor
81 const editor = ace.edit("a2a-connection-example");
82 const dark = localStorage.getItem("darkMode");
83 editor.setTheme(dark !== "false" ? "ace/theme/github_dark" : "ace/theme/tomorrow");
84 editor.session.setMode("ace/mode/text");
42 - editor.setValue(connectionUrl);
43 - editor.clearSelection();
85 + updateConnectionUrl();
86 editor.setReadOnly(true);
87 +
88 + // Update URL when project selection changes
89 + projectSelect.addEventListener('change', updateConnectionUrl);
90 }, 0);
91 </script>
92 </div>
webui/components/settings/external/api-examples.html
+61 -1
@@ -34,7 +34,8 @@
34 • <code>context_id</code> (string, optional): Existing chat context ID<br>
35 • <code>message</code> (string, required): The message to send<br>
36 • <code>attachments</code> (array, optional): Array of {filename, base64} objects<br>
37 - • <code>lifetime_hours</code> (number, optional): Chat lifetime in hours (default: 24)
37 + • <code>lifetime_hours</code> (number, optional): Chat lifetime in hours (default: 24)<br>
38 + • <code>project</code> (string, optional): Project name to activate (only on first message)
39 </p>
40 <p style="margin: 0; color: var(--color-text-secondary); font-size: 14px;">
41 <strong>Headers:</strong> <code>X-API-KEY</code> (required), <code>Content-Type: application/json</code>
@@ -52,6 +53,9 @@
53
54 <h4>File Attachment Example</h4>
55 <div id="api-attachment-example"></div>
56 +
57 + <h4>Project Usage Example</h4>
58 + <div id="api-project-example"></div>
59 </div>
60
61 <!-- Section 2: api_log_get Endpoint -->
@@ -602,11 +606,66 @@ async function attachmentWorkflow() {
606 // Run the complete workflow
607 attachmentWorkflow();`;
608
609 + // Project usage example
610 + const projectExample = `// Working with projects
611 +async function sendMessageWithProject() {
612 + try {
613 + // First message - activate project
614 + const response = await fetch('${url}/api_message', {
615 + method: 'POST',
616 + headers: {
617 + 'Content-Type': 'application/json',
618 + 'X-API-KEY': '${token}'
619 + },
620 + body: JSON.stringify({
621 + message: "Analyze the project structure",
622 + project: "my-web-app" // Activates this project
623 + })
624 + });
625 +
626 + const data = await response.json();
627 +
628 + if (response.ok) {
629 + console.log('✅ Project activated!');
630 + console.log('Context ID:', data.context_id);
631 + console.log('Response:', data.response);
632 +
633 + // Continue conversation - project already set
634 + const followUp = await fetch('${url}/api_message', {
635 + method: 'POST',
636 + headers: {
637 + 'Content-Type': 'application/json',
638 + 'X-API-KEY': '${token}'
639 + },
640 + body: JSON.stringify({
641 + context_id: data.context_id,
642 + message: "What files are in the project?"
643 + // Do NOT include project field here - already set on first message
644 + })
645 + });
646 +
647 + const followUpData = await followUp.json();
648 + console.log('Follow-up response:', followUpData.response);
649 + return followUpData;
650 + } else {
651 + console.error('❌ Error:', data.error);
652 + return null;
653 + }
654 + } catch (error) {
655 + console.error('❌ Request failed:', error);
656 + return null;
657 + }
658 +}
659 +
660 +// Call the function
661 +sendMessageWithProject();`;
662 +
663 // Initialize ACE editors
664 const editors = [
665 { id: "api-basic-example", content: basicExample },
666 { id: "api-continuation-example", content: continuationExample },
667 { id: "api-attachment-example", content: attachmentExample },
668 + { id: "api-project-example", content: projectExample },
669 { id: "api-log-get-example", content: logGetExample },
670 { id: "api-log-post-example", content: logPostExample },
671 { id: "api-terminate-example", content: terminateExample },
@@ -635,6 +694,7 @@ attachmentWorkflow();`;
694 #api-basic-example,
695 #api-continuation-example,
696 #api-attachment-example,
697 + #api-project-example,
698 #api-log-get-example,
699 #api-log-post-example,
700 #api-terminate-example,
webui/components/settings/mcp/server/example.html
+61 -20
@@ -20,37 +20,78 @@
20 </div>
21
22 <h3>Example MCP Server Configuration JSON</h3>
23 +
24 + <!-- Project selector -->
25 + <div style="margin: 16px 0;">
26 + <label for="mcp-project-select" style="display: block; margin-bottom: 8px; color: var(--color-text-primary);">
27 + Select Project (optional):
28 + </label>
29 + <select id="mcp-project-select" style="width: 100%; padding: 8px; background-color: var(--color-bg-secondary); color: var(--color-text-primary); border: 1px solid var(--color-border); border-radius: 4px;">
30 + <option value="">No project</option>
31 + </select>
32 + </div>
33 +
34 <div id="mcp-server-example"></div>
35
36 <script>
26 - setTimeout(() => {
37 + setTimeout(async () => {
38 const url = window.location.origin;
39 const token = settingsModalProxy.settings.sections.filter(x => x.id == "mcp_server")[0].fields.filter(x => x.id == "mcp_server_token")[0].value;
29 - const jsonExample = JSON.stringify({
30 - "mcpServers":
31 - {
32 - "agent-zero": {
33 - "type": "sse",
34 - "url": `${url}/mcp/t-${token}/sse`
35 - },
36 - "agent-zero-http": {
37 - "type": "streamable-http",
38 - "url": `${url}/mcp/t-${token}/http`
39 - }
40 +
41 + // Fetch and populate projects
42 + const projectSelect = document.getElementById('mcp-project-select');
43 + try {
44 + const response = await fetch('/projects', {
45 + method: 'POST',
46 + headers: { 'Content-Type': 'application/json' },
47 + body: JSON.stringify({ action: 'list' })
48 + });
49 + const data = await response.json();
50 + if (data.ok && data.data) {
51 + data.data.forEach(project => {
52 + const option = document.createElement('option');
53 + option.value = project.name;
54 + option.textContent = project.title || project.name;
55 + projectSelect.appendChild(option);
56 + });
57 }
41 - }, null, 2);
58 + } catch (e) {
59 + console.error('Failed to load projects:', e);
60 + }
61 +
62 + // Function to update JSON based on selected project
63 + function updateMcpConfig() {
64 + const selectedProject = projectSelect.value;
65 + const projectPath = selectedProject ? `/p-${selectedProject}` : '';
66
67 + const jsonExample = JSON.stringify({
68 + "mcpServers":
69 + {
70 + "agent-zero": {
71 + "type": "sse",
72 + "url": `${url}/mcp/t-${token}${projectPath}/sse`
73 + },
74 + "agent-zero-http": {
75 + "type": "streamable-http",
76 + "url": `${url}/mcp/t-${token}${projectPath}/http`
77 + }
78 + }
79 + }, null, 2);
80 +
81 + editor.setValue(jsonExample);
82 + editor.clearSelection();
83 + }
84 +
85 + // Initialize editor
86 const editor = ace.edit("mcp-server-example");
87 const dark = localStorage.getItem("darkMode");
45 - if (dark != "false") {
46 - editor.setTheme("ace/theme/github_dark");
47 - } else {
48 - editor.setTheme("ace/theme/tomorrow");
49 - }
88 + editor.setTheme(dark !== "false" ? "ace/theme/github_dark" : "ace/theme/tomorrow");
89 editor.session.setMode("ace/mode/json");
51 - editor.setValue(jsonExample);
52 - editor.clearSelection();
90 + updateMcpConfig();
91 editor.setReadOnly(true);
92 +
93 + // Update JSON when project selection changes
94 + projectSelect.addEventListener('change', updateMcpConfig);
95 }, 0);
96 </script>
97 <!-- </template> -->