feat: Agent Connect page + /api/agent/config endpoint

- New AgentConnect.jsx page with MCP URL, token, setup instructions
- Claude Desktop config snippet with copy button
- Claude Code CLI command with copy button
- Agent capabilities overview
- Added to bottom nav bar (6th item, plug icon)
- Backend /api/agent/config endpoint returns deployment-aware MCP details
- Nav padding adjusted for 6 items
- Frontend rebuilt with new page
This commit is contained in:
Claude 2026-07-01 03:45:34 +00:00
parent a6e6e2f54f
commit df39a8dc08
10 changed files with 325 additions and 72 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -7,8 +7,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<title>Fitness Rewards</title>
<script type="module" crossorigin src="/assets/index-DIoo6DtB.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CRHpiAro.css">
<script type="module" crossorigin src="/assets/index-De1TwOuS.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-ciTnvXdZ.css">
</head>
<body>
<div id="root"></div>

View file

@ -98,6 +98,7 @@ from diligence.routers.support import router as support_router
from diligence.routers.nutrition import router as nutrition_router
from diligence.routers.meal_plans import router as meal_plans_router
from diligence.routers.ai_chat import router as ai_chat_router
from diligence.routers.agent import router as agent_router
app.include_router(auth_router)
app.include_router(onboarding_router)
@ -112,6 +113,7 @@ app.include_router(programs_router)
app.include_router(nutrition_router)
app.include_router(meal_plans_router)
app.include_router(ai_chat_router)
app.include_router(agent_router)
@app.get("/api/health")

View file

@ -0,0 +1,35 @@
"""Agent connection endpoint — returns MCP config for AI agent setup."""
from __future__ import annotations
from fastapi import APIRouter, Depends
from diligence.config import get_settings
from diligence.utils.auth import get_current_user
router = APIRouter(prefix="/api/agent", tags=["agent"])
@router.get("/config")
async def agent_config(user=Depends(get_current_user)):
"""Return MCP connection details for the authenticated user."""
settings = get_settings()
# Determine MCP URL based on deployment mode
base = settings.base_url.rstrip("/")
if settings.is_sqlite:
# pip install path — MCP served from same process
mcp_url = f"{base}/mcp/sse"
else:
# Docker path — MCP on separate container port 3001
from urllib.parse import urlparse
parsed = urlparse(base)
docker_host = parsed.hostname or "localhost"
mcp_url = f"http://{docker_host}:3001/sse"
return {
"mcp_url": mcp_url,
"api_token": settings.api_token if getattr(user, "is_admin", False) else None,
"api_token_set": bool(settings.api_token),
"deployment": "local" if settings.is_sqlite else "docker",
"base_url": base,
"tools_count": 14,
}

View file

@ -19,6 +19,7 @@ import CatalogDetail from './pages/CatalogDetail'
import Support from './pages/Support'
import SupportAdmin from './pages/SupportAdmin'
import ChatCoach from './pages/ChatCoach'
import AgentConnect from './pages/AgentConnect'
function ProtectedRoute({ children }) {
if (!hasToken()) return <Navigate to="/login" />
@ -95,6 +96,9 @@ function NavBar() {
<NavLink to="/chat" className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}>
<span className="nav-icon">🧠</span> Coach
</NavLink>
<NavLink to="/agent" className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}>
<span className="nav-icon">🔌</span> Agent
</NavLink>
</nav>
)
}
@ -123,6 +127,7 @@ export default function App() {
<Route path="/settings/integrations" element={<ProtectedRoute><SettingsIntegrations /></ProtectedRoute>} />
<Route path="/meal-plan" element={<ProtectedRoute><MealPlan /></ProtectedRoute>} />
<Route path="/chat" element={<ProtectedRoute><ChatCoach /></ProtectedRoute>} />
<Route path="/agent" element={<ProtectedRoute><AgentConnect /></ProtectedRoute>} />
</Routes>
{hasToken() && <NavBar />}
</>

View file

@ -145,6 +145,9 @@ export const api = {
// Resources
getResourceRecommendations: () => request('/onboarding/recommendations'),
// Agent
agentConfig: () => request('/agent/config'),
};
export function setToken(token) {

View file

@ -233,7 +233,7 @@ input::placeholder, textarea::placeholder { color: var(--text-3); font-weight: 4
font-size: 0.62rem;
font-weight: 700;
letter-spacing: 0.03em;
padding: 6px 14px;
padding: 6px 10px;
text-decoration: none;
transition: color 0.15s;
border-radius: var(--r-sm);

View file

@ -0,0 +1,208 @@
import { useState, useEffect } from 'react'
import { api } from '../api'
function CopyButton({ text, label }) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch { /* fallback for non-HTTPS */ }
}
return (
<button
onClick={handleCopy}
className="btn-outline btn-sm"
style={{ fontSize: '0.75rem', padding: '6px 12px', whiteSpace: 'nowrap' }}
>
{copied ? 'Copied' : (label || 'Copy')}
</button>
)
}
function CodeBlock({ children, copyText }) {
return (
<div style={{
background: 'var(--bg)', border: '1px solid var(--divider)',
borderRadius: 'var(--r-sm)', padding: '14px 16px',
fontFamily: 'var(--font-mono)', fontSize: '0.78rem', lineHeight: 1.6,
overflowX: 'auto', position: 'relative', whiteSpace: 'pre',
}}>
{copyText && (
<div style={{ position: 'absolute', top: '8px', right: '8px' }}>
<CopyButton text={copyText} />
</div>
)}
{children}
</div>
)
}
export default function AgentConnect() {
const [config, setConfig] = useState(null)
const [loading, setLoading] = useState(true)
const [showToken, setShowToken] = useState(false)
useEffect(() => {
api.agentConfig()
.then(setConfig)
.catch(console.error)
.finally(() => setLoading(false))
}, [])
if (loading) return <div className="page"><div className="loading">Loading...</div></div>
if (!config) return <div className="page"><div className="error-msg">Failed to load agent config</div></div>
const maskedToken = config.api_token
? config.api_token.slice(0, 8) + '\u2026' + config.api_token.slice(-4)
: null
const claudeDesktopConfig = JSON.stringify({
"mcpServers": {
"diligence": {
"url": config.mcp_url,
...(config.api_token ? { "headers": { "Authorization": `Bearer ${config.api_token}` } } : {})
}
}
}, null, 2)
return (
<div className="page">
<div style={{ marginBottom: '20px' }}>
<h1 style={{
fontFamily: 'var(--font-display)', fontWeight: 800,
fontSize: '1.3rem', marginBottom: '4px',
}}>
Connect Your AI Agent
</h1>
<p style={{ color: 'var(--text-3)', fontSize: '0.85rem' }}>
Use any MCP-compatible AI to log workouts, track food, and manage your fitness.
</p>
</div>
{/* Connection Details */}
<div className="card">
<div className="section-label">Connection</div>
<div style={{ marginBottom: '14px' }}>
<div style={{ fontSize: '0.75rem', color: 'var(--text-3)', fontWeight: 600, marginBottom: '4px' }}>
MCP Endpoint
</div>
<div style={{
display: 'flex', alignItems: 'center', gap: '8px',
background: 'var(--bg)', borderRadius: 'var(--r-sm)',
padding: '10px 12px', border: '1px solid var(--divider)',
}}>
<code style={{
flex: 1, fontFamily: 'var(--font-mono)', fontSize: '0.82rem',
color: 'var(--accent)', wordBreak: 'break-all',
}}>
{config.mcp_url}
</code>
<CopyButton text={config.mcp_url} />
</div>
</div>
{config.api_token && (
<div style={{ marginBottom: '14px' }}>
<div style={{ fontSize: '0.75rem', color: 'var(--text-3)', fontWeight: 600, marginBottom: '4px' }}>
API Token
</div>
<div style={{
display: 'flex', alignItems: 'center', gap: '8px',
background: 'var(--bg)', borderRadius: 'var(--r-sm)',
padding: '10px 12px', border: '1px solid var(--divider)',
}}>
<code
onClick={() => setShowToken(!showToken)}
style={{
flex: 1, fontFamily: 'var(--font-mono)', fontSize: '0.82rem',
cursor: 'pointer', wordBreak: 'break-all',
}}
>
{showToken ? config.api_token : maskedToken}
</code>
<CopyButton text={config.api_token} />
</div>
<div style={{ fontSize: '0.7rem', color: 'var(--text-3)', marginTop: '4px' }}>
Tap token to reveal. Only visible to admins.
</div>
</div>
)}
{!config.api_token && config.api_token_set && (
<div style={{
fontSize: '0.8rem', color: 'var(--text-3)', fontStyle: 'italic',
padding: '8px 0',
}}>
API token is set but only visible to admin users.
</div>
)}
<div style={{
display: 'flex', gap: '12px', flexWrap: 'wrap',
fontSize: '0.78rem', color: 'var(--text-3)',
}}>
<span>{config.tools_count} tools available</span>
<span style={{ color: 'var(--divider)' }}>|</span>
<span>{config.deployment === 'local' ? 'Local (SQLite)' : 'Docker (PostgreSQL)'}</span>
</div>
</div>
{/* Claude Desktop Setup */}
<div className="card">
<div className="section-label">Claude Desktop</div>
<p style={{ fontSize: '0.82rem', color: 'var(--text-2)', marginBottom: '12px' }}>
Add this to your Claude Desktop MCP config:
</p>
<CodeBlock copyText={claudeDesktopConfig}>
{claudeDesktopConfig}
</CodeBlock>
<p style={{ fontSize: '0.75rem', color: 'var(--text-3)', marginTop: '10px' }}>
On macOS: <code style={{ fontFamily: 'var(--font-mono)', fontSize: '0.72rem' }}>
~/Library/Application Support/Claude/claude_desktop_config.json
</code>
</p>
</div>
{/* Claude Code Setup */}
<div className="card">
<div className="section-label">Claude Code</div>
<p style={{ fontSize: '0.82rem', color: 'var(--text-2)', marginBottom: '12px' }}>
Add the MCP server from your terminal:
</p>
<CodeBlock copyText={`claude mcp add diligence --transport sse ${config.mcp_url}`}>
{`claude mcp add diligence --transport sse ${config.mcp_url}`}
</CodeBlock>
</div>
{/* What your agent can do */}
<div className="card">
<div className="section-label">What Your Agent Can Do</div>
<div style={{ display: 'grid', gap: '8px' }}>
{[
['Log workouts', 'Track any activity and earn points automatically'],
['Track food', 'Search 400K+ foods, log meals with full macros'],
['Manage meal plans', 'Create plans, track compliance, adjust portions'],
['Check progress', 'Daily points, weekly summary, program status'],
['Redeem rewards', 'Spend earned points on rewards you set up'],
].map(([title, desc]) => (
<div key={title} style={{
display: 'flex', gap: '10px', alignItems: 'flex-start',
padding: '8px 0', borderBottom: '1px solid var(--divider)',
}}>
<span style={{ color: 'var(--green)', fontSize: '0.9rem', marginTop: '1px' }}>
&#10003;
</span>
<div>
<div style={{ fontWeight: 700, fontSize: '0.85rem' }}>{title}</div>
<div style={{ fontSize: '0.78rem', color: 'var(--text-3)' }}>{desc}</div>
</div>
</div>
))}
</div>
</div>
</div>
)
}