Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/memory/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,14 @@ Services that run continuously in the backend process:
| GET | `/api/nevermined/settlement-failures` | Admin | Failed settlements |
| POST | `/api/nevermined/retry-settlement/{log_id}` | Admin | Retry settlement |

### Platform Settings (3 endpoints)

| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/settings/mcp-url` | Get configured MCP server URL (any auth user) |
| PUT | `/api/settings/mcp-url` | Set MCP server URL (admin-only) |
| DELETE | `/api/settings/mcp-url` | Reset to auto-detect (admin-only) |

---

## Architectural Invariants
Expand Down
1 change: 1 addition & 0 deletions docs/memory/feature-flows.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
| 2026-03-26 | EVT-001 | Agent Event Subscriptions — lightweight pub/sub for inter-agent pipelines | [agent-event-subscriptions.md](feature-flows/agent-event-subscriptions.md) |
| 2026-03-25 | #129 | Active watchdog — reconcile DB state against agent process registries, recover orphans, auto-terminate timeouts | [cleanup-service.md](feature-flows/cleanup-service.md) |
| 2026-03-25 | #148 | Fix silent subscription registration failure — encryption key auto-generation, status endpoint, frontend warning | [subscription-management.md](feature-flows/subscription-management.md) |
| 2026-03-25 | #76 | Configurable MCP Server URL in Admin Settings | [platform-settings.md](feature-flows/platform-settings.md), [api-keys-page.md](feature-flows/api-keys-page.md) |
| 2026-03-25 | #74 | Auto-assign subscription to new agents (round-robin, rate-limit aware) | [subscription-management.md](feature-flows/subscription-management.md) |
| 2026-03-23 | VOICE-001 | Voice Chat — real-time voice conversations with agents via Gemini Live API | [voice-chat.md](feature-flows/voice-chat.md) |
| 2026-03-23 | SLACK-002 | Channel adapter abstraction + multi-agent Slack routing | [slack-channel-routing.md](feature-flows/slack-channel-routing.md) |
Expand Down
7 changes: 4 additions & 3 deletions docs/memory/feature-flows/api-keys-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,15 @@ As a Trinity user, I want to manage my MCP API keys from a dedicated page so tha

### Lifecycle (onMounted)
```javascript
// ApiKeys.vue:553-558
// ApiKeys.vue
onMounted(async () => {
await fetchUserRole() // Check if user is admin
await fetchApiKeys() // Load existing keys
await Promise.all([fetchMcpUrl(), fetchUserRole(), fetchApiKeys()])
await ensureDefaultKey() // Auto-create default key for first-time users
})
```

**MCP URL**: `fetchMcpUrl()` calls `GET /api/settings/mcp-url` to get the admin-configured URL. If set, `mcpServerUrl` computed uses it; otherwise falls back to auto-detect from `window.location.hostname`. (#76)

### Step 1: Fetch User Role
```javascript
// ApiKeys.vue:376-390
Expand Down
7 changes: 6 additions & 1 deletion docs/memory/feature-flows/platform-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Overview

Admin-only page for managing system-wide configuration including API keys (Anthropic, GitHub), Trinity Prompt, email whitelist, SSH access toggle, ops configuration settings, GitHub template configuration (TMPL-001), and default avatar generation (AVATAR-003).
Admin-only page for managing system-wide configuration including API keys (Anthropic, GitHub), Trinity Prompt, email whitelist, SSH access toggle, ops configuration settings, GitHub template configuration (TMPL-001), MCP Server URL (#76), and default avatar generation (AVATAR-003).

## User Stories

Expand All @@ -14,6 +14,7 @@ Admin-only page for managing system-wide configuration including API keys (Anthr
| SET-011 | As an admin, I want to update ops settings so that I can tune context warnings, cost limits, and other thresholds | Implemented |
| SET-012 | As an admin, I want to reset ops settings to defaults so that I can restore standard configuration | Implemented |
| AVATAR-003 | As an admin, I want to generate default avatars for all agents so that agents without custom avatars get AI-generated ones | Implemented |
| MCP-URL-001 | As an admin, I want to configure the external MCP server URL so that API Keys page shows the correct URL for production deployments | Implemented |

## Entry Points

Expand All @@ -30,6 +31,9 @@ Admin-only page for managing system-wide configuration including API keys (Anthr
- `GET /api/settings/github-templates` - Get GitHub templates config (TMPL-001)
- `PUT /api/settings/github-templates` - Set GitHub templates (TMPL-001)
- `DELETE /api/settings/github-templates` - Reset templates to defaults (TMPL-001)
- `GET /api/settings/mcp-url` - Get MCP URL config (any auth user) (#76)
- `PUT /api/settings/mcp-url` - Set custom MCP URL (admin-only) (#76)
- `DELETE /api/settings/mcp-url` - Reset MCP URL to auto-detect (admin-only) (#76)
- `POST /api/agents/avatars/generate-defaults` - Generate avatars for agents without one (AVATAR-003, see [agent-avatars.md](agent-avatars.md))

---
Expand All @@ -47,6 +51,7 @@ Admin-only page for managing system-wide configuration including API keys (Anthr
| Trinity Prompt | 224-289 | Textarea for custom agent instructions |
| Email Whitelist | 291-390 | Table of whitelisted emails with add/remove |
| SSH Access Toggle | 392-430 | Toggle switch for enabling SSH access |
| MCP Server URL | 1089-1149 | Configure external MCP URL with custom/auto-detect badge (#76) |
| Default Avatars | 1054-1092 | Generate AI avatars for agents without custom ones (AVATAR-003) |

**Key Reactive State**
Expand Down
8 changes: 8 additions & 0 deletions docs/memory/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,14 @@ Trinity is autonomous agent orchestration and infrastructure — sovereign infra
- **Key Features**: `list_recent_executions`, `get_execution_result`, `get_agent_activity_summary`; enables async polling pattern for agent-to-agent collaboration beyond 60s MCP timeout
- **Spec**: `docs/requirements/MCP_EXECUTION_QUERY_TOOLS.md`

### 7.4 Configurable MCP Server URL (MCP-URL-001)
- **Status**: ✅ Implemented (2026-03-25)
- **Requirement ID**: MCP-URL-001
- **GitHub Issue**: #76
- **Description**: Admin-configurable MCP server URL displayed on the API Keys page connection snippets. Replaces hardcoded `http://{hostname}:8080/mcp` which is wrong for production deployments where MCP is proxied through nginx.
- **Key Features**: `GET/PUT/DELETE /api/settings/mcp-url` endpoints, URL validation (requires `http(s)://` and `/mcp` suffix), Settings UI section with save/reset, auto-detect fallback when not configured
- **Flow**: `docs/memory/feature-flows/platform-settings.md`

---

## 8. Infrastructure
Expand Down
109 changes: 109 additions & 0 deletions src/backend/routers/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import re
import httpx
from typing import List, Dict, Any, Optional
from urllib.parse import urlparse
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel

Expand Down Expand Up @@ -968,6 +969,114 @@ async def delete_github_templates(
}


# ============================================================================
# MCP Server URL Configuration (#76)
# ============================================================================

MCP_URL_SETTING_KEY = "mcp_external_url"


class McpUrlUpdate(BaseModel):
"""Request body for updating the MCP server URL."""
url: str


def _get_default_mcp_url(request: Request) -> str:
"""Compute the auto-detected MCP URL from the request hostname."""
host = request.headers.get("host", "localhost:8080")
hostname = host.split(":")[0]
if hostname in ("localhost", "127.0.0.1"):
return "http://localhost:8080/mcp"
return f"http://{hostname}:8080/mcp"


def _validate_mcp_url(url: str) -> str:
"""Validate and normalize MCP URL. Returns normalized URL or raises HTTPException."""
url = url.strip().rstrip("/")

parsed = urlparse(url)
if parsed.scheme not in ("http", "https"):
raise HTTPException(
status_code=422,
detail="URL must start with http:// or https://"
)
if not parsed.netloc:
raise HTTPException(
status_code=422,
detail="Invalid URL format"
)
if not parsed.path.endswith("/mcp"):
raise HTTPException(
status_code=422,
detail="URL must end with /mcp"
)

return url


@router.get("/mcp-url")
async def get_mcp_url(
request: Request,
current_user: User = Depends(get_current_user)
):
"""
Get the configured MCP server URL.

Any authenticated user can read this (used by API Keys page).
Returns both the stored custom URL (if any) and the auto-detected default.
"""
stored_url = db.get_setting_value(MCP_URL_SETTING_KEY)
default_url = _get_default_mcp_url(request)

return {
"url": stored_url,
"default_url": default_url
}


@router.put("/mcp-url")
async def update_mcp_url(
body: McpUrlUpdate,
request: Request,
current_user: User = Depends(get_current_user)
):
"""
Set a custom MCP server URL.

Admin-only. Validates URL format (must be http/https, must end with /mcp).
"""
require_admin(current_user)

validated_url = _validate_mcp_url(body.url)
db.set_setting(MCP_URL_SETTING_KEY, validated_url)

return {
"success": True,
"url": validated_url
}


@router.delete("/mcp-url")
async def delete_mcp_url(
request: Request,
current_user: User = Depends(get_current_user)
):
"""
Reset MCP server URL to auto-detect.

Admin-only. Removes the custom URL, reverting to hostname-based auto-detection.
"""
require_admin(current_user)

deleted = db.delete_setting(MCP_URL_SETTING_KEY)

return {
"success": True,
"deleted": deleted,
"message": "MCP server URL reset to auto-detect"
}


# ============================================================================
# Generic Settings CRUD - /{key} catch-all routes
# NOTE: These must come AFTER specific routes like /api-keys
Expand Down
20 changes: 17 additions & 3 deletions src/frontend/src/views/ApiKeys.vue
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@

<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import axios from 'axios'
import NavBar from '../components/NavBar.vue'
import ConfirmDialog from '../components/ConfirmDialog.vue'
import { useAuthStore } from '../stores/auth'
Expand Down Expand Up @@ -341,15 +342,29 @@ const confirmDialog = reactive({
onConfirm: () => {}
})

// MCP server URL: use admin-configured URL if set, otherwise auto-detect from hostname
const configuredMcpUrl = ref(null)

const mcpServerUrl = computed(() => {
if (configuredMcpUrl.value) {
return configuredMcpUrl.value
}
const host = window.location.hostname
if (host === 'localhost' || host === '127.0.0.1') {
return 'http://localhost:8080/mcp'
}
// Production: MCP server runs on port 8080 (not proxied through nginx)
return `http://${host}:8080/mcp`
})

const fetchMcpUrl = async () => {
try {
const response = await axios.get('/api/settings/mcp-url')
configuredMcpUrl.value = response.data.url || null
} catch (error) {
console.error('Failed to fetch MCP URL setting:', error)
}
}

// Filter out agent-scoped keys for non-admin users
const displayedKeys = computed(() => {
if (isAdmin.value) {
Expand Down Expand Up @@ -551,8 +566,7 @@ const formatDate = (dateString) => {
}

onMounted(async () => {
await fetchUserRole()
await fetchApiKeys()
await Promise.all([fetchMcpUrl(), fetchUserRole(), fetchApiKeys()])
// After loading keys, ensure user has a default key (for first-time users)
await ensureDefaultKey()
})
Expand Down
Loading