=== 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
337 lines
12 KiB
TypeScript
337 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import React, { useCallback, useState } from 'react';
|
|
import {
|
|
X,
|
|
Trash2,
|
|
AlertTriangle,
|
|
Shield,
|
|
FileJson,
|
|
Settings2,
|
|
Tags,
|
|
} from 'lucide-react';
|
|
|
|
import type {
|
|
ToolDefinition,
|
|
ToolAnnotations,
|
|
SchemaProperty,
|
|
} from '@mcpengine/ai-pipeline/types';
|
|
|
|
import { ParamEditor } from './ParamEditor';
|
|
import { AuthConfigPanel } from './AuthConfigPanel';
|
|
|
|
interface ToolInspectorProps {
|
|
tool: ToolDefinition;
|
|
onChange: (tool: ToolDefinition) => void;
|
|
onDelete: (toolName: string) => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export function ToolInspector({ tool, onChange, onDelete, onClose }: ToolInspectorProps) {
|
|
const [outputExpanded, setOutputExpanded] = useState(false);
|
|
|
|
// Update a top-level field
|
|
const updateField = useCallback(
|
|
<K extends keyof ToolDefinition>(field: K, value: ToolDefinition[K]) => {
|
|
onChange({ ...tool, [field]: value });
|
|
},
|
|
[tool, onChange]
|
|
);
|
|
|
|
// Update annotations
|
|
const updateAnnotation = useCallback(
|
|
(key: keyof ToolAnnotations, value: boolean) => {
|
|
onChange({
|
|
...tool,
|
|
annotations: {
|
|
...tool.annotations,
|
|
[key]: value,
|
|
},
|
|
});
|
|
},
|
|
[tool, onChange]
|
|
);
|
|
|
|
// Update a single parameter
|
|
const updateParam = useCallback(
|
|
(paramName: string, updates: Partial<SchemaProperty>) => {
|
|
const newProps = { ...tool.inputSchema.properties };
|
|
newProps[paramName] = { ...newProps[paramName], ...updates };
|
|
onChange({
|
|
...tool,
|
|
inputSchema: { ...tool.inputSchema, properties: newProps },
|
|
});
|
|
},
|
|
[tool, onChange]
|
|
);
|
|
|
|
// Remove a parameter
|
|
const removeParam = useCallback(
|
|
(paramName: string) => {
|
|
const newProps = { ...tool.inputSchema.properties };
|
|
delete newProps[paramName];
|
|
const newRequired = (tool.inputSchema.required ?? []).filter(
|
|
(r) => r !== paramName
|
|
);
|
|
onChange({
|
|
...tool,
|
|
inputSchema: {
|
|
...tool.inputSchema,
|
|
properties: newProps,
|
|
required: newRequired,
|
|
},
|
|
});
|
|
},
|
|
[tool, onChange]
|
|
);
|
|
|
|
// Toggle required status
|
|
const toggleRequired = useCallback(
|
|
(paramName: string) => {
|
|
const required = tool.inputSchema.required ?? [];
|
|
const isReq = required.includes(paramName);
|
|
onChange({
|
|
...tool,
|
|
inputSchema: {
|
|
...tool.inputSchema,
|
|
required: isReq
|
|
? required.filter((r) => r !== paramName)
|
|
: [...required, paramName],
|
|
},
|
|
});
|
|
},
|
|
[tool, onChange]
|
|
);
|
|
|
|
// Add a new parameter
|
|
const addParam = useCallback(() => {
|
|
const name = `param_${Object.keys(tool.inputSchema.properties).length + 1}`;
|
|
onChange({
|
|
...tool,
|
|
inputSchema: {
|
|
...tool.inputSchema,
|
|
properties: {
|
|
...tool.inputSchema.properties,
|
|
[name]: { type: 'string', description: '' },
|
|
},
|
|
},
|
|
});
|
|
}, [tool, onChange]);
|
|
|
|
const paramEntries = Object.entries(tool.inputSchema?.properties ?? {});
|
|
|
|
return (
|
|
<div className="w-[380px] h-full bg-gray-900 border-l border-gray-700 flex flex-col overflow-hidden">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700">
|
|
<h2 className="text-sm font-semibold text-gray-100">Tool Inspector</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-1 rounded hover:bg-gray-800 text-gray-400 hover:text-gray-200 transition-colors"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Scrollable content */}
|
|
<div className="flex-1 overflow-y-auto">
|
|
{/* Name + Description */}
|
|
<section className="px-4 py-4 border-b border-gray-800">
|
|
<label className="block text-xs text-gray-400 mb-1.5">Name</label>
|
|
<input
|
|
type="text"
|
|
value={tool.name}
|
|
onChange={(e) => updateField('name', e.target.value)}
|
|
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 outline-none transition-colors"
|
|
/>
|
|
|
|
<label className="block text-xs text-gray-400 mb-1.5 mt-3">Description</label>
|
|
<textarea
|
|
value={tool.description}
|
|
onChange={(e) => updateField('description', e.target.value)}
|
|
rows={3}
|
|
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 outline-none transition-colors resize-none"
|
|
/>
|
|
|
|
<div className="flex gap-2 mt-3">
|
|
<div className="flex-1">
|
|
<label className="block text-xs text-gray-400 mb-1.5">Endpoint</label>
|
|
<input
|
|
type="text"
|
|
value={tool.endpoint}
|
|
onChange={(e) => updateField('endpoint', e.target.value)}
|
|
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 outline-none transition-colors"
|
|
/>
|
|
</div>
|
|
<div className="w-24">
|
|
<label className="block text-xs text-gray-400 mb-1.5">Method</label>
|
|
<select
|
|
value={tool.method}
|
|
onChange={(e) => updateField('method', e.target.value)}
|
|
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-gray-100 focus:border-indigo-500 outline-none"
|
|
>
|
|
<option value="GET">GET</option>
|
|
<option value="POST">POST</option>
|
|
<option value="PUT">PUT</option>
|
|
<option value="PATCH">PATCH</option>
|
|
<option value="DELETE">DELETE</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Parameters */}
|
|
<section className="px-4 py-4 border-b border-gray-800">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-2">
|
|
<Settings2 className="w-3.5 h-3.5 text-gray-400" />
|
|
<h3 className="text-xs font-medium text-gray-300 uppercase tracking-wider">
|
|
Parameters
|
|
</h3>
|
|
</div>
|
|
<button
|
|
onClick={addParam}
|
|
className="text-xs text-indigo-400 hover:text-indigo-300 transition-colors"
|
|
>
|
|
+ Add
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
{paramEntries.map(([name, prop]) => (
|
|
<ParamEditor
|
|
key={name}
|
|
name={name}
|
|
property={prop}
|
|
required={(tool.inputSchema.required ?? []).includes(name)}
|
|
onUpdate={(updates) => updateParam(name, updates)}
|
|
onToggleRequired={() => toggleRequired(name)}
|
|
onDelete={() => removeParam(name)}
|
|
/>
|
|
))}
|
|
{paramEntries.length === 0 && (
|
|
<p className="text-xs text-gray-500 py-2 text-center">
|
|
No parameters defined
|
|
</p>
|
|
)}
|
|
</div>
|
|
</section>
|
|
|
|
{/* Output Schema */}
|
|
<section className="px-4 py-4 border-b border-gray-800">
|
|
<button
|
|
onClick={() => setOutputExpanded(!outputExpanded)}
|
|
className="flex items-center gap-2 w-full"
|
|
>
|
|
<FileJson className="w-3.5 h-3.5 text-gray-400" />
|
|
<h3 className="text-xs font-medium text-gray-300 uppercase tracking-wider flex-1 text-left">
|
|
Output Schema
|
|
</h3>
|
|
<span className="text-xs text-gray-500">
|
|
{outputExpanded ? '▾' : '▸'}
|
|
</span>
|
|
</button>
|
|
{outputExpanded && (
|
|
<div className="mt-3">
|
|
<textarea
|
|
value={JSON.stringify(tool.outputSchema ?? {}, null, 2)}
|
|
onChange={(e) => {
|
|
try {
|
|
updateField('outputSchema', JSON.parse(e.target.value));
|
|
} catch {
|
|
// Invalid JSON, don't update
|
|
}
|
|
}}
|
|
rows={6}
|
|
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-xs text-gray-300 font-mono focus:border-indigo-500 outline-none resize-none"
|
|
/>
|
|
</div>
|
|
)}
|
|
</section>
|
|
|
|
{/* Auth Config */}
|
|
<section className="px-4 py-4 border-b border-gray-800">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Shield className="w-3.5 h-3.5 text-gray-400" />
|
|
<h3 className="text-xs font-medium text-gray-300 uppercase tracking-wider">
|
|
Authentication
|
|
</h3>
|
|
</div>
|
|
<AuthConfigPanel
|
|
config={undefined}
|
|
onChange={() => {
|
|
// Auth is configured at project level, not per-tool
|
|
// This panel is informational / override
|
|
}}
|
|
/>
|
|
</section>
|
|
|
|
{/* Annotations */}
|
|
<section className="px-4 py-4 border-b border-gray-800">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Tags className="w-3.5 h-3.5 text-gray-400" />
|
|
<h3 className="text-xs font-medium text-gray-300 uppercase tracking-wider">
|
|
Annotations
|
|
</h3>
|
|
</div>
|
|
<div className="space-y-2.5">
|
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={tool.annotations?.readOnlyHint ?? false}
|
|
onChange={(e) => updateAnnotation('readOnlyHint', e.target.checked)}
|
|
className="w-4 h-4 rounded border-gray-600 bg-gray-800 text-indigo-500 focus:ring-indigo-500/30"
|
|
/>
|
|
<div>
|
|
<span className="text-sm text-gray-200">Read Only</span>
|
|
<p className="text-[11px] text-gray-500">Tool only reads data, no side effects</p>
|
|
</div>
|
|
</label>
|
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={tool.annotations?.destructiveHint ?? false}
|
|
onChange={(e) => updateAnnotation('destructiveHint', e.target.checked)}
|
|
className="w-4 h-4 rounded border-gray-600 bg-gray-800 text-red-500 focus:ring-red-500/30"
|
|
/>
|
|
<div>
|
|
<span className="text-sm text-gray-200">Destructive</span>
|
|
<p className="text-[11px] text-gray-500">Tool may delete or modify data irreversibly</p>
|
|
</div>
|
|
</label>
|
|
<label className="flex items-center gap-3 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={tool.annotations?.idempotentHint ?? false}
|
|
onChange={(e) => updateAnnotation('idempotentHint', e.target.checked)}
|
|
className="w-4 h-4 rounded border-gray-600 bg-gray-800 text-emerald-500 focus:ring-emerald-500/30"
|
|
/>
|
|
<div>
|
|
<span className="text-sm text-gray-200">Idempotent</span>
|
|
<p className="text-[11px] text-gray-500">Same call can be repeated safely</p>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Danger Zone */}
|
|
<section className="px-4 py-4">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<AlertTriangle className="w-3.5 h-3.5 text-red-400" />
|
|
<h3 className="text-xs font-medium text-red-400 uppercase tracking-wider">
|
|
Danger Zone
|
|
</h3>
|
|
</div>
|
|
<button
|
|
onClick={() => onDelete(tool.name)}
|
|
className="flex items-center gap-2 w-full px-3 py-2 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm hover:bg-red-500/20 transition-colors"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
Delete Tool
|
|
</button>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|