Files
ollama-personal-trainer/studio-ref/src/components/MCPHarnessStudio.tsx

302 lines
13 KiB
TypeScript

import React, { useState } from "react";
import {
Wrench,
Plus,
Trash2,
Play,
Sparkles,
Code2,
Terminal,
ArrowRight,
RefreshCw,
} from "lucide-react";
import { MCPToolDeclaration, TrainingDataSample } from "../types";
interface MCPHarnessStudioProps {
mcpTools: MCPToolDeclaration[];
setMcpTools: React.Dispatch<React.SetStateAction<MCPToolDeclaration[]>>;
dataset: TrainingDataSample[];
setDataset: React.Dispatch<React.SetStateAction<TrainingDataSample[]>>;
onProceed: () => void;
}
export const MCPHarnessStudio: React.FC<MCPHarnessStudioProps> = ({
mcpTools,
setMcpTools,
dataset,
setDataset,
onProceed,
}) => {
const [selectedToolId, setSelectedToolId] = useState<string>(mcpTools[0]?.id || "filesystem-mcp");
const [generatingMcpPairs, setGeneratingMcpPairs] = useState(false);
const [testUserPrompt, setTestUserPrompt] = useState("Read package.json and summarize our frontend dependencies.");
const [harnessOutput, setHarnessOutput] = useState<string | null>(null);
// New tool creator state
const [newToolName, setNewToolName] = useState("");
const [newServerName, setNewServerName] = useState("");
const [newToolDesc, setNewToolDesc] = useState("");
const [newToolSchemaJson, setNewToolSchemaJson] = useState(`{
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search query or input parameter" }
},
"required": ["query"]
}`);
const activeTool = mcpTools.find((t) => t.id === selectedToolId) || mcpTools[0];
const handleAddTool = () => {
if (!newToolName.trim()) return;
try {
const parsedSchema = JSON.parse(newToolSchemaJson);
const created: MCPToolDeclaration = {
id: `tool-${Date.now()}`,
name: newToolName.trim(),
serverName: newServerName.trim() || "Custom MCP",
description: newToolDesc.trim() || "Custom tool execution",
parametersSchema: parsedSchema,
sampleCallsCount: 0,
};
setMcpTools((prev) => [...prev, created]);
setSelectedToolId(created.id);
setNewToolName("");
setNewToolDesc("");
} catch (e: any) {
console.error(e);
}
};
const handleDeleteTool = (id: string) => {
setMcpTools((prev) => prev.filter((t) => t.id !== id));
if (selectedToolId === id && mcpTools.length > 1) {
setSelectedToolId(mcpTools.find((t) => t.id !== id)!.id);
}
};
const handleGenerateMCPDataWithAI = async () => {
setGeneratingMcpPairs(true);
try {
const res = await fetch("/api/dataset/generate-mcp", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
mcpServers: mcpTools,
count: 6,
complexity: "advanced",
}),
});
const data = await res.json();
if (data.success && Array.isArray(data.data)) {
const newSamples: TrainingDataSample[] = data.data.map((item: any) => ({
id: `mcp-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`,
instruction: item.userQuery,
output: `<tool_call>\n${JSON.stringify(item.toolCalls?.[0] || {}, null, 2)}\n</tool_call>\n\n${item.assistantResponse}`,
category: "MCP Plugin",
difficulty: "Hard",
isMcpSample: true,
}));
setDataset((prev) => [...newSamples, ...prev]);
}
} catch (e: any) {
console.error(e);
} finally {
setGeneratingMcpPairs(false);
}
};
const handleRunHarnessSimulation = () => {
setHarnessOutput("Simulating MCP execution harness...");
setTimeout(() => {
setHarnessOutput(`[MCP HARNESS] Matched Tool: ${activeTool.name} (${activeTool.serverName})
[PAYLOAD EMITTED] { "path": "package.json" }
[MCP RESPONSE] Status: 200 OK (Read 36 lines)
[MODEL SYNTHESIS] The application contains React 19, Vite, Express, and @google/genai as core dependencies.`);
}, 600);
};
return (
<div className="space-y-6">
{/* Header Banner */}
<div className="bg-[#18181b]/50 border border-[#27272a] rounded-xl p-5 flex flex-wrap items-center justify-between gap-4">
<div className="max-w-2xl">
<div className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded text-[10px] font-mono text-blue-400 bg-blue-400/10 border border-blue-400/20 mb-2">
<Wrench className="w-3.5 h-3.5 text-blue-400" /> MCP (MODEL CONTEXT PROTOCOL) HARNESS
</div>
<h2 className="text-lg font-semibold text-[#f4f4f5]">
Train Local Models for Flawless MCP Plugin Execution
</h2>
<p className="text-xs text-zinc-400 mt-1 leading-relaxed">
Configure MCP server tool definitions (Filesystem, Postgres, Web Search, Terminal, GitHub) and auto-generate multi-turn function call datasets so your fine-tuned Ollama model executes tools in Cline, Cursor, Windsurf, and Claude Desktop.
</p>
</div>
<button
onClick={handleGenerateMCPDataWithAI}
disabled={generatingMcpPairs}
className="flex items-center gap-2 px-4 py-2 rounded text-xs font-medium bg-blue-600 hover:bg-blue-500 text-white shadow-sm transition-all cursor-pointer disabled:opacity-50"
>
{generatingMcpPairs ? (
<>
<RefreshCw className="w-3.5 h-3.5 animate-spin" />
<span>Generating MCP Pairs...</span>
</>
) : (
<>
<Sparkles className="w-3.5 h-3.5" />
<span>Auto-Synthesize Tool Pairs (AI)</span>
</>
)}
</button>
</div>
{/* Main Grid: Tool Registry + Schema & Harness Playground */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left: Registered MCP Tools */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-[10px] font-mono font-bold text-zinc-500 uppercase tracking-wider">
Active MCP Tools ({mcpTools.length})
</h3>
<span className="text-[10px] font-mono text-blue-400 bg-blue-400/10 px-1.5 py-0.5 rounded border border-blue-400/20">
JSON SCHEMA
</span>
</div>
<div className="space-y-1.5 max-h-[500px] overflow-y-auto pr-1">
{mcpTools.map((tool) => {
const isSelected = selectedToolId === tool.id;
return (
<div
key={tool.id}
onClick={() => setSelectedToolId(tool.id)}
className={`p-3 rounded-lg border transition-all cursor-pointer ${
isSelected
? "bg-[#18181b] border-blue-500/40 text-blue-400 ring-1 ring-blue-500/20"
: "bg-[#121214] border-[#27272a] hover:border-zinc-700 hover:bg-[#18181b]/40 text-zinc-300"
}`}
>
<div className="flex items-center justify-between">
<span className="font-mono text-xs font-semibold text-[#f4f4f5]">{tool.name}</span>
<span className="text-[10px] font-mono text-zinc-400 bg-zinc-900 px-1.5 py-0.5 rounded border border-zinc-800">
{tool.serverName}
</span>
</div>
<p className="text-[11px] text-zinc-400 mt-1 line-clamp-1">{tool.description}</p>
</div>
);
})}
</div>
{/* Add New Tool Card */}
<div className="bg-[#18181b]/50 border border-[#27272a] rounded-xl p-4 space-y-2.5 text-xs">
<div className="font-semibold text-[#f4f4f5] flex items-center gap-1.5 text-xs">
<Plus className="w-3.5 h-3.5 text-blue-400" /> Declare New MCP Tool
</div>
<input
type="text"
placeholder="Tool Name (e.g. docker_container_exec)"
value={newToolName}
onChange={(e) => setNewToolName(e.target.value)}
className="w-full bg-zinc-950 border border-zinc-800 rounded px-2.5 py-1.5 text-xs text-zinc-200 placeholder-zinc-600 focus:ring-1 focus:ring-blue-600 outline-none"
/>
<input
type="text"
placeholder="Server (e.g. Docker MCP Server)"
value={newServerName}
onChange={(e) => setNewServerName(e.target.value)}
className="w-full bg-zinc-950 border border-zinc-800 rounded px-2.5 py-1.5 text-xs text-zinc-200 placeholder-zinc-600 focus:ring-1 focus:ring-blue-600 outline-none"
/>
<input
type="text"
placeholder="Description for model prompt"
value={newToolDesc}
onChange={(e) => setNewToolDesc(e.target.value)}
className="w-full bg-zinc-950 border border-zinc-800 rounded px-2.5 py-1.5 text-xs text-zinc-200 placeholder-zinc-600 focus:ring-1 focus:ring-blue-600 outline-none"
/>
<button
onClick={handleAddTool}
className="w-full py-1.5 rounded text-xs font-medium bg-zinc-900 hover:bg-zinc-800 text-zinc-300 border border-zinc-800 cursor-pointer"
>
Add Tool to Training Pipeline
</button>
</div>
</div>
{/* Right: Active Tool Schema & Test Harness Simulator */}
<div className="lg:col-span-2 space-y-6">
{activeTool && (
<div className="bg-[#18181b]/50 border border-[#27272a] rounded-xl p-5 space-y-4">
<div className="flex items-center justify-between border-b border-[#27272a] pb-3">
<div>
<div className="text-[10px] text-blue-400 font-mono uppercase">{activeTool.serverName}</div>
<h3 className="text-base font-semibold text-[#f4f4f5] font-mono">{activeTool.name}</h3>
</div>
<button
onClick={() => handleDeleteTool(activeTool.id)}
className="text-zinc-500 hover:text-rose-400 p-1.5 rounded border border-transparent hover:border-zinc-700 cursor-pointer"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
<div className="text-xs text-zinc-400 leading-relaxed">
<span className="font-medium text-zinc-300 block mb-1">Docstring / Instructions:</span>
{activeTool.description}
</div>
{/* JSON Schema */}
<div>
<div className="text-xs font-medium text-zinc-400 mb-1.5 flex items-center gap-1.5">
<Code2 className="w-3.5 h-3.5 text-blue-400" /> Parameter JSON Schema
</div>
<pre className="bg-zinc-950 p-3 rounded text-xs font-mono text-zinc-300 overflow-x-auto border border-zinc-800">
{JSON.stringify(activeTool.parametersSchema, null, 2)}
</pre>
</div>
{/* Interactive MCP Test Harness Simulator */}
<div className="bg-black/30 p-4 rounded border border-zinc-800 space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-[#f4f4f5] flex items-center gap-1.5">
<Terminal className="w-3.5 h-3.5 text-blue-400" /> Interactive Harness Verification
</span>
<button
onClick={handleRunHarnessSimulation}
className="flex items-center gap-1 px-3 py-1 rounded text-xs font-medium bg-blue-600 hover:bg-blue-500 text-white cursor-pointer shadow-sm"
>
<Play className="w-3 h-3" /> Test Trigger
</button>
</div>
<input
type="text"
value={testUserPrompt}
onChange={(e) => setTestUserPrompt(e.target.value)}
className="w-full bg-zinc-950 border border-zinc-800 rounded px-2.5 py-1.5 text-xs text-zinc-200 focus:ring-1 focus:ring-blue-600 outline-none font-mono"
/>
{harnessOutput && (
<pre className="p-3 bg-zinc-950 rounded text-[11px] font-mono text-emerald-400 border border-zinc-800 whitespace-pre-wrap leading-relaxed">
{harnessOutput}
</pre>
)}
</div>
</div>
)}
<div className="flex justify-end">
<button
onClick={onProceed}
className="flex items-center gap-2 px-4 py-2 rounded text-xs font-medium bg-blue-600 hover:bg-blue-500 text-white shadow-sm transition-all cursor-pointer"
>
<span>Proceed to Model-to-Model Distillation</span>
<ArrowRight className="w-3.5 h-3.5" />
</button>
</div>
</div>
</div>
</div>
);
};