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
47 changes: 36 additions & 11 deletions cmd/mcpproxy/status_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,24 @@ import (

// StatusInfo holds the collected status data for display.
type StatusInfo struct {
State string `json:"state"`
ListenAddr string `json:"listen_addr"`
Uptime string `json:"uptime,omitempty"`
UptimeSeconds float64 `json:"uptime_seconds,omitempty"`
APIKey string `json:"api_key"`
WebUIURL string `json:"web_ui_url"`
Servers *ServerCounts `json:"servers,omitempty"`
SocketPath string `json:"socket_path,omitempty"`
ConfigPath string `json:"config_path,omitempty"`
Version string `json:"version,omitempty"`
State string `json:"state"`
Edition string `json:"edition"`
ListenAddr string `json:"listen_addr"`
Uptime string `json:"uptime,omitempty"`
UptimeSeconds float64 `json:"uptime_seconds,omitempty"`
APIKey string `json:"api_key"`
WebUIURL string `json:"web_ui_url"`
Servers *ServerCounts `json:"servers,omitempty"`
SocketPath string `json:"socket_path,omitempty"`
ConfigPath string `json:"config_path,omitempty"`
Version string `json:"version,omitempty"`
TeamsInfo *TeamsStatusInfo `json:"teams,omitempty"`
}

// TeamsStatusInfo holds teams-specific status information.
type TeamsStatusInfo struct {
OAuthProvider string `json:"oauth_provider"`
AdminEmails []string `json:"admin_emails"`
}

// ServerCounts holds upstream server statistics.
Expand Down Expand Up @@ -151,11 +159,15 @@ func collectStatusFromDaemon(cfg *config.Config, socketPath, configPath string)

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

// Add teams info if available
info.TeamsInfo = collectTeamsInfo(cfg)

// Get status data (running, listen_addr, upstream_stats)
statusData, err := client.GetStatus(ctx)
if err != nil {
Expand Down Expand Up @@ -208,13 +220,18 @@ func collectStatusFromConfig(cfg *config.Config, socketPath, configPath string)
listenAddr = "127.0.0.1:8080"
}

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

info.TeamsInfo = collectTeamsInfo(cfg)

return info
}

func extractServerCounts(stats map[string]interface{}) *ServerCounts {
Expand Down Expand Up @@ -321,6 +338,7 @@ func printStatusTable(info *StatusInfo) {
fmt.Println("MCPProxy Status")

fmt.Printf(" %-12s %s\n", "State:", info.State)
fmt.Printf(" %-12s %s\n", "Edition:", info.Edition)

if info.Version != "" {
fmt.Printf(" %-12s %s\n", "Version:", info.Version)
Expand All @@ -346,6 +364,13 @@ func printStatusTable(info *StatusInfo) {
if info.ConfigPath != "" {
fmt.Printf(" %-12s %s\n", "Config:", info.ConfigPath)
}

if info.TeamsInfo != nil {
fmt.Println()
fmt.Println("Teams")
fmt.Printf(" %-12s %s\n", "OAuth:", info.TeamsInfo.OAuthProvider)
fmt.Printf(" %-12s %s\n", "Admins:", strings.Join(info.TeamsInfo.AdminEmails, ", "))
}
}

func loadStatusConfig() (*config.Config, error) {
Expand Down
18 changes: 18 additions & 0 deletions cmd/mcpproxy/status_teams.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//go:build teams

package main

import "github.com/smart-mcp-proxy/mcpproxy-go/internal/config"

func collectTeamsInfo(cfg *config.Config) *TeamsStatusInfo {
if cfg.Teams == nil || !cfg.Teams.Enabled {
return nil
}
info := &TeamsStatusInfo{
AdminEmails: cfg.Teams.AdminEmails,
}
if cfg.Teams.OAuth != nil {
info.OAuthProvider = cfg.Teams.OAuth.Provider
}
return info
}
9 changes: 9 additions & 0 deletions cmd/mcpproxy/status_teams_stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//go:build !teams

package main

import "github.com/smart-mcp-proxy/mcpproxy-go/internal/config"

func collectTeamsInfo(_ *config.Config) *TeamsStatusInfo {
return nil
}
21 changes: 7 additions & 14 deletions cmd/mcpproxy/teams_register.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,10 @@

package main

import (
"log"

"github.com/smart-mcp-proxy/mcpproxy-go/internal/teams"
)

func init() {
// Initialize all registered teams features.
// Individual feature packages (auth, workspace, etc.) register
// themselves via their own init() functions.
if err := teams.SetupAll(teams.Dependencies{}); err != nil {
log.Fatalf("failed to initialize teams features: %v", err)
}
}
// Teams features are registered via init() functions in their respective
// packages. The actual setup happens when the server calls teams.SetupAll()
// during HTTP server initialization (see internal/server/teams_wire.go).
//
// This file imports the teams package for its init() side effects,
// which register feature modules in the teams registry.
import _ "github.com/smart-mcp-proxy/mcpproxy-go/internal/teams"
9 changes: 7 additions & 2 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ import ConnectionStatus from '@/components/ConnectionStatus.vue'
import AuthErrorModal from '@/components/AuthErrorModal.vue'
import { useSystemStore } from '@/stores/system'
import { useServersStore } from '@/stores/servers'
import { useAuthStore } from '@/stores/auth'
import api, { type APIAuthEvent } from '@/services/api'

const systemStore = useSystemStore()
const serversStore = useServersStore()
const authStore = useAuthStore()

// Authentication modal state
const authModal = reactive({
Expand Down Expand Up @@ -93,7 +95,10 @@ function handleAuthError(event: APIAuthEvent) {
authModal.show = true
}

onMounted(() => {
onMounted(async () => {
// Initialize auth state (needed for teams edition role-based nav)
await authStore.checkAuth()

// Set up API error listener
removeAPIListener = api.addEventListener(handleAuthError)

Expand Down Expand Up @@ -133,4 +138,4 @@ onUnmounted(() => {
opacity: 0;
transform: translateX(-10px);
}
</style>
</style>
135 changes: 120 additions & 15 deletions frontend/src/components/SidebarNav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,90 @@
<div class="px-6 py-5 border-b border-base-300">
<router-link to="/" class="flex items-center space-x-3">
<img src="/src/assets/logo.svg" alt="MCPProxy Logo" class="w-10 h-10" />
<span class="text-xl font-bold">MCPProxy</span>
<div>
<span class="text-xl font-bold">MCPProxy</span>
<span v-if="authStore.isTeamsEdition" class="badge badge-xs badge-primary ml-1">Teams</span>
</div>
</router-link>
</div>

<!-- Navigation Menu -->
<nav class="flex-1 p-4">
<ul class="menu">
<li v-for="item in menuItems" :key="item.path">
<router-link
:to="item.path"
:class="{ 'active': isActiveRoute(item.path) }"
class="flex items-center space-x-3 py-3 px-4 rounded-lg"
>
<span class="text-lg">{{ item.name }}</span>
</router-link>
</li>
</ul>
<nav class="flex-1 p-4 overflow-y-auto">
<!-- Teams Edition: User Menu -->
<template v-if="authStore.isTeamsEdition">
<ul class="menu">
<li class="menu-title" v-if="authStore.isAdmin">
<span>My Workspace</span>
</li>
<li v-for="item in teamsUserMenu" :key="item.path">
<router-link
:to="item.path"
:class="{ 'active': isActiveRoute(item.path) }"
class="flex items-center space-x-3 py-3 px-4 rounded-lg"
>
<span class="text-lg">{{ item.name }}</span>
</router-link>
</li>
</ul>

<!-- Admin Section -->
<template v-if="authStore.isAdmin">
<div class="divider my-2 px-2"></div>
<ul class="menu">
<li class="menu-title">
<span>Administration</span>
</li>
<li v-for="item in teamsAdminMenu" :key="item.path">
<router-link
:to="item.path"
:class="{ 'active': isActiveRoute(item.path) }"
class="flex items-center space-x-3 py-3 px-4 rounded-lg"
>
<span class="text-lg">{{ item.name }}</span>
</router-link>
</li>
</ul>
</template>
</template>

<!-- Personal Edition: Original Menu -->
<template v-else>
<ul class="menu">
<li v-for="item in personalMenu" :key="item.path">
<router-link
:to="item.path"
:class="{ 'active': isActiveRoute(item.path) }"
class="flex items-center space-x-3 py-3 px-4 rounded-lg"
>
<span class="text-lg">{{ item.name }}</span>
</router-link>
</li>
</ul>
</template>
</nav>

<!-- User Info (Teams Edition) -->
<div v-if="authStore.isTeamsEdition && authStore.isAuthenticated" class="px-4 py-3 border-t border-base-300">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2 min-w-0">
<div class="avatar placeholder">
<div class="bg-primary text-primary-content rounded-full w-8">
<span class="text-xs">{{ userInitials }}</span>
</div>
</div>
<div class="min-w-0">
<div class="text-sm font-medium truncate">{{ authStore.displayName }}</div>
<div v-if="authStore.user?.email" class="text-xs text-base-content/50 truncate">{{ authStore.user.email }}</div>
</div>
</div>
<button @click="handleLogout" class="btn btn-ghost btn-xs" title="Sign out">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
</button>
</div>
</div>

<!-- Version Display -->
<div v-if="systemStore.version" class="px-4 py-2 border-t border-base-300">
<div class="text-xs text-base-content/60">
Expand Down Expand Up @@ -65,13 +130,18 @@
</template>

<script setup lang="ts">
import { useRoute } from 'vue-router'
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useSystemStore } from '@/stores/system'
import { useAuthStore } from '@/stores/auth'

const route = useRoute()
const router = useRouter()
const systemStore = useSystemStore()
const authStore = useAuthStore()

const menuItems = [
// Personal edition menu (unchanged from original)
const personalMenu = [
{ name: 'Dashboard', path: '/' },
{ name: 'Servers', path: '/servers' },
{ name: 'Secrets', path: '/secrets' },
Expand All @@ -82,10 +152,45 @@ const menuItems = [
{ name: 'Configuration', path: '/settings' },
]

// Teams edition: items visible to all authenticated users
const teamsUserMenu = [
{ name: 'My Servers', path: '/my/servers' },
{ name: 'My Activity', path: '/my/activity' },
{ name: 'Agent Tokens', path: '/my/tokens' },
{ name: 'Diagnostics', path: '/my/diagnostics' },
{ name: 'Search', path: '/search' },
]

// Teams edition: items visible only to admins
const teamsAdminMenu = [
{ name: 'Dashboard', path: '/admin/dashboard' },
{ name: 'Server Management', path: '/admin/servers' },
{ name: 'Activity (All)', path: '/activity' },
{ name: 'Users', path: '/admin/users' },
{ name: 'Sessions', path: '/sessions' },
{ name: 'Configuration', path: '/settings' },
]

// Compute user initials for avatar
const userInitials = computed(() => {
const name = authStore.displayName
if (!name) return '?'
const parts = name.split(/[\s@]+/)
if (parts.length >= 2) {
return (parts[0][0] + parts[1][0]).toUpperCase()
}
return name.substring(0, 2).toUpperCase()
})

function isActiveRoute(path: string): boolean {
if (path === '/') {
return route.path === '/'
}
return route.path.startsWith(path)
}

async function handleLogout() {
await authStore.logout()
router.push('/login')
}
</script>
6 changes: 5 additions & 1 deletion frontend/src/components/TopHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
<span class="hidden sm:inline ml-2">Add Server</span>
<span class="hidden sm:inline ml-2">{{ addServerLabel }}</span>
</button>
</div>

Expand Down Expand Up @@ -95,11 +95,15 @@ import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useSystemStore } from '@/stores/system'
import { useServersStore } from '@/stores/servers'
import { useAuthStore } from '@/stores/auth'
import AddServerModal from './AddServerModal.vue'

const router = useRouter()
const systemStore = useSystemStore()
const serversStore = useServersStore()
const authStore = useAuthStore()

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

const searchQuery = ref('')
const copyTooltip = ref('Copy MCP address')
Expand Down
Loading
Loading