@particle-academy/agent-integrations 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -0
- package/dist/bridges/flow.d.cts +72 -0
- package/dist/bridges/flow.d.ts +72 -0
- package/dist/bridges/whiteboard.d.cts +40 -0
- package/dist/bridges/whiteboard.d.ts +40 -0
- package/dist/bridges-flow.cjs +330 -0
- package/dist/bridges-flow.cjs.map +1 -0
- package/dist/bridges-flow.js +4 -0
- package/dist/bridges-flow.js.map +1 -0
- package/dist/bridges-whiteboard.cjs +409 -0
- package/dist/bridges-whiteboard.cjs.map +1 -0
- package/dist/bridges-whiteboard.js +4 -0
- package/dist/bridges-whiteboard.js.map +1 -0
- package/dist/chunk-2VOQJKSU.js +320 -0
- package/dist/chunk-2VOQJKSU.js.map +1 -0
- package/dist/chunk-5ZUHNNLR.js +398 -0
- package/dist/chunk-5ZUHNNLR.js.map +1 -0
- package/dist/chunk-6LTKCNLF.js +68 -0
- package/dist/chunk-6LTKCNLF.js.map +1 -0
- package/dist/chunk-FLEOQUKF.js +157 -0
- package/dist/chunk-FLEOQUKF.js.map +1 -0
- package/dist/chunk-QGCF7YKW.js +130 -0
- package/dist/chunk-QGCF7YKW.js.map +1 -0
- package/dist/index.cjs +1632 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +155 -0
- package/dist/index.d.ts +155 -0
- package/dist/index.js +567 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.cts +73 -0
- package/dist/mcp/index.d.ts +73 -0
- package/dist/mcp.cjs +210 -0
- package/dist/mcp.cjs.map +1 -0
- package/dist/mcp.js +4 -0
- package/dist/mcp.js.map +1 -0
- package/dist/server-Bv985us3.d.cts +173 -0
- package/dist/server-Bv985us3.d.ts +173 -0
- package/dist/sharing/index.d.cts +89 -0
- package/dist/sharing/index.d.ts +89 -0
- package/dist/sharing.cjs +166 -0
- package/dist/sharing.cjs.map +1 -0
- package/dist/sharing.js +3 -0
- package/dist/sharing.js.map +1 -0
- package/dist/styles.css +331 -0
- package/dist/styles.css.map +1 -0
- package/dist/types-CRPA_D0z.d.ts +18 -0
- package/dist/types-DR5AS6Rd.d.cts +18 -0
- package/docs/relay-protocol.md +57 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# @particle-academy/agent-integrations
|
|
2
|
+
|
|
3
|
+
MCP-driven agent presence in collab sessions. Each open session gets a **micro-MCP server** running in-page; agents (in-browser or external via relay) connect to it and act as participants — adding sticky notes, drawing, moving items, leaving cursor trails.
|
|
4
|
+
|
|
5
|
+
Also ships the **agent UX surface**: a chat-and-tool-log panel, an on-canvas presence cursor, and a brief activity highlight for items the agent just touched.
|
|
6
|
+
|
|
7
|
+
> v0.1 — focused on `@particle-academy/fancy-whiteboard` as the first bridged surface. The bridge layer is package-agnostic, so other fancy-* packages can register their own tool sets per session.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @particle-academy/agent-integrations
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import "@particle-academy/agent-integrations/styles.css";
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Architecture
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
┌─ Browser tab ───────────────────────────────────────┐
|
|
23
|
+
│ Whiteboard UI ── controlled state ── Bridge │
|
|
24
|
+
│ ▲ │ │
|
|
25
|
+
│ │ tool calls mutate state ▼ │
|
|
26
|
+
│ └────────── MicroMcpServer ◄─── Transport ◄─ │
|
|
27
|
+
│ ▲ │
|
|
28
|
+
└──────────────────────────────────────────┼──────────┘
|
|
29
|
+
│
|
|
30
|
+
┌────────┴─────────┐
|
|
31
|
+
│ │
|
|
32
|
+
in-process relay
|
|
33
|
+
(in-page agent) (external agent
|
|
34
|
+
via Reverb / WS)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- **`MicroMcpServer`** is a transport-agnostic JSON-RPC 2.0 / MCP protocol handler. Register tools, attach transports, done.
|
|
38
|
+
- **`InProcessTransport`** wires an in-page agent (e.g. an embedded Claude widget) to the server with zero serialization.
|
|
39
|
+
- **`RelayTransport`** wraps any JSON duplex channel (Reverb whisper, WebRTC data channel, SSE+POST tunnel) so external agents can reach the browser session.
|
|
40
|
+
- **Bridges** install a cohesive set of MCP tools against a host's controlled state. v0.1 ships the whiteboard bridge with the full tool kit.
|
|
41
|
+
- **UI components** (`AgentPanel`, `AgentCursor`, `AgentActivityHighlight`) make agent presence visible.
|
|
42
|
+
|
|
43
|
+
## Quick start (in-page agent + whiteboard)
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import {
|
|
47
|
+
MicroMcpServer,
|
|
48
|
+
attachInProcess,
|
|
49
|
+
registerWhiteboardBridge,
|
|
50
|
+
AgentPanel,
|
|
51
|
+
AgentCursor,
|
|
52
|
+
type AgentActivity,
|
|
53
|
+
} from "@particle-academy/agent-integrations";
|
|
54
|
+
import "@particle-academy/agent-integrations/styles.css";
|
|
55
|
+
|
|
56
|
+
function MyBoard() {
|
|
57
|
+
const [notes, setNotes] = useState([]);
|
|
58
|
+
const [shapes, setShapes] = useState([]);
|
|
59
|
+
const [connectors, setConnectors] = useState([]);
|
|
60
|
+
const [strokes, setStrokes] = useState([]);
|
|
61
|
+
const [viewport, setViewport] = useState({ x: 0, y: 0, zoom: 1 });
|
|
62
|
+
const [agentCursor, setAgentCursor] = useState(null);
|
|
63
|
+
const [activity, setActivity] = useState<AgentActivity[]>([]);
|
|
64
|
+
|
|
65
|
+
const serverRef = useRef<MicroMcpServer>();
|
|
66
|
+
const transportRef = useRef<InProcessTransport>();
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const server = new MicroMcpServer({
|
|
70
|
+
info: { name: "whiteboard-session", version: "0.1.0" },
|
|
71
|
+
});
|
|
72
|
+
const bridge = registerWhiteboardBridge(server, {
|
|
73
|
+
adapter: {
|
|
74
|
+
getNotes: () => notes, setNotes,
|
|
75
|
+
getShapes: () => shapes, setShapes,
|
|
76
|
+
getConnectors: () => connectors, setConnectors,
|
|
77
|
+
getStrokes: () => strokes, setStrokes,
|
|
78
|
+
getViewport: () => viewport, setViewport,
|
|
79
|
+
setAgentCursor,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const transport = attachInProcess(server);
|
|
83
|
+
serverRef.current = server;
|
|
84
|
+
transportRef.current = transport;
|
|
85
|
+
return () => bridge.dispose();
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
// ... render board, AgentPanel, AgentCursor when agentCursor is non-null ...
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
For an end-to-end runnable, see the sandbox demo at `/whiteboard-agent` (added in a follow-up PR).
|
|
93
|
+
|
|
94
|
+
## External agent via relay
|
|
95
|
+
|
|
96
|
+
The relay is host-implemented — this package only defines the JSON envelope. See `docs/relay-protocol.md`.
|
|
97
|
+
|
|
98
|
+
Pattern (Reverb):
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const channel = Echo.private(`agent.session.${id}`);
|
|
102
|
+
const transport = attachRelay(server, {
|
|
103
|
+
sendToRemote: (frame) => channel.whisper("mcp", frame),
|
|
104
|
+
});
|
|
105
|
+
channel.listenForWhisper("mcp", (frame) => transport.deliverFromRemote(frame));
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Agents (Claude Desktop, Cline, custom) connect to the same channel via your auth bridge.
|
|
109
|
+
|
|
110
|
+
## Tools shipped (whiteboard bridge)
|
|
111
|
+
|
|
112
|
+
| Tool | Purpose |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `whiteboard_get_state` | Full snapshot |
|
|
115
|
+
| `whiteboard_list_items` | Summary of every item |
|
|
116
|
+
| `whiteboard_get_item` | Detail by id |
|
|
117
|
+
| `whiteboard_add_sticky` / `_update_sticky` | Sticky CRUD |
|
|
118
|
+
| `whiteboard_add_shape` / `_update_shape` | Full shape kit (rect, rounded-rect, ellipse, diamond, triangle, line, arrow, text) |
|
|
119
|
+
| `whiteboard_add_connector` | Connect two items / points |
|
|
120
|
+
| `whiteboard_add_stroke` | Pen layer |
|
|
121
|
+
| `whiteboard_delete_item` | Generic delete |
|
|
122
|
+
| `whiteboard_set_viewport` | Pan / zoom |
|
|
123
|
+
| `whiteboard_set_agent_cursor` | Move presence |
|
|
124
|
+
|
|
125
|
+
## Status
|
|
126
|
+
|
|
127
|
+
`v0.1` — protocol + whiteboard bridge + UI. APIs will evolve before `v1`.
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { M as MicroMcpServer } from '../server-Bv985us3.cjs';
|
|
2
|
+
import { B as Bridge } from '../types-DR5AS6Rd.cjs';
|
|
3
|
+
|
|
4
|
+
type FlowNode = {
|
|
5
|
+
id: string;
|
|
6
|
+
type?: string;
|
|
7
|
+
position: {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
};
|
|
11
|
+
data: {
|
|
12
|
+
kind?: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
status?: string;
|
|
16
|
+
statusText?: string;
|
|
17
|
+
config?: Record<string, unknown>;
|
|
18
|
+
[k: string]: unknown;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
type FlowEdge = {
|
|
22
|
+
id: string;
|
|
23
|
+
source: string;
|
|
24
|
+
target: string;
|
|
25
|
+
sourceHandle?: string;
|
|
26
|
+
targetHandle?: string;
|
|
27
|
+
label?: string;
|
|
28
|
+
[k: string]: unknown;
|
|
29
|
+
};
|
|
30
|
+
type NodeRunStatus = "idle" | "queued" | "running" | "done" | "error";
|
|
31
|
+
type ExecutorRegistry = Record<string, unknown>;
|
|
32
|
+
type RunResult = {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
outputs: Record<string, unknown>;
|
|
35
|
+
error?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Adapter the host provides — same shape as the editor's local state plus
|
|
40
|
+
* an optional `run`/`cancel` pair so agents can trigger executions.
|
|
41
|
+
*/
|
|
42
|
+
type FlowBridgeAdapter = {
|
|
43
|
+
getNodes: () => FlowNode[];
|
|
44
|
+
setNodes: (next: FlowNode[] | ((prev: FlowNode[]) => FlowNode[])) => void;
|
|
45
|
+
getEdges: () => FlowEdge[];
|
|
46
|
+
setEdges: (next: FlowEdge[] | ((prev: FlowEdge[]) => FlowEdge[])) => void;
|
|
47
|
+
/** Optional: invoke runFlow with the host's executor registry. */
|
|
48
|
+
run?: (executors?: ExecutorRegistry) => Promise<RunResult>;
|
|
49
|
+
/** Optional: cancel the in-flight run. */
|
|
50
|
+
cancel?: () => void;
|
|
51
|
+
/** Optional: set per-node status text without going through the runner
|
|
52
|
+
* (useful for agents narrating). */
|
|
53
|
+
setNodeStatus?: (id: string, status: NodeRunStatus, text?: string) => void;
|
|
54
|
+
};
|
|
55
|
+
type FlowBridgeOptions = {
|
|
56
|
+
adapter: FlowBridgeAdapter;
|
|
57
|
+
/** Identity tagged onto agent-authored nodes. */
|
|
58
|
+
agent?: {
|
|
59
|
+
id: string;
|
|
60
|
+
name?: string;
|
|
61
|
+
color?: string;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* registerFlowBridge — wires an MCP tool set against a fancy-flow editor's
|
|
66
|
+
* controlled state. Mirrors the whiteboard bridge in shape: read tools,
|
|
67
|
+
* mutation tools (add / update / delete nodes + edges), and optional
|
|
68
|
+
* run/cancel if the host provides those callbacks.
|
|
69
|
+
*/
|
|
70
|
+
declare function registerFlowBridge(server: MicroMcpServer, options: FlowBridgeOptions): Bridge;
|
|
71
|
+
|
|
72
|
+
export { type FlowBridgeAdapter, type FlowBridgeOptions, registerFlowBridge };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { M as MicroMcpServer } from '../server-Bv985us3.js';
|
|
2
|
+
import { B as Bridge } from '../types-CRPA_D0z.js';
|
|
3
|
+
|
|
4
|
+
type FlowNode = {
|
|
5
|
+
id: string;
|
|
6
|
+
type?: string;
|
|
7
|
+
position: {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
};
|
|
11
|
+
data: {
|
|
12
|
+
kind?: string;
|
|
13
|
+
label?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
status?: string;
|
|
16
|
+
statusText?: string;
|
|
17
|
+
config?: Record<string, unknown>;
|
|
18
|
+
[k: string]: unknown;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
type FlowEdge = {
|
|
22
|
+
id: string;
|
|
23
|
+
source: string;
|
|
24
|
+
target: string;
|
|
25
|
+
sourceHandle?: string;
|
|
26
|
+
targetHandle?: string;
|
|
27
|
+
label?: string;
|
|
28
|
+
[k: string]: unknown;
|
|
29
|
+
};
|
|
30
|
+
type NodeRunStatus = "idle" | "queued" | "running" | "done" | "error";
|
|
31
|
+
type ExecutorRegistry = Record<string, unknown>;
|
|
32
|
+
type RunResult = {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
outputs: Record<string, unknown>;
|
|
35
|
+
error?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Adapter the host provides — same shape as the editor's local state plus
|
|
40
|
+
* an optional `run`/`cancel` pair so agents can trigger executions.
|
|
41
|
+
*/
|
|
42
|
+
type FlowBridgeAdapter = {
|
|
43
|
+
getNodes: () => FlowNode[];
|
|
44
|
+
setNodes: (next: FlowNode[] | ((prev: FlowNode[]) => FlowNode[])) => void;
|
|
45
|
+
getEdges: () => FlowEdge[];
|
|
46
|
+
setEdges: (next: FlowEdge[] | ((prev: FlowEdge[]) => FlowEdge[])) => void;
|
|
47
|
+
/** Optional: invoke runFlow with the host's executor registry. */
|
|
48
|
+
run?: (executors?: ExecutorRegistry) => Promise<RunResult>;
|
|
49
|
+
/** Optional: cancel the in-flight run. */
|
|
50
|
+
cancel?: () => void;
|
|
51
|
+
/** Optional: set per-node status text without going through the runner
|
|
52
|
+
* (useful for agents narrating). */
|
|
53
|
+
setNodeStatus?: (id: string, status: NodeRunStatus, text?: string) => void;
|
|
54
|
+
};
|
|
55
|
+
type FlowBridgeOptions = {
|
|
56
|
+
adapter: FlowBridgeAdapter;
|
|
57
|
+
/** Identity tagged onto agent-authored nodes. */
|
|
58
|
+
agent?: {
|
|
59
|
+
id: string;
|
|
60
|
+
name?: string;
|
|
61
|
+
color?: string;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* registerFlowBridge — wires an MCP tool set against a fancy-flow editor's
|
|
66
|
+
* controlled state. Mirrors the whiteboard bridge in shape: read tools,
|
|
67
|
+
* mutation tools (add / update / delete nodes + edges), and optional
|
|
68
|
+
* run/cancel if the host provides those callbacks.
|
|
69
|
+
*/
|
|
70
|
+
declare function registerFlowBridge(server: MicroMcpServer, options: FlowBridgeOptions): Bridge;
|
|
71
|
+
|
|
72
|
+
export { type FlowBridgeAdapter, type FlowBridgeOptions, registerFlowBridge };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { StickyNoteItem, ShapeItem, ConnectorItem, Stroke, Viewport, RemoteCursor } from '@particle-academy/fancy-whiteboard';
|
|
2
|
+
import { M as MicroMcpServer } from '../server-Bv985us3.cjs';
|
|
3
|
+
import { B as Bridge } from '../types-DR5AS6Rd.cjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* State accessors / mutators the bridge needs from the host. The host owns
|
|
7
|
+
* whiteboard state (controlled props on fancy-whiteboard components); the
|
|
8
|
+
* bridge calls into these to read or change it.
|
|
9
|
+
*/
|
|
10
|
+
type WhiteboardBridgeAdapter = {
|
|
11
|
+
getNotes: () => StickyNoteItem[];
|
|
12
|
+
setNotes: (next: StickyNoteItem[] | ((prev: StickyNoteItem[]) => StickyNoteItem[])) => void;
|
|
13
|
+
getShapes: () => ShapeItem[];
|
|
14
|
+
setShapes: (next: ShapeItem[] | ((prev: ShapeItem[]) => ShapeItem[])) => void;
|
|
15
|
+
getConnectors: () => ConnectorItem[];
|
|
16
|
+
setConnectors: (next: ConnectorItem[] | ((prev: ConnectorItem[]) => ConnectorItem[])) => void;
|
|
17
|
+
getStrokes: () => Stroke[];
|
|
18
|
+
setStrokes: (next: Stroke[] | ((prev: Stroke[]) => Stroke[])) => void;
|
|
19
|
+
getViewport: () => Viewport;
|
|
20
|
+
setViewport: (next: Viewport) => void;
|
|
21
|
+
/** Optional: agent presence cursor (for the visualizer). */
|
|
22
|
+
setAgentCursor?: (cursor: RemoteCursor | null) => void;
|
|
23
|
+
};
|
|
24
|
+
type WhiteboardBridgeOptions = {
|
|
25
|
+
adapter: WhiteboardBridgeAdapter;
|
|
26
|
+
/** Identity used when the agent stamps authorId on items / cursor. */
|
|
27
|
+
agent?: {
|
|
28
|
+
id: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
color?: string;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* registerWhiteboardBridge — wires a full MCP tool set against a fancy-
|
|
35
|
+
* whiteboard session controlled by the host. Returns a Bridge handle the
|
|
36
|
+
* host can dispose to tear everything down.
|
|
37
|
+
*/
|
|
38
|
+
declare function registerWhiteboardBridge(server: MicroMcpServer, options: WhiteboardBridgeOptions): Bridge;
|
|
39
|
+
|
|
40
|
+
export { type WhiteboardBridgeAdapter, type WhiteboardBridgeOptions, registerWhiteboardBridge };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { StickyNoteItem, ShapeItem, ConnectorItem, Stroke, Viewport, RemoteCursor } from '@particle-academy/fancy-whiteboard';
|
|
2
|
+
import { M as MicroMcpServer } from '../server-Bv985us3.js';
|
|
3
|
+
import { B as Bridge } from '../types-CRPA_D0z.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* State accessors / mutators the bridge needs from the host. The host owns
|
|
7
|
+
* whiteboard state (controlled props on fancy-whiteboard components); the
|
|
8
|
+
* bridge calls into these to read or change it.
|
|
9
|
+
*/
|
|
10
|
+
type WhiteboardBridgeAdapter = {
|
|
11
|
+
getNotes: () => StickyNoteItem[];
|
|
12
|
+
setNotes: (next: StickyNoteItem[] | ((prev: StickyNoteItem[]) => StickyNoteItem[])) => void;
|
|
13
|
+
getShapes: () => ShapeItem[];
|
|
14
|
+
setShapes: (next: ShapeItem[] | ((prev: ShapeItem[]) => ShapeItem[])) => void;
|
|
15
|
+
getConnectors: () => ConnectorItem[];
|
|
16
|
+
setConnectors: (next: ConnectorItem[] | ((prev: ConnectorItem[]) => ConnectorItem[])) => void;
|
|
17
|
+
getStrokes: () => Stroke[];
|
|
18
|
+
setStrokes: (next: Stroke[] | ((prev: Stroke[]) => Stroke[])) => void;
|
|
19
|
+
getViewport: () => Viewport;
|
|
20
|
+
setViewport: (next: Viewport) => void;
|
|
21
|
+
/** Optional: agent presence cursor (for the visualizer). */
|
|
22
|
+
setAgentCursor?: (cursor: RemoteCursor | null) => void;
|
|
23
|
+
};
|
|
24
|
+
type WhiteboardBridgeOptions = {
|
|
25
|
+
adapter: WhiteboardBridgeAdapter;
|
|
26
|
+
/** Identity used when the agent stamps authorId on items / cursor. */
|
|
27
|
+
agent?: {
|
|
28
|
+
id: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
color?: string;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* registerWhiteboardBridge — wires a full MCP tool set against a fancy-
|
|
35
|
+
* whiteboard session controlled by the host. Returns a Bridge handle the
|
|
36
|
+
* host can dispose to tear everything down.
|
|
37
|
+
*/
|
|
38
|
+
declare function registerWhiteboardBridge(server: MicroMcpServer, options: WhiteboardBridgeOptions): Bridge;
|
|
39
|
+
|
|
40
|
+
export { type WhiteboardBridgeAdapter, type WhiteboardBridgeOptions, registerWhiteboardBridge };
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/mcp/server.ts
|
|
4
|
+
function textResult(text, structured) {
|
|
5
|
+
return {
|
|
6
|
+
content: [{ type: "text", text }],
|
|
7
|
+
...structured !== void 0 ? { structuredContent: structured } : {}
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function errorResult(text) {
|
|
11
|
+
return { content: [{ type: "text", text }], isError: true };
|
|
12
|
+
}
|
|
13
|
+
var num = (v, fallback) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
14
|
+
var str = (v, fallback = "") => typeof v === "string" ? v : fallback;
|
|
15
|
+
var newId = (prefix) => `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
|
|
16
|
+
function registerFlowBridge(server, options) {
|
|
17
|
+
const { adapter } = options;
|
|
18
|
+
({ ...options.agent ?? {} });
|
|
19
|
+
const disposers = [];
|
|
20
|
+
const reg = (name, description, properties, required, handler) => {
|
|
21
|
+
disposers.push(
|
|
22
|
+
server.registerTool(
|
|
23
|
+
{
|
|
24
|
+
name,
|
|
25
|
+
description,
|
|
26
|
+
inputSchema: { type: "object", properties, required, additionalProperties: false }
|
|
27
|
+
},
|
|
28
|
+
async (args) => {
|
|
29
|
+
try {
|
|
30
|
+
return await handler(args);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return errorResult(e instanceof Error ? e.message : String(e));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
reg("flow_get_state", "Get the full graph: nodes + edges.", {}, [], () => {
|
|
39
|
+
const state = { nodes: adapter.getNodes(), edges: adapter.getEdges() };
|
|
40
|
+
return textResult(JSON.stringify(state, null, 2), state);
|
|
41
|
+
});
|
|
42
|
+
reg("flow_list_nodes", "Summarise every node: id, kind, label, position, status.", {}, [], () => {
|
|
43
|
+
const items = adapter.getNodes().map((n) => ({
|
|
44
|
+
id: n.id,
|
|
45
|
+
kind: n.type,
|
|
46
|
+
label: n.data?.label,
|
|
47
|
+
x: Math.round(n.position.x),
|
|
48
|
+
y: Math.round(n.position.y),
|
|
49
|
+
status: n.data?.status ?? "idle"
|
|
50
|
+
}));
|
|
51
|
+
const text = items.map((i) => `${i.kind} ${i.id}: "${i.label}" @(${i.x},${i.y}) [${i.status}]`).join("\n") || "(empty graph)";
|
|
52
|
+
return textResult(text, items);
|
|
53
|
+
});
|
|
54
|
+
reg(
|
|
55
|
+
"flow_get_node",
|
|
56
|
+
"Get a single node's full record by id.",
|
|
57
|
+
{ id: { type: "string" } },
|
|
58
|
+
["id"],
|
|
59
|
+
(args) => {
|
|
60
|
+
const id = str(args.id);
|
|
61
|
+
const node = adapter.getNodes().find((n) => n.id === id);
|
|
62
|
+
if (!node) return errorResult(`No node with id ${id}`);
|
|
63
|
+
return textResult(JSON.stringify(node, null, 2), node);
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
reg(
|
|
67
|
+
"flow_list_node_kinds",
|
|
68
|
+
"List every node kind registered in fancy-flow's registry. Use this to discover what's authorable before adding nodes.",
|
|
69
|
+
{ category: { type: "string", description: "Optional category filter: trigger | logic | data | ai | io | human | output | custom." } },
|
|
70
|
+
[],
|
|
71
|
+
async () => {
|
|
72
|
+
try {
|
|
73
|
+
const { listNodeKinds } = await import('@particle-academy/fancy-flow');
|
|
74
|
+
const cat = adapter ? void 0 : void 0;
|
|
75
|
+
const all = (cat ? listNodeKinds(cat) : listNodeKinds()).map((k) => ({
|
|
76
|
+
name: k.name,
|
|
77
|
+
category: k.category,
|
|
78
|
+
label: k.label,
|
|
79
|
+
description: k.description,
|
|
80
|
+
icon: k.icon,
|
|
81
|
+
accent: k.accent,
|
|
82
|
+
inputs: k.inputs ?? [],
|
|
83
|
+
outputs: k.outputs ?? [],
|
|
84
|
+
configFields: (k.configSchema ?? []).map((f) => ({ key: f.key, type: f.type, label: f.label, required: !!f.required }))
|
|
85
|
+
}));
|
|
86
|
+
const text = all.map((k) => `${k.category}/${k.name}: ${k.label}${k.description ? " \u2014 " + k.description : ""}`).join("\n");
|
|
87
|
+
return textResult(text || "(no kinds registered)", all);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
return errorResult(`fancy-flow registry not available: ${e instanceof Error ? e.message : String(e)}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
reg(
|
|
94
|
+
"flow_get_node_schema",
|
|
95
|
+
"Get the full configSchema + ports for a node kind. Use to know exactly what fields a kind accepts before calling flow_add_node.",
|
|
96
|
+
{ name: { type: "string" } },
|
|
97
|
+
["name"],
|
|
98
|
+
async (args) => {
|
|
99
|
+
try {
|
|
100
|
+
const { getNodeKind } = await import('@particle-academy/fancy-flow');
|
|
101
|
+
const k = getNodeKind(str(args.name));
|
|
102
|
+
if (!k) return errorResult(`No kind registered: ${args.name}`);
|
|
103
|
+
const summary = {
|
|
104
|
+
name: k.name,
|
|
105
|
+
category: k.category,
|
|
106
|
+
label: k.label,
|
|
107
|
+
description: k.description,
|
|
108
|
+
inputs: k.inputs ?? [],
|
|
109
|
+
outputs: k.outputs ?? [],
|
|
110
|
+
configSchema: k.configSchema ?? [],
|
|
111
|
+
defaultConfig: k.defaultConfig ?? null
|
|
112
|
+
};
|
|
113
|
+
return textResult(JSON.stringify(summary, null, 2), summary);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return errorResult(`fancy-flow registry not available: ${e instanceof Error ? e.message : String(e)}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
reg("flow_list_edges", "Summarise every edge.", {}, [], () => {
|
|
120
|
+
const items = adapter.getEdges().map((e) => ({
|
|
121
|
+
id: e.id,
|
|
122
|
+
from: `${e.source}${e.sourceHandle ? `:${e.sourceHandle}` : ""}`,
|
|
123
|
+
to: `${e.target}${e.targetHandle ? `:${e.targetHandle}` : ""}`
|
|
124
|
+
}));
|
|
125
|
+
return textResult(items.map((i) => `${i.id}: ${i.from} \u2192 ${i.to}`).join("\n") || "(no edges)", items);
|
|
126
|
+
});
|
|
127
|
+
reg(
|
|
128
|
+
"flow_add_node",
|
|
129
|
+
"Add a node of any kind registered in fancy-flow's registry. Call flow_list_node_kinds first to discover what's available.",
|
|
130
|
+
{
|
|
131
|
+
kind: { type: "string", description: "Registry kind name (e.g. memory_store, llm_call, branch)." },
|
|
132
|
+
label: { type: "string" },
|
|
133
|
+
x: { type: "number" },
|
|
134
|
+
y: { type: "number" },
|
|
135
|
+
description: { type: "string" },
|
|
136
|
+
config: { type: "object", description: "Config fields per the kind's configSchema." },
|
|
137
|
+
body: { type: "string", description: "Note kinds only \u2014 body text." }
|
|
138
|
+
},
|
|
139
|
+
["kind", "label", "x", "y"],
|
|
140
|
+
async (args) => {
|
|
141
|
+
const kindName = str(args.kind);
|
|
142
|
+
let kindDef = null;
|
|
143
|
+
try {
|
|
144
|
+
const { getNodeKind, defaultConfigFor } = await import('@particle-academy/fancy-flow');
|
|
145
|
+
kindDef = getNodeKind(kindName);
|
|
146
|
+
var defaults = kindDef ? defaultConfigFor(kindDef) : {};
|
|
147
|
+
} catch {
|
|
148
|
+
var defaults = {};
|
|
149
|
+
}
|
|
150
|
+
const isLegacy = ["trigger", "action", "decision", "output", "note", "subgraph"].includes(kindName);
|
|
151
|
+
if (!kindDef && !isLegacy) {
|
|
152
|
+
return errorResult(`Unknown kind: ${kindName} \u2014 call flow_list_node_kinds for the registry.`);
|
|
153
|
+
}
|
|
154
|
+
const id = newId("n");
|
|
155
|
+
const config = { ...defaults, ...args.config && typeof args.config === "object" ? args.config : {} };
|
|
156
|
+
const node = {
|
|
157
|
+
id,
|
|
158
|
+
type: kindName,
|
|
159
|
+
position: { x: num(args.x), y: num(args.y) },
|
|
160
|
+
data: {
|
|
161
|
+
kind: kindName,
|
|
162
|
+
label: str(args.label),
|
|
163
|
+
...args.description ? { description: str(args.description) } : {},
|
|
164
|
+
config,
|
|
165
|
+
...kindName === "note" && args.body ? { body: str(args.body) } : {}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
adapter.setNodes((all) => [...all, node]);
|
|
169
|
+
return textResult(`Added ${kindName} ${id} ("${str(args.label)}")`, node);
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
reg(
|
|
173
|
+
"flow_update_node",
|
|
174
|
+
"Update fields on a node. Only provided fields change.",
|
|
175
|
+
{
|
|
176
|
+
id: { type: "string" },
|
|
177
|
+
label: { type: "string" },
|
|
178
|
+
x: { type: "number" },
|
|
179
|
+
y: { type: "number" },
|
|
180
|
+
description: { type: "string" },
|
|
181
|
+
config: { type: "object" }
|
|
182
|
+
},
|
|
183
|
+
["id"],
|
|
184
|
+
(args) => {
|
|
185
|
+
const id = str(args.id);
|
|
186
|
+
let updated = null;
|
|
187
|
+
adapter.setNodes(
|
|
188
|
+
(all) => all.map((n) => {
|
|
189
|
+
if (n.id !== id) return n;
|
|
190
|
+
updated = {
|
|
191
|
+
...n,
|
|
192
|
+
position: {
|
|
193
|
+
x: args.x !== void 0 ? num(args.x) : n.position.x,
|
|
194
|
+
y: args.y !== void 0 ? num(args.y) : n.position.y
|
|
195
|
+
},
|
|
196
|
+
data: {
|
|
197
|
+
...n.data,
|
|
198
|
+
...args.label !== void 0 ? { label: str(args.label) } : {},
|
|
199
|
+
...args.description !== void 0 ? { description: str(args.description) } : {},
|
|
200
|
+
...args.config && typeof args.config === "object" ? { config: { ...n.data.config ?? {}, ...args.config } } : {}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
return updated;
|
|
204
|
+
})
|
|
205
|
+
);
|
|
206
|
+
if (!updated) return errorResult(`No node with id ${id}`);
|
|
207
|
+
return textResult(`Updated node ${id}`, updated);
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
reg(
|
|
211
|
+
"flow_delete_node",
|
|
212
|
+
"Remove a node by id (also removes any connected edges).",
|
|
213
|
+
{ id: { type: "string" } },
|
|
214
|
+
["id"],
|
|
215
|
+
(args) => {
|
|
216
|
+
const id = str(args.id);
|
|
217
|
+
if (!adapter.getNodes().some((n) => n.id === id)) {
|
|
218
|
+
return errorResult(`No node with id ${id}`);
|
|
219
|
+
}
|
|
220
|
+
adapter.setNodes((all) => all.filter((n) => n.id !== id));
|
|
221
|
+
adapter.setEdges((all) => all.filter((e) => e.source !== id && e.target !== id));
|
|
222
|
+
return textResult(`Deleted node ${id}`);
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
reg(
|
|
226
|
+
"flow_connect",
|
|
227
|
+
"Create an edge between two nodes (optionally specifying handle ids).",
|
|
228
|
+
{
|
|
229
|
+
source: { type: "string" },
|
|
230
|
+
target: { type: "string" },
|
|
231
|
+
sourceHandle: { type: "string" },
|
|
232
|
+
targetHandle: { type: "string" },
|
|
233
|
+
label: { type: "string" }
|
|
234
|
+
},
|
|
235
|
+
["source", "target"],
|
|
236
|
+
(args) => {
|
|
237
|
+
const source = str(args.source);
|
|
238
|
+
const target = str(args.target);
|
|
239
|
+
const all = adapter.getNodes();
|
|
240
|
+
if (!all.find((n) => n.id === source)) return errorResult(`No source node ${source}`);
|
|
241
|
+
if (!all.find((n) => n.id === target)) return errorResult(`No target node ${target}`);
|
|
242
|
+
const edge = {
|
|
243
|
+
id: newId("e"),
|
|
244
|
+
source,
|
|
245
|
+
target,
|
|
246
|
+
...args.sourceHandle ? { sourceHandle: str(args.sourceHandle) } : {},
|
|
247
|
+
...args.targetHandle ? { targetHandle: str(args.targetHandle) } : {},
|
|
248
|
+
...args.label ? { label: str(args.label) } : {}
|
|
249
|
+
};
|
|
250
|
+
adapter.setEdges((existing) => [...existing, edge]);
|
|
251
|
+
return textResult(`Connected ${source}${edge.sourceHandle ? `:${edge.sourceHandle}` : ""} \u2192 ${target}${edge.targetHandle ? `:${edge.targetHandle}` : ""}`, edge);
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
reg(
|
|
255
|
+
"flow_disconnect",
|
|
256
|
+
"Remove an edge by id.",
|
|
257
|
+
{ id: { type: "string" } },
|
|
258
|
+
["id"],
|
|
259
|
+
(args) => {
|
|
260
|
+
const id = str(args.id);
|
|
261
|
+
if (!adapter.getEdges().some((e) => e.id === id)) {
|
|
262
|
+
return errorResult(`No edge ${id}`);
|
|
263
|
+
}
|
|
264
|
+
adapter.setEdges((all) => all.filter((e) => e.id !== id));
|
|
265
|
+
return textResult(`Disconnected ${id}`);
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
reg(
|
|
269
|
+
"flow_set_node_status",
|
|
270
|
+
"Manually set a node's status badge (idle | queued | running | done | error) and optional text. Useful for narration outside a run.",
|
|
271
|
+
{
|
|
272
|
+
id: { type: "string" },
|
|
273
|
+
status: { type: "string", enum: ["idle", "queued", "running", "done", "error"] },
|
|
274
|
+
text: { type: "string" }
|
|
275
|
+
},
|
|
276
|
+
["id", "status"],
|
|
277
|
+
(args) => {
|
|
278
|
+
const id = str(args.id);
|
|
279
|
+
const status = str(args.status);
|
|
280
|
+
const text = args.text !== void 0 ? str(args.text) : void 0;
|
|
281
|
+
if (adapter.setNodeStatus) {
|
|
282
|
+
adapter.setNodeStatus(id, status, text);
|
|
283
|
+
} else {
|
|
284
|
+
let found = false;
|
|
285
|
+
adapter.setNodes(
|
|
286
|
+
(all) => all.map((n) => {
|
|
287
|
+
if (n.id !== id) return n;
|
|
288
|
+
found = true;
|
|
289
|
+
return { ...n, data: { ...n.data, status, statusText: text } };
|
|
290
|
+
})
|
|
291
|
+
);
|
|
292
|
+
if (!found) return errorResult(`No node with id ${id}`);
|
|
293
|
+
}
|
|
294
|
+
return textResult(`${id} \u2192 ${status}${text ? ` (${text})` : ""}`);
|
|
295
|
+
}
|
|
296
|
+
);
|
|
297
|
+
reg(
|
|
298
|
+
"flow_run",
|
|
299
|
+
"Trigger a run of the current graph. Returns the topological result. Requires the host to have wired `run` into the adapter.",
|
|
300
|
+
{},
|
|
301
|
+
[],
|
|
302
|
+
async () => {
|
|
303
|
+
if (!adapter.run) return errorResult("Host did not provide a run handler.");
|
|
304
|
+
const result = await adapter.run();
|
|
305
|
+
return textResult(result.ok ? "Run complete" : `Run failed: ${result.error ?? "unknown"}`, result);
|
|
306
|
+
}
|
|
307
|
+
);
|
|
308
|
+
reg(
|
|
309
|
+
"flow_cancel",
|
|
310
|
+
"Cancel an in-flight run.",
|
|
311
|
+
{},
|
|
312
|
+
[],
|
|
313
|
+
() => {
|
|
314
|
+
if (!adapter.cancel) return errorResult("Host did not provide a cancel handler.");
|
|
315
|
+
adapter.cancel();
|
|
316
|
+
return textResult("Run cancelled");
|
|
317
|
+
}
|
|
318
|
+
);
|
|
319
|
+
return {
|
|
320
|
+
id: "flow",
|
|
321
|
+
title: "Flow",
|
|
322
|
+
dispose: () => {
|
|
323
|
+
for (const d of disposers) d();
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
exports.registerFlowBridge = registerFlowBridge;
|
|
329
|
+
//# sourceMappingURL=bridges-flow.cjs.map
|
|
330
|
+
//# sourceMappingURL=bridges-flow.cjs.map
|