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
17 changes: 17 additions & 0 deletions cmd/mcpproxy/doctor_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,23 @@ func displaySecurityFeaturesStatus() {
return
}

// Routing Mode status (Spec 031)
routingMode := cfg.RoutingMode
if routingMode == "" {
routingMode = config.RoutingModeRetrieveTools
}
fmt.Printf(" Routing Mode: %s\n", routingMode)
switch routingMode {
case config.RoutingModeDirect:
fmt.Println(" All upstream tools exposed directly via /mcp endpoint")
case config.RoutingModeCodeExecution:
fmt.Println(" JS orchestration via code_execution tool")
default:
fmt.Println(" BM25 search via retrieve_tools + call_tool variants")
}
fmt.Printf(" Endpoints: /mcp/all (direct), /mcp/code (code_execution), /mcp/call (retrieve_tools)\n")
fmt.Println()

// Sensitive Data Detection status
sddConfig := cfg.SensitiveDataDetection
if sddConfig == nil || sddConfig.IsEnabled() {
Expand Down
36 changes: 25 additions & 11 deletions cmd/mcpproxy/status_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type StatusInfo struct {
UptimeSeconds float64 `json:"uptime_seconds,omitempty"`
APIKey string `json:"api_key"`
WebUIURL string `json:"web_ui_url"`
RoutingMode string `json:"routing_mode"`
Servers *ServerCounts `json:"servers,omitempty"`
SocketPath string `json:"socket_path,omitempty"`
ConfigPath string `json:"config_path,omitempty"`
Expand Down Expand Up @@ -158,11 +159,17 @@ func collectStatusFromDaemon(cfg *config.Config, socketPath, configPath string)
defer cancel()

info := &StatusInfo{
State: "Running",
Edition: Edition,
APIKey: cfg.APIKey,
SocketPath: socketPath,
ConfigPath: configPath,
State: "Running",
Edition: Edition,
APIKey: cfg.APIKey,
RoutingMode: cfg.RoutingMode,
SocketPath: socketPath,
ConfigPath: configPath,
}

// Apply routing mode default if empty
if info.RoutingMode == "" {
info.RoutingMode = config.RoutingModeRetrieveTools
}

// Add teams info if available
Expand Down Expand Up @@ -220,13 +227,19 @@ func collectStatusFromConfig(cfg *config.Config, socketPath, configPath string)
listenAddr = "127.0.0.1:8080"
}

routingMode := cfg.RoutingMode
if routingMode == "" {
routingMode = config.RoutingModeRetrieveTools
}

info := &StatusInfo{
State: "Not running",
Edition: Edition,
ListenAddr: listenAddr + " (configured)",
APIKey: cfg.APIKey,
WebUIURL: statusBuildWebUIURL(listenAddr, cfg.APIKey),
ConfigPath: configPath,
State: "Not running",
Edition: Edition,
ListenAddr: listenAddr + " (configured)",
APIKey: cfg.APIKey,
WebUIURL: statusBuildWebUIURL(listenAddr, cfg.APIKey),
RoutingMode: routingMode,
ConfigPath: configPath,
}

info.TeamsInfo = collectTeamsInfo(cfg)
Expand Down Expand Up @@ -351,6 +364,7 @@ func printStatusTable(info *StatusInfo) {
}

fmt.Printf(" %-12s %s\n", "API Key:", info.APIKey)
fmt.Printf(" %-12s %s\n", "Routing:", info.RoutingMode)
fmt.Printf(" %-12s %s\n", "Web UI:", info.WebUIURL)

if info.Servers != nil {
Expand Down
123 changes: 123 additions & 0 deletions cmd/mcpproxy/status_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,129 @@ func TestStatusJSONOutput(t *testing.T) {
}
}

func TestStatusRoutingModeInTable(t *testing.T) {
tests := []struct {
name string
routingMode string
expected string
}{
{
name: "retrieve_tools mode",
routingMode: "retrieve_tools",
expected: "retrieve_tools",
},
{
name: "direct mode",
routingMode: "direct",
expected: "direct",
},
{
name: "code_execution mode",
routingMode: "code_execution",
expected: "code_execution",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
info := &StatusInfo{
State: "Running",
Edition: "personal",
ListenAddr: "127.0.0.1:8080",
APIKey: "a1b2****a1b2",
WebUIURL: "http://127.0.0.1:8080/ui/?apikey=test",
RoutingMode: tt.routingMode,
}

old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w

printStatusTable(info)

w.Close()
os.Stdout = old

buf := make([]byte, 4096)
n, _ := r.Read(buf)
output := string(buf[:n])

if !strings.Contains(output, "Routing:") {
t.Errorf("expected output to contain 'Routing:', output:\n%s", output)
}
if !strings.Contains(output, tt.expected) {
t.Errorf("expected output to contain %q, output:\n%s", tt.expected, output)
}
})
}
}

func TestStatusRoutingModeInJSON(t *testing.T) {
info := &StatusInfo{
State: "Running",
Edition: "personal",
ListenAddr: "127.0.0.1:8080",
APIKey: "testkey",
WebUIURL: "http://127.0.0.1:8080/ui/",
RoutingMode: "direct",
}

old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w

err := printStatusJSON(info)

w.Close()
os.Stdout = old

if err != nil {
t.Fatalf("printStatusJSON failed: %v", err)
}

buf := make([]byte, 8192)
n, _ := r.Read(buf)
output := string(buf[:n])

var result StatusInfo
if jsonErr := json.Unmarshal([]byte(output), &result); jsonErr != nil {
t.Fatalf("invalid JSON: %v\nOutput: %s", jsonErr, output)
}

if result.RoutingMode != "direct" {
t.Errorf("expected routing_mode 'direct', got %q", result.RoutingMode)
}
}

func TestCollectStatusFromConfigRoutingMode(t *testing.T) {
t.Run("uses config routing mode", func(t *testing.T) {
cfg := &config.Config{
Listen: "127.0.0.1:8080",
APIKey: "testkey",
RoutingMode: "direct",
}

info := collectStatusFromConfig(cfg, "/tmp/test.sock", "/tmp/config.json")

if info.RoutingMode != "direct" {
t.Errorf("expected routing mode 'direct', got %q", info.RoutingMode)
}
})

t.Run("defaults to retrieve_tools when empty", func(t *testing.T) {
cfg := &config.Config{
Listen: "127.0.0.1:8080",
APIKey: "testkey",
}

info := collectStatusFromConfig(cfg, "/tmp/test.sock", "/tmp/config.json")

if info.RoutingMode != config.RoutingModeRetrieveTools {
t.Errorf("expected routing mode %q, got %q", config.RoutingModeRetrieveTools, info.RoutingMode)
}
})
}

// parseTestDuration is a helper to parse duration strings for tests.
func parseTestDuration(s string) (time.Duration, error) {
return time.ParseDuration(s)
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ onMounted(async () => {

// Fetch version info
systemStore.fetchInfo()

// Fetch routing mode info
systemStore.fetchRouting()
})

onUnmounted(() => {
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/components/TopHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@
<span class="text-xs opacity-60">Tools</span>
</div>

<!-- Routing Mode -->
<div class="flex items-center space-x-2 px-3 py-2 bg-base-200 rounded-lg text-sm">
<span class="text-xs opacity-60">Mode:</span>
<span class="font-medium">{{ routingModeLabel }}</span>
</div>

<!-- Proxy Address with Copy -->
<div v-if="systemStore.listenAddr" class="flex items-center space-x-2 px-3 py-2 bg-base-200 rounded-lg">
<span class="text-xs font-medium opacity-60">Proxy:</span>
Expand Down Expand Up @@ -105,6 +111,18 @@ const authStore = useAuthStore()

const addServerLabel = computed(() => authStore.isTeamsEdition ? 'Add Personal Server' : 'Add Server')

const routingModeLabel = computed(() => {
const mode = systemStore.routingMode
switch (mode) {
case 'direct':
return 'Direct'
case 'code_execution':
return 'Code Exec'
default:
return 'Retrieve'
}
})

const searchQuery = ref('')
const copyTooltip = ref('Copy MCP address')
const showAddServerModal = ref(false)
Expand Down
11 changes: 8 additions & 3 deletions frontend/src/services/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { APIResponse, Server, Tool, ToolApproval, SearchResult, StatusUpdate, SecretRef, MigrationAnalysis, ConfigSecretsResponse, GetToolCallsResponse, GetToolCallDetailResponse, GetServerToolCallsResponse, GetConfigResponse, ValidateConfigResponse, ConfigApplyResult, ServerTokenMetrics, GetRegistriesResponse, SearchRegistryServersResponse, RepositoryServer, GetSessionsResponse, GetSessionDetailResponse, InfoResponse, ActivityListResponse, ActivityDetailResponse, ActivitySummaryResponse, ImportResponse, AgentTokenInfo, CreateAgentTokenRequest, CreateAgentTokenResponse } from '@/types'
import type { APIResponse, Server, Tool, ToolApproval, SearchResult, StatusUpdate, SecretRef, MigrationAnalysis, ConfigSecretsResponse, GetToolCallsResponse, GetToolCallDetailResponse, GetServerToolCallsResponse, GetConfigResponse, ValidateConfigResponse, ConfigApplyResult, ServerTokenMetrics, GetRegistriesResponse, SearchRegistryServersResponse, RepositoryServer, GetSessionsResponse, GetSessionDetailResponse, InfoResponse, ActivityListResponse, ActivityDetailResponse, ActivitySummaryResponse, ImportResponse, AgentTokenInfo, CreateAgentTokenRequest, CreateAgentTokenResponse, RoutingInfo } from '@/types'

// Event types for API service
export interface APIAuthEvent {
Expand Down Expand Up @@ -205,8 +205,13 @@ class APIService {
}

// Status endpoint
async getStatus(): Promise<APIResponse<{ edition: string; running: boolean }>> {
return this.request<{ edition: string; running: boolean }>('/api/v1/status')
async getStatus(): Promise<APIResponse<{ edition: string; running: boolean; routing_mode: string }>> {
return this.request<{ edition: string; running: boolean; routing_mode: string }>('/api/v1/status')
}

// Routing mode endpoint
async getRouting(): Promise<APIResponse<RoutingInfo>> {
return this.request<RoutingInfo>('/api/v1/routing')
}

// Server endpoints
Expand Down
22 changes: 20 additions & 2 deletions frontend/src/stores/system.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { StatusUpdate, Theme, Toast, InfoResponse } from '@/types'
import type { StatusUpdate, Theme, Toast, InfoResponse, RoutingInfo } from '@/types'
import api from '@/services/api'

export const useSystemStore = defineStore('system', () => {
Expand All @@ -11,6 +11,7 @@ export const useSystemStore = defineStore('system', () => {
const currentTheme = ref<string>('corporate')
const toasts = ref<Toast[]>([])
const info = ref<InfoResponse | null>(null)
const routing = ref<RoutingInfo | null>(null)

// Available themes
const themes: Theme[] = [
Expand Down Expand Up @@ -59,6 +60,9 @@ export const useSystemStore = defineStore('system', () => {
const updateAvailable = computed(() => info.value?.update?.available ?? false)
const latestVersion = computed(() => info.value?.update?.latest_version ?? '')

// Routing mode
const routingMode = computed(() => routing.value?.routing_mode ?? status.value?.routing_mode ?? 'retrieve_tools')

// Actions
function connectEventSource() {
if (eventSource.value) {
Expand Down Expand Up @@ -348,6 +352,17 @@ export const useSystemStore = defineStore('system', () => {
}
}

async function fetchRouting() {
try {
const response = await api.getRouting()
if (response.success && response.data) {
routing.value = response.data
}
} catch (error) {
console.error('Failed to fetch routing:', error)
}
}

// Initialize theme on store creation
loadTheme()

Expand All @@ -359,6 +374,7 @@ export const useSystemStore = defineStore('system', () => {
toasts,
themes,
info,
routing,

// Computed
isRunning,
Expand All @@ -368,6 +384,7 @@ export const useSystemStore = defineStore('system', () => {
version,
updateAvailable,
latestVersion,
routingMode,

// Actions
connectEventSource,
Expand All @@ -378,5 +395,6 @@ export const useSystemStore = defineStore('system', () => {
removeToast,
clearToasts,
fetchInfo,
fetchRouting,
}
})
})
14 changes: 14 additions & 0 deletions frontend/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export interface SearchResult {
export interface StatusUpdate {
running: boolean
listen_addr: string
routing_mode?: string
upstream_stats: {
connected_servers: number
total_servers: number
Expand All @@ -124,6 +125,19 @@ export interface StatusUpdate {
timestamp: number
}

// Routing mode types
export interface RoutingInfo {
routing_mode: string
description: string
endpoints: {
default: string
direct: string
code_execution: string
retrieve_tools: string
}
available_modes: string[]
}

// Dashboard stats
export interface DashboardStats {
servers: {
Expand Down
Loading
Loading