Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions corpus/frontend/reactflow/background.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Background
kind: component
description: >-
Renders different background types common in node-based UIs. Comes with three
variants: lines, dots, and cross.
importPath: "import { Background, BackgroundVariant } from '@xyflow/react'"
props:
- name: variant
type: BackgroundVariant
description: Background pattern type.
default: BackgroundVariant.Dots
- name: gap
type: "number | [number, number]"
description: Gap between pattern elements.
default: "20"
- name: size
type: number
description: Size of pattern dots/lines.
default: "1"
- name: color
type: string
description: Pattern color.
- name: lineWidth
type: number
description: Stroke width for lines/cross variant.
default: "1"
usage: |
<ReactFlow nodes={nodes} edges={edges}>
<Background variant={BackgroundVariant.Dots} gap={20} size={1} />
</ReactFlow>
examples:
- title: Cross pattern background
category: styling
code: |
import { Background, BackgroundVariant } from '@xyflow/react';

<Background variant={BackgroundVariant.Cross} gap={24} size={2} color="#ddd" />
relatedApis:
- ReactFlow
- MiniMap
- Controls
46 changes: 46 additions & 0 deletions corpus/frontend/reactflow/controls.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Controls
kind: component
description: >-
Renders a small panel with zoom in, zoom out, fit view, and lock viewport
buttons.
importPath: "import { Controls, ControlButton } from '@xyflow/react'"
props:
- name: showZoom
type: boolean
description: Show zoom buttons.
default: "true"
- name: showFitView
type: boolean
description: Show fit view button.
default: "true"
- name: showInteractive
type: boolean
description: Show lock button.
default: "true"
- name: position
type: PanelPosition
description: Corner position.
default: "'bottom-left'"
- name: orientation
type: "'horizontal' | 'vertical'"
description: Layout direction.
default: "'vertical'"
usage: |
<ReactFlow nodes={nodes} edges={edges}>
<Controls position="bottom-left" />
</ReactFlow>
examples:
- title: Custom control button
category: interaction
code: |
import { Controls, ControlButton } from '@xyflow/react';

<Controls>
<ControlButton onClick={() => console.log('custom action')}>
<Icon />
</ControlButton>
</Controls>
relatedApis:
- ReactFlow
- ControlButton
- Panel
71 changes: 71 additions & 0 deletions corpus/frontend/reactflow/handle.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Handle
kind: component
description: >-
Used in custom nodes to define connection points. Handles can be sources
(outgoing) or targets (incoming).
importPath: "import { Handle, Position } from '@xyflow/react'"
props:
- name: type
type: "'source' | 'target'"
description: Whether this is an input or output handle.
default: "'source'"
- name: position
type: Position
description: "Side of the node: Position.Top, Bottom, Left, Right."
default: Position.Top
- name: id
type: string
description: Handle ID, needed when a node has multiple handles of the same type.
- name: isConnectable
type: boolean
description: Whether connections can be made to/from this handle.
default: "true"
- name: isConnectableStart
type: boolean
description: Whether a connection can start from this handle.
default: "true"
- name: isConnectableEnd
type: boolean
description: Whether a connection can end on this handle.
default: "true"
- name: isValidConnection
type: IsValidConnection
description: Custom validation logic for connections to this handle.
- name: onConnect
type: OnConnect
description: Callback when connection is made to this handle.
usage: |
import { Handle, Position } from '@xyflow/react';

function CustomNode({ data }) {
return (
<>
<Handle type="target" position={Position.Left} />
<div>{data.label}</div>
<Handle type="source" position={Position.Right} />
</>
);
}
examples:
- title: Multiple handles
category: custom-nodes
code: |
function MultiHandleNode({ data }) {
return (
<div className="p-4 border rounded bg-white">
<Handle type="target" position={Position.Top} id="a" />
<Handle type="target" position={Position.Left} id="b" />
<div>{data.label}</div>
<Handle type="source" position={Position.Bottom} id="c" />
<Handle type="source" position={Position.Right} id="d" />
</div>
);
}
tips:
- Use the id prop when a node has multiple source or target handles.
- Prefer isValidConnection on <ReactFlow> over per-handle validation for performance.
- Add 'nodrag' class to interactive elements inside a node to prevent drag when clicking them.
relatedApis:
- ReactFlow
- NodeResizer
- NodeToolbar
10 changes: 10 additions & 0 deletions corpus/frontend/reactflow/index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,13 @@ namespace: frontend.reactflow
apis:
reactflow:
file: reactflow.yaml
handle:
file: handle.yaml
background:
file: background.yaml
controls:
file: controls.yaml
minimap:
file: minimap.yaml
usereactflow:
file: usereactflow.yaml
41 changes: 41 additions & 0 deletions corpus/frontend/reactflow/minimap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: MiniMap
kind: component
description: >-
Renders an overview of your flow. Each node appears as an SVG element showing
the current viewport position relative to the full flow.
importPath: "import { MiniMap } from '@xyflow/react'"
props:
- name: nodeColor
type: "string | ((node: Node) => string)"
description: Color of minimap nodes.
- name: nodeStrokeColor
type: "string | ((node: Node) => string)"
description: Stroke color of minimap nodes.
- name: nodeStrokeWidth
type: number
description: Stroke width.
default: "2"
- name: maskColor
type: string
description: Color of the area outside the viewport.
- name: position
type: PanelPosition
description: Corner position.
default: "'bottom-right'"
- name: pannable
type: boolean
description: Allow panning via minimap.
default: "false"
- name: zoomable
type: boolean
description: Allow zooming via minimap.
default: "false"
usage: |
<ReactFlow nodes={nodes} edges={edges}>
<MiniMap nodeColor={(n) => n.type === 'input' ? '#6366f1' : '#94a3b8'} pannable zoomable />
</ReactFlow>
examples: []
relatedApis:
- ReactFlow
- Background
- Controls
48 changes: 48 additions & 0 deletions corpus/frontend/reactflow/usereactflow.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: useReactFlow
kind: hook
description: >-
Returns a ReactFlowInstance to update nodes/edges, manipulate the viewport,
or query flow state. Does NOT cause re-renders on state changes.
importPath: "import { useReactFlow } from '@xyflow/react'"
returns: ReactFlowInstance
usage: |
const { getNodes, setNodes, addNodes, getEdges, setEdges, addEdges,
fitView, zoomIn, zoomOut, getViewport, setViewport,
screenToFlowPosition, deleteElements, updateNode, updateNodeData,
getIntersectingNodes, toObject } = useReactFlow();
examples:
- title: Add node on button click
category: interaction
code: |
function AddNodeButton() {
const { addNodes, screenToFlowPosition } = useReactFlow();
const onClick = () => {
addNodes({
id: crypto.randomUUID(),
position: screenToFlowPosition({ x: 200, y: 200 }),
data: { label: 'New Node' },
});
};
return <button onClick={onClick}>Add Node</button>;
}
- title: Delete selected elements
category: interaction
code: |
function DeleteButton() {
const { deleteElements, getNodes, getEdges } = useReactFlow();
const onClick = async () => {
const selectedNodes = getNodes().filter((n) => n.selected);
const selectedEdges = getEdges().filter((e) => e.selected);
await deleteElements({ nodes: selectedNodes, edges: selectedEdges });
};
return <button onClick={onClick}>Delete Selected</button>;
}
tips:
- Must be used inside <ReactFlowProvider> or <ReactFlow>.
- Unlike useNodes/useEdges, this hook won't cause re-renders on state changes. Query state on demand.
- "Pass useReactFlow() as dependency to useCallback/useEffect - it's not initialized on first render."
relatedApis:
- ReactFlowProvider
- ReactFlowInstance
- useNodes
- useEdges
66 changes: 28 additions & 38 deletions src/plugins/reactflow/tools/get-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,7 @@ type CorpusIndex = {

type CorpusNamespaceIndex = {
namespace?: string;
apis?: {
reactflow?: {
file?: string;
};
};
apis?: Record<string, { file?: string }>;
};

type CorpusApiEntry = {
Expand All @@ -36,57 +32,63 @@ type CorpusApiEntry = {
relatedApis?: string[];
};

type LoadedCorpusApi = {
api: Parameters<typeof formatApiReference>[0];
source: string;
};

type ReactFlowExample = Parameters<typeof formatApiReference>[0]["examples"][number];

const moduleDir = dirname(fileURLToPath(import.meta.url));
const corpusRoot = join(moduleDir, "../../../../corpus");
const corpusNamespace = "frontend.reactflow";
const corpusApiName = "ReactFlow";
type ReactFlowExample = Parameters<typeof formatApiReference>[0]["examples"][number];

let cachedCorpusReactFlowApi: { api: Parameters<typeof formatApiReference>[0]; source: string } | null | undefined;
const cachedCorpusApis = new Map<string, LoadedCorpusApi | null>();

function normalizeApiName(name: string): string {
return name.toLowerCase().replace(/[<>/()]/g, "").trim();
}

function loadCorpusReactFlowApi(): { api: Parameters<typeof formatApiReference>[0]; source: string } | null {
if (cachedCorpusReactFlowApi !== undefined) {
return cachedCorpusReactFlowApi;
function loadCorpusApi(name: string): LoadedCorpusApi | null {
const key = normalizeApiName(name);
if (cachedCorpusApis.has(key)) {
return cachedCorpusApis.get(key) ?? null;
}

try {
const indexRaw = readFileSync(join(corpusRoot, "index.yaml"), "utf8");
const index = YAML.parse(indexRaw) as CorpusIndex | null;
const namespaceIndexPath = index?.namespaces?.[corpusNamespace]?.index;
if (!namespaceIndexPath) {
cachedCorpusReactFlowApi = null;
return cachedCorpusReactFlowApi;
cachedCorpusApis.set(key, null);
return null;
}

const namespaceRaw = readFileSync(join(corpusRoot, namespaceIndexPath), "utf8");
const namespaceIndex = YAML.parse(namespaceRaw) as CorpusNamespaceIndex | null;
const apiPath = namespaceIndex?.apis?.reactflow?.file;
const apiPath = namespaceIndex?.apis?.[key]?.file;
if (!apiPath) {
cachedCorpusReactFlowApi = null;
return cachedCorpusReactFlowApi;
cachedCorpusApis.set(key, null);
return null;
}

const apiRaw = readFileSync(join(corpusRoot, "frontend/reactflow", apiPath), "utf8");
const api = YAML.parse(apiRaw) as CorpusApiEntry | null;
if (
!api ||
normalizeApiName(api.name ?? "") !== normalizeApiName(corpusApiName) ||
normalizeApiName(api.name ?? "") !== key ||
!api.usage ||
!api.importPath ||
!api.description ||
!api.kind
) {
cachedCorpusReactFlowApi = null;
return cachedCorpusReactFlowApi;
cachedCorpusApis.set(key, null);
return null;
}

cachedCorpusReactFlowApi = {
const loaded: LoadedCorpusApi = {
api: {
name: api.name ?? corpusApiName,
name: api.name ?? name,
kind: api.kind as Parameters<typeof formatApiReference>[0]["kind"],
description: api.description,
importPath: api.importPath,
Expand All @@ -108,26 +110,14 @@ function loadCorpusReactFlowApi(): { api: Parameters<typeof formatApiReference>[
},
source: corpusNamespace,
};
return cachedCorpusReactFlowApi ?? null;
cachedCorpusApis.set(key, loaded);
return loaded;
} catch {
cachedCorpusReactFlowApi = null;
cachedCorpusApis.set(key, null);
return null;
}
}

function getReactFlowApi(name: string) {
if (normalizeApiName(name) !== normalizeApiName(corpusApiName)) {
return getApiByName(name, ALL_APIS);
}

const corpusEntry = loadCorpusReactFlowApi();
if (corpusEntry) {
return corpusEntry.api;
}

return getApiByName(name, ALL_APIS);
}

export function register(server: McpServer): void {
server.tool(
"reactflow_get_api",
Expand All @@ -140,8 +130,8 @@ export function register(server: McpServer): void {
),
},
async ({ name }) => {
const corpusEntry = normalizeApiName(name) === normalizeApiName(corpusApiName) ? loadCorpusReactFlowApi() : null;
const api = getReactFlowApi(name);
const corpusEntry = loadCorpusApi(name);
const api = corpusEntry?.api ?? getApiByName(name, ALL_APIS);
if (!api) {
const suggestions = searchApis(name, ALL_APIS)
.slice(0, 5)
Expand Down
Loading
Loading