Jake Shore 96e52666c5 MCPEngine full sync — studio scaffold, factory v2, server updates, state.json — 2026-02-12
=== 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
2026-02-12 17:58:33 -05:00

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>
);
}