flow / src /lib /agentDocs.ts
mfn
Your own agent: hosted relay by default (api.mrfake.name/relay), MCP over HTTP snippets, local relay moved to Advanced
e5b3e51
Raw History Blame Contribute Delete
7.81 kB
import { TOOLS } from "./copilotTools";
/** Lifetime of a bridge token. Short on purpose: it grants full control of the canvas. */
export const TOKEN_TTL_MS = 4 * 60 * 60 * 1000;
export const HOSTED_BRIDGE_URL = "https://api.mrfake.name/relay";
export const DEFAULT_BRIDGE_URL = (import.meta.env.VITE_BRIDGE_URL as string | undefined) || HOSTED_BRIDGE_URL;
/** Default of older builds (local relay); saved settings with it are moved to the hosted relay. */
export const LEGACY_BRIDGE_URL = "http://127.0.0.1:7433";
/** The hosted relay speaks MCP over HTTP at /mcp; a local gr-canvas-bridge.mjs speaks MCP on stdio. */
export const isHostedRelay = (url: string) => url.replace(/\/+$/, "") === DEFAULT_BRIDGE_URL || url.replace(/\/+$/, "") === HOSTED_BRIDGE_URL;
export const appOrigin = () => (typeof location !== "undefined" ? location.origin : "https://gr-canvas.local");
export const docsUrl = () => `${appOrigin()}/llms.txt`;
export const bridgeScriptUrl = () => `${appOrigin()}/bridge/gr-canvas-bridge.mjs`;
function schemaLine(name: string, schema: Record<string, unknown> | undefined, required: string[]): string {
const props = (schema?.properties ?? {}) as Record<string, { type?: string; description?: string; default?: unknown }>;
const keys = Object.keys(props);
if (!keys.length) return ` ${name}()`;
const args = keys.map((k) => {
const p = props[k];
const req = required.includes(k) ? "" : "?";
const desc = p.description ? ` β€” ${p.description}` : "";
return ` ${k}${req}: ${p.type ?? "any"}${desc}`;
});
return ` ${name}(\n${args.join("\n")}\n )`;
}
/** Plain-text tool reference generated from the live tool definitions, so it never drifts. */
export function toolReference(): string {
return Object.values(TOOLS)
.map((t) => {
const f = t.spec.function;
const params = f.parameters as { properties?: Record<string, unknown>; required?: string[] } | undefined;
return `${schemaLine(f.name, params as Record<string, unknown>, params?.required ?? [])}\n [${t.kind}] ${f.description}`;
})
.join("\n\n");
}
/** Tool manifest sent to the relay (and exposed as /tools and MCP tools/list). */
export function toolManifest() {
return Object.values(TOOLS).map((t) => ({ name: t.spec.function.name, description: t.spec.function.description, kind: t.kind, parameters: t.spec.function.parameters }));
}
export interface DocsOpts {
bridgeUrl: string;
token: string;
expires: number;
}
/** The whole guide an external agent needs: how to connect + tool reference. */
export function agentDocs(o: DocsOpts): string {
const ttl = Math.max(0, Math.round((o.expires - Date.now()) / 60000));
return `# Flow β€” drive the canvas from your own agent
Flow is a node editor where any public Gradio Space on the Hugging Face Hub is a node.
A workflow is a graph: input nodes (in:text, in:image, in:audio, in:video, in:file, in:number,
in:toggle) feed values into Space endpoint nodes (space:owner/name/endpoint). Links go from an
output port to an input port with a compatible type (image, audio, video, file, text, number,
boolean, gallery, json; "any" matches anything). Outputs flow between Spaces as URLs.
You control the canvas that is open in the user's browser tab through a relay: the tab keeps a
connection open to it, and your calls are forwarded into the tab. Keep using the same token; the
tab must stay open.
## Connect
Relay: ${o.bridgeUrl}
Token: ${o.token} (expires in ~${ttl} min; the user can rotate it in Flow β†’ Copilot β†’ Your own agent)
Docs: ${docsUrl()}
Every request carries the token: Authorization: Bearer ${o.token}
GET ${o.bridgeUrl}/status β†’ {"ok":true,"tab":true,"tools":${Object.keys(TOOLS).length}} (tab=false means the user has not connected the tab)
GET ${o.bridgeUrl}/tools β†’ JSON tool list with JSON-schema parameters
POST ${o.bridgeUrl}/call body {"name":"get_workflow","arguments":{}}
β†’ {"ok":true,"result":...} or {"ok":false,"error":"..."}
${isHostedRelay(o.bridgeUrl) ? ` POST ${o.bridgeUrl}/mcp MCP (Streamable HTTP, JSON-RPC): initialize, tools/list, tools/call, ping
GET ${o.bridgeUrl}/llms.txt this guide
Slow tools (wait_for_run, run-related calls) can take minutes: keep the request open (up to ~10 min).
` : ""}
Example:
curl -s ${o.bridgeUrl}/call -H "Authorization: Bearer ${o.token}" -H "content-type: application/json" \\
-d '{"name":"search_spaces","arguments":{"query":"remove background","limit":5}}'
## How to work
1. Call get_workflow first so you know the current node ids and which Spaces are loaded.
2. To use a Space: search_spaces β†’ pick one that is running (usable=true, most likes) β†’ add_space to load
its endpoints. Read the returned ports before adding nodes.
3. Build with add_node and connect, using the ids you got back. Set sensible widget values with set_values.
Then call auto_layout so the graph reads left to right with nothing overlapping; use move_nodes for
fine placement (positions and sizes come from get_workflow; x grows right, y grows down).
4. Only call run_workflow when the user asked for a run: it spends their Hugging Face quota. Then call
wait_for_run and report the outcome.
5. If something fails, read get_run_status (log + per-node errors) and get_node on the failing node, and
quote the actual error text.
6. Node types look like "space:owner/name/endpoint" or "in:text". Never invent types; use ones returned by tools.
7. The user watches the canvas live. Edits are applied immediately, without confirmation, so keep them deliberate.
## Tools
${toolReference()}
`;
}
/** Short prompt to paste into any chat agent. */
export function agentPrompt(o: DocsOpts): string {
return `You can control my Flow workflow canvas (a node editor for Hugging Face Gradio Spaces).
Read the guide first: ${docsUrl()}
Then call tools over HTTP through the relay at ${o.bridgeUrl} with header "Authorization: Bearer ${o.token}":
GET /status, GET /tools, and POST /call with {"name": "<tool>", "arguments": {...}}.
Start with get_workflow. Only run_workflow when I ask (it costs my quota), then wait_for_run.`;
}
/** Claude Code / Cursor / Claude Desktop style MCP config. */
export function mcpConfig(o: DocsOpts, scriptPath: string): string {
if (isHostedRelay(o.bridgeUrl))
return JSON.stringify({ mcpServers: { flow: { type: "http", url: `${o.bridgeUrl}/mcp`, headers: { Authorization: `Bearer ${o.token}` } } } }, null, 2);
return JSON.stringify(
{
mcpServers: {
"flow": {
command: "node",
args: [scriptPath, "--mcp", "--port", String(portOf(o.bridgeUrl))],
env: { GR_CANVAS_TOKEN: o.token },
},
},
},
null,
2,
);
}
/** One-line `claude mcp add` for Claude Code. */
export function claudeCodeCommand(o: DocsOpts, scriptPath: string): string {
if (isHostedRelay(o.bridgeUrl)) return `claude mcp add --transport http flow ${o.bridgeUrl}/mcp --header "Authorization: Bearer ${o.token}"`;
return `claude mcp add flow -e GR_CANVAS_TOKEN=${o.token} -- node ${scriptPath} --mcp --port ${portOf(o.bridgeUrl)}`;
}
export function curlExample(o: DocsOpts): string {
return `curl -s ${o.bridgeUrl}/call -H "Authorization: Bearer ${o.token}" -H "content-type: application/json" \\\n -d '{"name":"get_workflow","arguments":{}}'`;
}
export function portOf(url: string): number {
try {
const u = new URL(url);
return Number(u.port) || (u.protocol === "https:" ? 443 : 80);
} catch {
return 7433;
}
}
export function newToken(): string {
const b = new Uint8Array(24);
crypto.getRandomValues(b);
return "grc_" + btoa(String.fromCharCode(...b)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}