diff --git a/corpus/frontend/reactflow/background.yaml b/corpus/frontend/reactflow/background.yaml
new file mode 100644
index 0000000..9bd785a
--- /dev/null
+++ b/corpus/frontend/reactflow/background.yaml
@@ -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: |
+
+
+
+examples:
+ - title: Cross pattern background
+ category: styling
+ code: |
+ import { Background, BackgroundVariant } from '@xyflow/react';
+
+
+relatedApis:
+ - ReactFlow
+ - MiniMap
+ - Controls
diff --git a/corpus/frontend/reactflow/controls.yaml b/corpus/frontend/reactflow/controls.yaml
new file mode 100644
index 0000000..aed07c1
--- /dev/null
+++ b/corpus/frontend/reactflow/controls.yaml
@@ -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: |
+
+
+
+examples:
+ - title: Custom control button
+ category: interaction
+ code: |
+ import { Controls, ControlButton } from '@xyflow/react';
+
+
+ console.log('custom action')}>
+
+
+
+relatedApis:
+ - ReactFlow
+ - ControlButton
+ - Panel
diff --git a/corpus/frontend/reactflow/handle.yaml b/corpus/frontend/reactflow/handle.yaml
new file mode 100644
index 0000000..b2b57f9
--- /dev/null
+++ b/corpus/frontend/reactflow/handle.yaml
@@ -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 (
+ <>
+
+
{data.label}
+
+ >
+ );
+ }
+examples:
+ - title: Multiple handles
+ category: custom-nodes
+ code: |
+ function MultiHandleNode({ data }) {
+ return (
+
+ );
+ }
+tips:
+ - Use the id prop when a node has multiple source or target handles.
+ - Prefer isValidConnection on 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
diff --git a/corpus/frontend/reactflow/index.yaml b/corpus/frontend/reactflow/index.yaml
index b005965..d3bb54e 100644
--- a/corpus/frontend/reactflow/index.yaml
+++ b/corpus/frontend/reactflow/index.yaml
@@ -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
diff --git a/corpus/frontend/reactflow/minimap.yaml b/corpus/frontend/reactflow/minimap.yaml
new file mode 100644
index 0000000..9c9d435
--- /dev/null
+++ b/corpus/frontend/reactflow/minimap.yaml
@@ -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: |
+
+ n.type === 'input' ? '#6366f1' : '#94a3b8'} pannable zoomable />
+
+examples: []
+relatedApis:
+ - ReactFlow
+ - Background
+ - Controls
diff --git a/corpus/frontend/reactflow/usereactflow.yaml b/corpus/frontend/reactflow/usereactflow.yaml
new file mode 100644
index 0000000..f1f7a29
--- /dev/null
+++ b/corpus/frontend/reactflow/usereactflow.yaml
@@ -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 ;
+ }
+ - 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 ;
+ }
+tips:
+ - Must be used inside or .
+ - 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
diff --git a/src/plugins/reactflow/tools/get-api.ts b/src/plugins/reactflow/tools/get-api.ts
index 0bc15ec..8c99119 100644
--- a/src/plugins/reactflow/tools/get-api.ts
+++ b/src/plugins/reactflow/tools/get-api.ts
@@ -16,11 +16,7 @@ type CorpusIndex = {
type CorpusNamespaceIndex = {
namespace?: string;
- apis?: {
- reactflow?: {
- file?: string;
- };
- };
+ apis?: Record;
};
type CorpusApiEntry = {
@@ -36,21 +32,27 @@ type CorpusApiEntry = {
relatedApis?: string[];
};
+type LoadedCorpusApi = {
+ api: Parameters[0];
+ source: string;
+};
+
+type ReactFlowExample = Parameters[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[0]["examples"][number];
-let cachedCorpusReactFlowApi: { api: Parameters[0]; source: string } | null | undefined;
+const cachedCorpusApis = new Map();
function normalizeApiName(name: string): string {
return name.toLowerCase().replace(/[<>/()]/g, "").trim();
}
-function loadCorpusReactFlowApi(): { api: Parameters[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 {
@@ -58,35 +60,35 @@ function loadCorpusReactFlowApi(): { api: Parameters[
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[0]["kind"],
description: api.description,
importPath: api.importPath,
@@ -108,26 +110,14 @@ function loadCorpusReactFlowApi(): { api: Parameters[
},
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",
@@ -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)
diff --git a/tests/reactflow-corpus-backed-tools-behaviour.test.ts b/tests/reactflow-corpus-backed-tools-behaviour.test.ts
index 491d417..4e104ad 100644
--- a/tests/reactflow-corpus-backed-tools-behaviour.test.ts
+++ b/tests/reactflow-corpus-backed-tools-behaviour.test.ts
@@ -13,11 +13,55 @@ test("reactflow_get_api prefers corpus metadata for ReactFlow", async () => {
expect(text).toContain("import { ReactFlow } from '@xyflow/react'");
});
-test("reactflow_get_api falls back to in-file data for non-corpus APIs", async () => {
+test("reactflow_get_api prefers corpus metadata for Handle", async () => {
const result = await reactFlowGetApi.invoke({ name: "Handle" });
const text = extractTextContent(result);
expect(text).toContain("# Handle (component)");
+ expect(text).toContain("**Corpus Source:** frontend.reactflow");
expect(text).toContain("import { Handle, Position } from '@xyflow/react'");
+});
+
+test("reactflow_get_api prefers corpus metadata for Background", async () => {
+ const result = await reactFlowGetApi.invoke({ name: "Background" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Background (component)");
+ expect(text).toContain("**Corpus Source:** frontend.reactflow");
+ expect(text).toContain("import { Background, BackgroundVariant } from '@xyflow/react'");
+});
+
+test("reactflow_get_api prefers corpus metadata for Controls", async () => {
+ const result = await reactFlowGetApi.invoke({ name: "Controls" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# Controls (component)");
+ expect(text).toContain("**Corpus Source:** frontend.reactflow");
+ expect(text).toContain("import { Controls, ControlButton } from '@xyflow/react'");
+});
+
+test("reactflow_get_api prefers corpus metadata for MiniMap", async () => {
+ const result = await reactFlowGetApi.invoke({ name: "MiniMap" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# MiniMap (component)");
+ expect(text).toContain("**Corpus Source:** frontend.reactflow");
+ expect(text).toContain("import { MiniMap } from '@xyflow/react'");
+});
+
+test("reactflow_get_api prefers corpus metadata for useReactFlow", async () => {
+ const result = await reactFlowGetApi.invoke({ name: "useReactFlow" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# useReactFlow (hook)");
+ expect(text).toContain("**Corpus Source:** frontend.reactflow");
+ expect(text).toContain("fitView, zoomIn, zoomOut");
+});
+
+test("reactflow_get_api falls back to in-file data for non-corpus APIs", async () => {
+ const result = await reactFlowGetApi.invoke({ name: "NodeResizer" });
+ const text = extractTextContent(result);
+
+ expect(text).toContain("# NodeResizer (component)");
expect(text).not.toContain("**Corpus Source:** frontend.reactflow");
});