=== NEW === - studio/ — MCPEngine Studio scaffold (Next.js monorepo, build plan) - docs/FACTORY-V2.md — Factory v2 architecture doc - docs/CALENDLY_MCP_BUILD_SUMMARY.md — Calendly MCP build report === UPDATED SERVERS === - fieldedge: Added jobs-tools, UI build script, main entry update - lightspeed: Updated main + server entry points - squarespace: Added collection-browser + page-manager apps - toast: Added main + server entry points === INFRA === - infra/command-center/state.json — Updated pipeline state - infra/command-center/FACTORY-V2.md — Factory v2 operator playbook
229 lines
8.5 KiB
TypeScript
229 lines
8.5 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useCallback } from 'react';
|
|
import { Key, Globe, Shield, XCircle, X } from 'lucide-react';
|
|
|
|
import type { AuthConfig } from '@mcpengine/ai-pipeline/types';
|
|
|
|
interface AuthConfigPanelProps {
|
|
config?: AuthConfig;
|
|
onChange: (config: AuthConfig | undefined) => void;
|
|
}
|
|
|
|
type AuthType = 'none' | 'api_key' | 'oauth2' | 'bearer';
|
|
|
|
const AUTH_OPTIONS: { value: AuthType; label: string; icon: React.ReactNode }[] = [
|
|
{ value: 'none', label: 'None', icon: <XCircle className="w-3.5 h-3.5" /> },
|
|
{ value: 'api_key', label: 'API Key', icon: <Key className="w-3.5 h-3.5" /> },
|
|
{ value: 'oauth2', label: 'OAuth2', icon: <Globe className="w-3.5 h-3.5" /> },
|
|
{ value: 'bearer', label: 'Bearer', icon: <Shield className="w-3.5 h-3.5" /> },
|
|
];
|
|
|
|
export function AuthConfigPanel({ config, onChange }: AuthConfigPanelProps) {
|
|
const currentType: AuthType = (['api_key', 'oauth2', 'bearer'].includes(config?.type ?? '') ? config!.type : 'none') as AuthType;
|
|
|
|
const [keyName, setKeyName] = useState(config?.keyName ?? '');
|
|
const [keyLocation, setKeyLocation] = useState<'header' | 'query'>(
|
|
config?.keyLocation ?? 'header'
|
|
);
|
|
const [authUrl, setAuthUrl] = useState(config?.oauthConfig?.authUrl ?? '');
|
|
const [tokenUrl, setTokenUrl] = useState(config?.oauthConfig?.tokenUrl ?? '');
|
|
const [scopes, setScopes] = useState<string[]>(config?.oauthConfig?.scopes ?? []);
|
|
const [scopeInput, setScopeInput] = useState('');
|
|
const [bearerInstructions, setBearerInstructions] = useState('');
|
|
|
|
const selectType = useCallback(
|
|
(type: AuthType) => {
|
|
if (type === 'none') {
|
|
onChange(undefined);
|
|
return;
|
|
}
|
|
const base: AuthConfig = { type };
|
|
if (type === 'api_key') {
|
|
base.keyName = keyName;
|
|
base.keyLocation = keyLocation;
|
|
} else if (type === 'oauth2') {
|
|
base.oauthConfig = { authUrl, tokenUrl, scopes };
|
|
}
|
|
onChange(base);
|
|
},
|
|
[onChange, keyName, keyLocation, authUrl, tokenUrl, scopes]
|
|
);
|
|
|
|
const addScope = useCallback(() => {
|
|
const trimmed = scopeInput.trim();
|
|
if (trimmed && !scopes.includes(trimmed)) {
|
|
const newScopes = [...scopes, trimmed];
|
|
setScopes(newScopes);
|
|
setScopeInput('');
|
|
onChange({
|
|
type: 'oauth2',
|
|
oauthConfig: { authUrl, tokenUrl, scopes: newScopes },
|
|
});
|
|
}
|
|
}, [scopeInput, scopes, onChange, authUrl, tokenUrl]);
|
|
|
|
const removeScope = useCallback(
|
|
(scope: string) => {
|
|
const newScopes = scopes.filter((s) => s !== scope);
|
|
setScopes(newScopes);
|
|
onChange({
|
|
type: 'oauth2',
|
|
oauthConfig: { authUrl, tokenUrl, scopes: newScopes },
|
|
});
|
|
},
|
|
[scopes, onChange, authUrl, tokenUrl]
|
|
);
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{/* Auth type radio buttons */}
|
|
<div className="grid grid-cols-4 gap-1.5">
|
|
{AUTH_OPTIONS.map((opt) => (
|
|
<button
|
|
key={opt.value}
|
|
onClick={() => selectType(opt.value)}
|
|
className={`
|
|
flex flex-col items-center gap-1 py-2 px-1 rounded-lg border text-xs transition-colors
|
|
${currentType === opt.value
|
|
? 'border-indigo-500 bg-indigo-500/10 text-indigo-400'
|
|
: 'border-gray-700 bg-gray-800 text-gray-400 hover:border-gray-600'
|
|
}
|
|
`}
|
|
>
|
|
{opt.icon}
|
|
<span>{opt.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* API Key config */}
|
|
{currentType === 'api_key' && (
|
|
<div className="space-y-2 pt-1">
|
|
<div>
|
|
<label className="block text-[10px] text-gray-500 mb-1">Key Name</label>
|
|
<input
|
|
type="text"
|
|
value={keyName}
|
|
onChange={(e) => {
|
|
setKeyName(e.target.value);
|
|
onChange({
|
|
type: 'api_key',
|
|
keyName: e.target.value,
|
|
keyLocation,
|
|
});
|
|
}}
|
|
placeholder="e.g. X-API-Key"
|
|
className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-xs text-gray-300 outline-none focus:border-indigo-500 transition-colors"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-[10px] text-gray-500 mb-1">Location</label>
|
|
<select
|
|
value={keyLocation}
|
|
onChange={(e) => {
|
|
const loc = e.target.value as 'header' | 'query';
|
|
setKeyLocation(loc);
|
|
onChange({ type: 'api_key', keyName, keyLocation: loc });
|
|
}}
|
|
className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-xs text-gray-300 outline-none"
|
|
>
|
|
<option value="header">Header</option>
|
|
<option value="query">Query Parameter</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* OAuth2 config */}
|
|
{currentType === 'oauth2' && (
|
|
<div className="space-y-2 pt-1">
|
|
<div>
|
|
<label className="block text-[10px] text-gray-500 mb-1">Authorization URL</label>
|
|
<input
|
|
type="text"
|
|
value={authUrl}
|
|
onChange={(e) => {
|
|
setAuthUrl(e.target.value);
|
|
onChange({
|
|
type: 'oauth2',
|
|
oauthConfig: { authUrl: e.target.value, tokenUrl, scopes },
|
|
});
|
|
}}
|
|
placeholder="https://provider.com/oauth/authorize"
|
|
className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-xs text-gray-300 outline-none focus:border-indigo-500 transition-colors"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-[10px] text-gray-500 mb-1">Token URL</label>
|
|
<input
|
|
type="text"
|
|
value={tokenUrl}
|
|
onChange={(e) => {
|
|
setTokenUrl(e.target.value);
|
|
onChange({
|
|
type: 'oauth2',
|
|
oauthConfig: { authUrl, tokenUrl: e.target.value, scopes },
|
|
});
|
|
}}
|
|
placeholder="https://provider.com/oauth/token"
|
|
className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-xs text-gray-300 outline-none focus:border-indigo-500 transition-colors"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-[10px] text-gray-500 mb-1">Scopes</label>
|
|
<div className="flex flex-wrap gap-1.5 mb-1.5">
|
|
{scopes.map((scope) => (
|
|
<span
|
|
key={scope}
|
|
className="inline-flex items-center gap-1 bg-indigo-500/15 text-indigo-400 text-[10px] px-2 py-0.5 rounded-full"
|
|
>
|
|
{scope}
|
|
<button onClick={() => removeScope(scope)}>
|
|
<X className="w-2.5 h-2.5" />
|
|
</button>
|
|
</span>
|
|
))}
|
|
</div>
|
|
<div className="flex gap-1.5">
|
|
<input
|
|
type="text"
|
|
value={scopeInput}
|
|
onChange={(e) => setScopeInput(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && addScope()}
|
|
placeholder="Add scope..."
|
|
className="flex-1 bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-xs text-gray-300 outline-none focus:border-indigo-500 transition-colors"
|
|
/>
|
|
<button
|
|
onClick={addScope}
|
|
className="px-2 py-1.5 bg-gray-700 text-gray-300 text-xs rounded hover:bg-gray-600 transition-colors"
|
|
>
|
|
Add
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Bearer config */}
|
|
{currentType === 'bearer' && (
|
|
<div className="space-y-2 pt-1">
|
|
<div>
|
|
<label className="block text-[10px] text-gray-500 mb-1">Instructions</label>
|
|
<textarea
|
|
value={bearerInstructions}
|
|
onChange={(e) => setBearerInstructions(e.target.value)}
|
|
rows={3}
|
|
placeholder="Provide instructions for obtaining a bearer token..."
|
|
className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-xs text-gray-300 outline-none focus:border-indigo-500 transition-colors resize-none"
|
|
/>
|
|
</div>
|
|
<p className="text-[10px] text-gray-500">
|
|
Users will supply a Bearer token at runtime via environment variable.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|