add tab to modify mcp_config.json in front end
This commit is contained in:
@@ -4,10 +4,12 @@ import {
|
|||||||
deleteGraphConfig,
|
deleteGraphConfig,
|
||||||
getGraphConfig,
|
getGraphConfig,
|
||||||
getGraphDefaultConfig,
|
getGraphDefaultConfig,
|
||||||
|
getMcpToolConfig,
|
||||||
listAvailableGraphs,
|
listAvailableGraphs,
|
||||||
listGraphConfigs,
|
listGraphConfigs,
|
||||||
listPipelines,
|
listPipelines,
|
||||||
stopPipeline,
|
stopPipeline,
|
||||||
|
updateMcpToolConfig,
|
||||||
upsertGraphConfig,
|
upsertGraphConfig,
|
||||||
} from "./api/frontApis";
|
} from "./api/frontApis";
|
||||||
import type {
|
import type {
|
||||||
@@ -37,6 +39,8 @@ type LaunchCredentials = {
|
|||||||
authKeyMasked: string;
|
authKeyMasked: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ActiveTab = "agents" | "mcp";
|
||||||
|
|
||||||
const DEFAULT_ENTRY_POINT = "fastapi_server/server_dashscope.py";
|
const DEFAULT_ENTRY_POINT = "fastapi_server/server_dashscope.py";
|
||||||
const DEFAULT_LLM_NAME = "qwen-plus";
|
const DEFAULT_LLM_NAME = "qwen-plus";
|
||||||
const DEFAULT_PORT = 8100;
|
const DEFAULT_PORT = 8100;
|
||||||
@@ -118,6 +122,7 @@ function toEditable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const [activeTab, setActiveTab] = useState<ActiveTab>("agents");
|
||||||
const [graphs, setGraphs] = useState<string[]>([]);
|
const [graphs, setGraphs] = useState<string[]>([]);
|
||||||
const [configItems, setConfigItems] = useState<GraphConfigListItem[]>([]);
|
const [configItems, setConfigItems] = useState<GraphConfigListItem[]>([]);
|
||||||
const [running, setRunning] = useState<PipelineRunInfo[]>([]);
|
const [running, setRunning] = useState<PipelineRunInfo[]>([]);
|
||||||
@@ -126,6 +131,9 @@ export default function App() {
|
|||||||
const [editor, setEditor] = useState<EditableAgent | null>(null);
|
const [editor, setEditor] = useState<EditableAgent | null>(null);
|
||||||
const [statusMessage, setStatusMessage] = useState<string>("");
|
const [statusMessage, setStatusMessage] = useState<string>("");
|
||||||
const [launchCredentials, setLaunchCredentials] = useState<LaunchCredentials | null>(null);
|
const [launchCredentials, setLaunchCredentials] = useState<LaunchCredentials | null>(null);
|
||||||
|
const [mcpConfigPath, setMcpConfigPath] = useState<string>("");
|
||||||
|
const [mcpConfigRaw, setMcpConfigRaw] = useState<string>("");
|
||||||
|
const [mcpToolKeys, setMcpToolKeys] = useState<string[]>([]);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
const configKeySet = useMemo(
|
const configKeySet = useMemo(
|
||||||
@@ -208,6 +216,16 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}, [selectedId, configKeySet]);
|
}, [selectedId, configKeySet]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab !== "mcp") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mcpConfigRaw) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reloadMcpConfig().catch(() => undefined);
|
||||||
|
}, [activeTab]);
|
||||||
|
|
||||||
async function selectExisting(item: GraphConfigListItem): Promise<void> {
|
async function selectExisting(item: GraphConfigListItem): Promise<void> {
|
||||||
const id = makeAgentKey(item.pipeline_id, item.prompt_set_id);
|
const id = makeAgentKey(item.pipeline_id, item.prompt_set_id);
|
||||||
setSelectedId(id);
|
setSelectedId(id);
|
||||||
@@ -321,6 +339,37 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function reloadMcpConfig(): Promise<void> {
|
||||||
|
setBusy(true);
|
||||||
|
setStatusMessage("Loading MCP config...");
|
||||||
|
try {
|
||||||
|
const resp = await getMcpToolConfig();
|
||||||
|
setMcpConfigPath(resp.path || "");
|
||||||
|
setMcpConfigRaw(resp.raw_content || "");
|
||||||
|
setMcpToolKeys(resp.tool_keys || []);
|
||||||
|
setStatusMessage("MCP config loaded.");
|
||||||
|
} catch (error) {
|
||||||
|
setStatusMessage((error as Error).message);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveMcpConfig(): Promise<void> {
|
||||||
|
setBusy(true);
|
||||||
|
setStatusMessage("Saving MCP config...");
|
||||||
|
try {
|
||||||
|
const resp = await updateMcpToolConfig({ raw_content: mcpConfigRaw });
|
||||||
|
setMcpConfigPath(resp.path || "");
|
||||||
|
setMcpToolKeys(resp.tool_keys || []);
|
||||||
|
setStatusMessage("MCP config saved.");
|
||||||
|
} catch (error) {
|
||||||
|
setStatusMessage((error as Error).message);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveConfig(): Promise<void> {
|
async function saveConfig(): Promise<void> {
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
return;
|
return;
|
||||||
@@ -507,243 +556,303 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showSidebar = activeTab === "agents";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className={`app ${showSidebar ? "" : "full-width"}`}>
|
||||||
<aside className="sidebar">
|
{showSidebar ? (
|
||||||
<div className="sidebar-header">
|
<aside className="sidebar">
|
||||||
<h2>Agents</h2>
|
<div className="sidebar-header">
|
||||||
<button onClick={addDraftAgent} disabled={busy}>
|
<h2>Agents</h2>
|
||||||
+ New
|
<button onClick={addDraftAgent} disabled={busy}>
|
||||||
</button>
|
+ New
|
||||||
</div>
|
|
||||||
<div className="agent-list">
|
|
||||||
{rows.map((row) => (
|
|
||||||
<button
|
|
||||||
key={row.id}
|
|
||||||
className={`agent-item ${selectedId === row.id ? "selected" : ""}`}
|
|
||||||
onClick={() => {
|
|
||||||
if (row.isDraft) {
|
|
||||||
const selectedDraft = draftAgents.find((d) => d.id === row.id) || null;
|
|
||||||
setSelectedId(row.id);
|
|
||||||
setEditor(selectedDraft);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const item = visibleConfigItems.find(
|
|
||||||
(x) => makeAgentKey(x.pipeline_id, x.prompt_set_id) === row.id
|
|
||||||
);
|
|
||||||
if (item) {
|
|
||||||
selectExisting(item);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>{row.label}</span>
|
|
||||||
<small>{row.graphId}</small>
|
|
||||||
</button>
|
</button>
|
||||||
))}
|
</div>
|
||||||
{rows.length === 0 ? <p className="empty">No agents configured yet.</p> : null}
|
<div className="agent-list">
|
||||||
</div>
|
{rows.map((row) => (
|
||||||
</aside>
|
<button
|
||||||
|
key={row.id}
|
||||||
|
className={`agent-item ${selectedId === row.id ? "selected" : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (row.isDraft) {
|
||||||
|
const selectedDraft = draftAgents.find((d) => d.id === row.id) || null;
|
||||||
|
setSelectedId(row.id);
|
||||||
|
setEditor(selectedDraft);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const item = visibleConfigItems.find(
|
||||||
|
(x) => makeAgentKey(x.pipeline_id, x.prompt_set_id) === row.id
|
||||||
|
);
|
||||||
|
if (item) {
|
||||||
|
selectExisting(item);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{row.label}</span>
|
||||||
|
<small>{row.graphId}</small>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{rows.length === 0 ? <p className="empty">No agents configured yet.</p> : null}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<main className="content">
|
<main className="content">
|
||||||
<header className="content-header">
|
<header className="content-header">
|
||||||
<h1>Agent Configuration</h1>
|
<h1>Agent Manager</h1>
|
||||||
<div className="header-actions">
|
<div className="tabs">
|
||||||
<button onClick={saveConfig} disabled={busy || !editor}>
|
<button
|
||||||
Save
|
type="button"
|
||||||
|
className={`tab-button ${activeTab === "agents" ? "active" : ""}`}
|
||||||
|
onClick={() => setActiveTab("agents")}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
Agents
|
||||||
</button>
|
</button>
|
||||||
<button onClick={runSelected} disabled={busy || !editor}>
|
<button
|
||||||
Run
|
type="button"
|
||||||
</button>
|
className={`tab-button ${activeTab === "mcp" ? "active" : ""}`}
|
||||||
<button onClick={stopSelected} disabled={busy || !editor}>
|
onClick={() => setActiveTab("mcp")}
|
||||||
Stop
|
disabled={busy}
|
||||||
</button>
|
>
|
||||||
<button onClick={deleteSelected} disabled={busy || !editor}>
|
MCP Config
|
||||||
Delete
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{statusMessage ? <p className="status">{statusMessage}</p> : null}
|
{statusMessage ? <p className="status">{statusMessage}</p> : null}
|
||||||
{launchCredentials ? (
|
{activeTab === "agents" ? (
|
||||||
<div className="launch-credentials">
|
<div className="tab-pane">
|
||||||
<h3>Access Credentials (shown once)</h3>
|
<div className="header-actions">
|
||||||
<div>
|
<button onClick={saveConfig} disabled={busy || !editor}>
|
||||||
<strong>URL:</strong>{" "}
|
Save
|
||||||
<a href={launchCredentials.url} target="_blank" rel="noreferrer">
|
</button>
|
||||||
{launchCredentials.url}
|
<button onClick={runSelected} disabled={busy || !editor}>
|
||||||
</a>
|
Run
|
||||||
<button
|
</button>
|
||||||
type="button"
|
<button onClick={stopSelected} disabled={busy || !editor}>
|
||||||
onClick={() => copyText(launchCredentials.url, "URL")}
|
Stop
|
||||||
disabled={busy}
|
</button>
|
||||||
>
|
<button onClick={deleteSelected} disabled={busy || !editor}>
|
||||||
Copy URL
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<strong>{launchCredentials.authType} key:</strong> {launchCredentials.authKey}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => copyText(launchCredentials.authKey, "auth key")}
|
|
||||||
disabled={busy}
|
|
||||||
>
|
|
||||||
Copy Key
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>Header:</strong> <code>{authHeaderValue}</code>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => copyText(authHeaderValue, "auth header")}
|
|
||||||
disabled={busy}
|
|
||||||
>
|
|
||||||
Copy Header
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p className="empty">
|
|
||||||
Stored after launch as masked value: {launchCredentials.authKeyMasked}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!editor ? (
|
{launchCredentials ? (
|
||||||
<div className="empty-panel">
|
<div className="launch-credentials">
|
||||||
<p>Select an agent from the left or create a new one.</p>
|
<h3>Access Credentials (shown once)</h3>
|
||||||
</div>
|
<div>
|
||||||
) : (
|
<strong>URL:</strong>{" "}
|
||||||
<section className="form-grid">
|
<a href={launchCredentials.url} target="_blank" rel="noreferrer">
|
||||||
<label>
|
{launchCredentials.url}
|
||||||
Agent Type (graph_id)
|
</a>
|
||||||
<select
|
<button
|
||||||
value={editor.graphId}
|
type="button"
|
||||||
onChange={(e) => changeGraph(e.target.value)}
|
onClick={() => copyText(launchCredentials.url, "URL")}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
>
|
>
|
||||||
{graphs.map((graph) => (
|
Copy URL
|
||||||
<option key={graph} value={graph}>
|
</button>
|
||||||
{graph}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{graphArchImage && (
|
|
||||||
<div className="graph-arch-section">
|
|
||||||
<h3>Graph Architecture</h3>
|
|
||||||
<div className="graph-arch-image-container">
|
|
||||||
<img
|
|
||||||
src={graphArchImage}
|
|
||||||
alt={`${editor.graphId} architecture diagram`}
|
|
||||||
className="graph-arch-image"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>{launchCredentials.authType} key:</strong> {launchCredentials.authKey}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => copyText(launchCredentials.authKey, "auth key")}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
Copy Key
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Header:</strong> <code>{authHeaderValue}</code>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => copyText(authHeaderValue, "auth header")}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
Copy Header
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="empty">
|
||||||
|
Stored after launch as masked value: {launchCredentials.authKeyMasked}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : null}
|
||||||
|
|
||||||
<label>
|
{!editor ? (
|
||||||
pipeline_id
|
<div className="empty-panel">
|
||||||
<input
|
<p>Select an agent from the left or create a new one.</p>
|
||||||
value={editor.pipelineId}
|
</div>
|
||||||
onChange={(e) => updateEditor("pipelineId", e.target.value)}
|
) : (
|
||||||
placeholder="example: routing-agent-1"
|
<section className="form-grid">
|
||||||
disabled={busy}
|
<label>
|
||||||
/>
|
Agent Type (graph_id)
|
||||||
</label>
|
<select
|
||||||
|
value={editor.graphId}
|
||||||
|
onChange={(e) => changeGraph(e.target.value)}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
{graphs.map((graph) => (
|
||||||
|
<option key={graph} value={graph}>
|
||||||
|
{graph}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label>
|
{graphArchImage && (
|
||||||
prompt_set_id
|
<div className="graph-arch-section">
|
||||||
<input value={editor.promptSetId || "(assigned on save)"} readOnly />
|
<h3>Graph Architecture</h3>
|
||||||
</label>
|
<div className="graph-arch-image-container">
|
||||||
|
<img
|
||||||
<label>
|
src={graphArchImage}
|
||||||
tool_keys (comma separated)
|
alt={`${editor.graphId} architecture diagram`}
|
||||||
<input
|
className="graph-arch-image"
|
||||||
value={editor.toolKeys.join(", ")}
|
/>
|
||||||
onChange={(e) => updateEditor("toolKeys", parseToolCsv(e.target.value))}
|
|
||||||
placeholder="tool_a, tool_b"
|
|
||||||
disabled={busy}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
port
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
value={editor.port}
|
|
||||||
onChange={(e) => updateEditor("port", Number(e.target.value))}
|
|
||||||
disabled={busy}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
api_key
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={editor.apiKey}
|
|
||||||
onChange={(e) => updateEditor("apiKey", e.target.value)}
|
|
||||||
placeholder="Enter provider API key"
|
|
||||||
disabled={busy}
|
|
||||||
/>
|
|
||||||
{editor.apiKey ? (
|
|
||||||
<small className="empty">Preview: {maskSecretPreview(editor.apiKey)}</small>
|
|
||||||
) : null}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
llm_name
|
|
||||||
<input
|
|
||||||
value={editor.llmName}
|
|
||||||
onChange={(e) => updateEditor("llmName", e.target.value)}
|
|
||||||
disabled={busy}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="prompt-section">
|
|
||||||
<h3>Prompts</h3>
|
|
||||||
{Object.keys(editor.prompts).length === 0 ? (
|
|
||||||
<p className="empty">No prompt keys returned from backend.</p>
|
|
||||||
) : (
|
|
||||||
Object.entries(editor.prompts).map(([key, value]) => (
|
|
||||||
<label key={key}>
|
|
||||||
{key}
|
|
||||||
<textarea
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => updatePrompt(key, e.target.value)}
|
|
||||||
rows={4}
|
|
||||||
disabled={busy}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="run-info">
|
|
||||||
<h3>Running Instances</h3>
|
|
||||||
{selectedRuns.length === 0 ? (
|
|
||||||
<p className="empty">No active runs for this agent.</p>
|
|
||||||
) : (
|
|
||||||
selectedRuns.map((run) => (
|
|
||||||
<div key={run.run_id} className="run-card">
|
|
||||||
<div>
|
|
||||||
<strong>run_id:</strong> {run.run_id}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>pid:</strong> {run.pid}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>url:</strong>{" "}
|
|
||||||
<a href={run.url} target="_blank" rel="noreferrer">
|
|
||||||
{run.url}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>auth:</strong> {run.auth_header_name} Bearer {run.auth_key_masked}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
)}
|
||||||
)}
|
|
||||||
|
<label>
|
||||||
|
pipeline_id
|
||||||
|
<input
|
||||||
|
value={editor.pipelineId}
|
||||||
|
onChange={(e) => updateEditor("pipelineId", e.target.value)}
|
||||||
|
placeholder="example: routing-agent-1"
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
prompt_set_id
|
||||||
|
<input value={editor.promptSetId || "(assigned on save)"} readOnly />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
tool_keys (comma separated)
|
||||||
|
<input
|
||||||
|
value={editor.toolKeys.join(", ")}
|
||||||
|
onChange={(e) => updateEditor("toolKeys", parseToolCsv(e.target.value))}
|
||||||
|
placeholder="tool_a, tool_b"
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
port
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={editor.port}
|
||||||
|
onChange={(e) => updateEditor("port", Number(e.target.value))}
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
api_key
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={editor.apiKey}
|
||||||
|
onChange={(e) => updateEditor("apiKey", e.target.value)}
|
||||||
|
placeholder="Enter provider API key"
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
{editor.apiKey ? (
|
||||||
|
<small className="empty">Preview: {maskSecretPreview(editor.apiKey)}</small>
|
||||||
|
) : null}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
llm_name
|
||||||
|
<input
|
||||||
|
value={editor.llmName}
|
||||||
|
onChange={(e) => updateEditor("llmName", e.target.value)}
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="prompt-section">
|
||||||
|
<h3>Prompts</h3>
|
||||||
|
{Object.keys(editor.prompts).length === 0 ? (
|
||||||
|
<p className="empty">No prompt keys returned from backend.</p>
|
||||||
|
) : (
|
||||||
|
Object.entries(editor.prompts).map(([key, value]) => (
|
||||||
|
<label key={key}>
|
||||||
|
{key}
|
||||||
|
<textarea
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => updatePrompt(key, e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="run-info">
|
||||||
|
<h3>Running Instances</h3>
|
||||||
|
{selectedRuns.length === 0 ? (
|
||||||
|
<p className="empty">No active runs for this agent.</p>
|
||||||
|
) : (
|
||||||
|
selectedRuns.map((run) => (
|
||||||
|
<div key={run.run_id} className="run-card">
|
||||||
|
<div>
|
||||||
|
<strong>run_id:</strong> {run.run_id}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>pid:</strong> {run.pid}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>url:</strong>{" "}
|
||||||
|
<a href={run.url} target="_blank" rel="noreferrer">
|
||||||
|
{run.url}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>auth:</strong> {run.auth_header_name} Bearer {run.auth_key_masked}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<section className="mcp-config-section tab-pane">
|
||||||
|
<div className="mcp-config-header">
|
||||||
|
<h3>Edit MCP Tool Options</h3>
|
||||||
|
<div className="header-actions">
|
||||||
|
<button type="button" onClick={reloadMcpConfig} disabled={busy}>
|
||||||
|
Reload
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={saveMcpConfig} disabled={busy}>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="empty">
|
||||||
|
This tab edits <code>configs/mcp_config.json</code> directly (comments supported).
|
||||||
|
</p>
|
||||||
|
{mcpConfigPath ? (
|
||||||
|
<p className="empty">
|
||||||
|
File: <code>{mcpConfigPath}</code>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<p className="empty">
|
||||||
|
Tool options detected: {mcpToolKeys.length ? mcpToolKeys.join(", ") : "(none)"}
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
className="mcp-config-editor"
|
||||||
|
value={mcpConfigRaw}
|
||||||
|
onChange={(e) => setMcpConfigRaw(e.target.value)}
|
||||||
|
rows={18}
|
||||||
|
spellCheck={false}
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import type {
|
|||||||
GraphConfigReadResponse,
|
GraphConfigReadResponse,
|
||||||
GraphConfigUpsertRequest,
|
GraphConfigUpsertRequest,
|
||||||
GraphConfigUpsertResponse,
|
GraphConfigUpsertResponse,
|
||||||
|
McpToolConfigResponse,
|
||||||
|
McpToolConfigUpdateRequest,
|
||||||
|
McpToolConfigUpdateResponse,
|
||||||
PipelineCreateRequest,
|
PipelineCreateRequest,
|
||||||
PipelineCreateResponse,
|
PipelineCreateResponse,
|
||||||
PipelineListResponse,
|
PipelineListResponse,
|
||||||
@@ -85,6 +88,19 @@ export function deleteGraphConfig(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMcpToolConfig(): Promise<McpToolConfigResponse> {
|
||||||
|
return fetchJson("/v1/tool-configs/mcp");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateMcpToolConfig(
|
||||||
|
payload: McpToolConfigUpdateRequest
|
||||||
|
): Promise<McpToolConfigUpdateResponse> {
|
||||||
|
return fetchJson("/v1/tool-configs/mcp", {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function createPipeline(
|
export function createPipeline(
|
||||||
payload: PipelineCreateRequest
|
payload: PipelineCreateRequest
|
||||||
): Promise<PipelineCreateResponse> {
|
): Promise<PipelineCreateResponse> {
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ body {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app.full-width {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
border-right: 1px solid #dbe2ea;
|
border-right: 1px solid #dbe2ea;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
@@ -94,6 +98,21 @@ button:disabled {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button.active {
|
||||||
|
background: #edf3ff;
|
||||||
|
border-color: #4d7ef3;
|
||||||
|
color: #1a4fc5;
|
||||||
|
}
|
||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -139,6 +158,10 @@ button:disabled {
|
|||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab-pane {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.form-grid {
|
.form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
@@ -206,6 +229,35 @@ button:disabled {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mcp-config-section {
|
||||||
|
background: #f7fbff;
|
||||||
|
border: 1px solid #d7e6f6;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-config-header {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-config-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-config-editor {
|
||||||
|
border: 1px solid #c9d4e2;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
resize: vertical;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
color: #687788;
|
color: #687788;
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
|
|||||||
@@ -85,3 +85,19 @@ export type PipelineStopResponse = {
|
|||||||
status: string;
|
status: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type McpToolConfigResponse = {
|
||||||
|
path: string;
|
||||||
|
raw_content: string;
|
||||||
|
tool_keys: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type McpToolConfigUpdateRequest = {
|
||||||
|
raw_content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type McpToolConfigUpdateResponse = {
|
||||||
|
status: string;
|
||||||
|
path: string;
|
||||||
|
tool_keys: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user