feat(mcp): operators can switch MCP (and OAuth) off
Admin switch in the system settings, MCP_ENABLED fixes it. Off: the endpoint answers 404, OAuth is off, the API key settings hide the MCP tab. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
39a04082fc
commit
c6f28aa816
17 files changed
+197
-30
No files matched your search
@@ -160,6 +160,8 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
backupRetentionDays: sys.backupRetentionDays || 3,
|
||||
retentionAllowNever: sys.retentionAllowNever ?? true,
|
||||
retentionNeverViaEnv: Boolean(sys.retentionNeverViaEnv),
|
||||
mcpEnabled: sys.mcpEnabled ?? true,
|
||||
mcpConfiguredViaEnv: Boolean(sys.mcpConfiguredViaEnv),
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
@@ -1334,6 +1336,24 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* AI agents over MCP (and claude.ai / ChatGPT over OAuth) */}
|
||||
<div className="p-4 bg-canvas border border-border rounded-card flex items-center justify-between gap-4">
|
||||
<div className="pr-4">
|
||||
<p className="font-bold text-main">KI-Agenten (MCP) erlauben</p>
|
||||
<p className="text-[11px] text-muted mt-0.5">
|
||||
Eingeschaltet können Claude Code, Codex & Co. per API-Schlüssel und claude.ai oder ChatGPT als Connector
|
||||
auf MindSift zugreifen. Ausgeschaltet ist der MCP-Endpunkt samt Connector-Anmeldung abgeschaltet;
|
||||
bestehende Connector-Verbindungen ruhen, API-Schlüssel funktionieren per curl weiter.
|
||||
{settings.mcpConfiguredViaEnv && ' Festgelegt per MCP_ENABLED.'}
|
||||
</p>
|
||||
</div>
|
||||
<ToggleSwitch
|
||||
checked={settings.mcpEnabled !== false}
|
||||
disabled={Boolean(settings.mcpConfiguredViaEnv)}
|
||||
onChange={(mcpEnabled) => handleSaveSystemSettings({ mcpEnabled })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Who may register without an invite */}
|
||||
{(() => {
|
||||
const mode = settings.registrationMode || 'closed';
|
||||
|
||||
@@ -37,6 +37,8 @@ export const ApiKeysPanel: React.FC<ApiKeysPanelProps> = ({ onIngested }) => {
|
||||
const [connectorUrl, setConnectorUrl] = useState('');
|
||||
const [copiedConnector, setCopiedConnector] = useState(false);
|
||||
const [access, setAccess] = useState<Access>('rest');
|
||||
// The operator can switch MCP off; then only the curl / smart home access is shown
|
||||
const [mcpEnabled, setMcpEnabled] = useState(true);
|
||||
|
||||
// Live Webhook Simulator State
|
||||
const [simTitle, setSimTitle] = useState('Waschmaschine im Keller ist fertig!');
|
||||
@@ -59,7 +61,10 @@ export const ApiKeysPanel: React.FC<ApiKeysPanelProps> = ({ onIngested }) => {
|
||||
api.getTaskLists().then(setTaskLists).catch(() => {});
|
||||
api
|
||||
.getAuthStatus()
|
||||
.then((st) => setConnectorUrl(st.mcpConnectorUrl || ''))
|
||||
.then((st) => {
|
||||
setConnectorUrl(st.mcpConnectorUrl || '');
|
||||
setMcpEnabled(st.mcpEnabled !== false);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
@@ -358,30 +363,32 @@ http_headers = { "X-API-Key" = "${exampleToken}" }`
|
||||
|
||||
{/* How to connect: one API, two accesses */}
|
||||
<div className="pt-4 border-t border-border space-y-4">
|
||||
<div className="flex flex-wrap gap-1.5" role="tablist" aria-label="Zugang">
|
||||
{(
|
||||
[
|
||||
['rest', 'curl / Smart Home', Terminal],
|
||||
['mcp', 'KI-Agenten (MCP)', Bot],
|
||||
] as const
|
||||
).map(([value, label, Icon]) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={access === value}
|
||||
onClick={() => setAccess(value)}
|
||||
className={`h-11 sm:h-8 px-3 rounded-btn border text-xs flex items-center gap-1.5 transition-colors ${
|
||||
access === value ? 'border-accent bg-accent/10 text-main' : 'border-border text-muted hover:text-main'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-3.5 h-3.5 text-accent" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{mcpEnabled && (
|
||||
<div className="flex flex-wrap gap-1.5" role="tablist" aria-label="Zugang">
|
||||
{(
|
||||
[
|
||||
['rest', 'curl / Smart Home', Terminal],
|
||||
['mcp', 'KI-Agenten (MCP)', Bot],
|
||||
] as const
|
||||
).map(([value, label, Icon]) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={access === value}
|
||||
onClick={() => setAccess(value)}
|
||||
className={`h-11 sm:h-8 px-3 rounded-btn border text-xs flex items-center gap-1.5 transition-colors ${
|
||||
access === value ? 'border-accent bg-accent/10 text-main' : 'border-border text-muted hover:text-main'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-3.5 h-3.5 text-accent" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{access === 'rest' && (
|
||||
{(access === 'rest' || !mcpEnabled) && (
|
||||
<div className="space-y-5">
|
||||
{/* Reading: e.g. a smart home sensor for what is due today */}
|
||||
<div>
|
||||
@@ -547,7 +554,7 @@ http_headers = { "X-API-Key" = "${exampleToken}" }`
|
||||
</div>
|
||||
)}
|
||||
|
||||
{access === 'mcp' && (
|
||||
{access === 'mcp' && mcpEnabled && (
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-7 h-7 bg-accent text-on-accent flex items-center justify-center flex-shrink-0">
|
||||
|
||||
@@ -3,13 +3,16 @@ import { render, screen, fireEvent, act, within } from '@testing-library/react';
|
||||
|
||||
const createAPIKey = vi.fn(async () => ({ id: 'k1', userId: 'u', name: 'HA', keyPrefix: 'mindsift_sec_ab', scopes: 'webhook:triage,webhook:list', createdAt: '' }));
|
||||
const updateAPIKey = vi.fn(async () => ({}));
|
||||
const getAuthStatus = vi.fn(async (): Promise<{ mcpConnectorUrl?: string; mcpEnabled?: boolean }> => ({
|
||||
mcpConnectorUrl: 'https://ms.example/api/v1/mcp',
|
||||
}));
|
||||
vi.mock('../../../lib/api', () => ({
|
||||
api: {
|
||||
getAPIKeys: vi.fn(async () => [
|
||||
{ id: 'k0', userId: 'u', name: 'Rauchmelder', keyPrefix: 'mindsift_sec_xy', scopes: 'webhook:triage,webhook:task', createdAt: '' },
|
||||
{ id: 'k9', userId: 'u', name: 'Claude (OAuth)', keyPrefix: 'OAuth', scopes: 'tasks:read', createdAt: '', kind: 'oauth' },
|
||||
]),
|
||||
getAuthStatus: vi.fn(async () => ({ mcpConnectorUrl: 'https://ms.example/api/v1/mcp' })),
|
||||
getAuthStatus: (...args: unknown[]) => getAuthStatus(...(args as [])),
|
||||
getAllLists: vi.fn(async () => []),
|
||||
getTaskLists: vi.fn(async () => []),
|
||||
createAPIKey: (...args: unknown[]) => createAPIKey(...(args as [])),
|
||||
@@ -59,6 +62,14 @@ describe('ApiKeysPanel', () => {
|
||||
expect(screen.getByText(/\[mcp_servers\.mindsift\]/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('hides the MCP tab when the operator switched MCP off', async () => {
|
||||
getAuthStatus.mockResolvedValueOnce({ mcpConnectorUrl: '', mcpEnabled: false });
|
||||
render(<ApiKeysPanel />);
|
||||
await act(async () => {});
|
||||
expect(screen.queryByRole('tab', { name: /KI-Agenten/ })).toBeNull();
|
||||
expect(screen.getAllByText(/tasks\?due=today&status=open/).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('marks OAuth connections instead of showing a key prefix', async () => {
|
||||
render(<ApiKeysPanel />);
|
||||
expect(await screen.findByText('Claude (OAuth)')).toBeTruthy();
|
||||
|
||||
Reference in new issue
Block a user