The Noodles External Control API allows AI tools, scripts, and external applications to programmatically control the Noodles visualization platform. This enables automated data pipeline creation, testing, and debugging without manual UI interaction.
The External Control API provides:
- WebSocket-based communication for real-time bidirectional control
- Pipeline creation and testing tools for automated workflows
- Direct node manipulation for fine-grained control
- State observation for monitoring changes
- Tool execution for accessing all MCP (Model Context Protocol) tools
There are two ways to connect to Noodles:
Claude Desktop
↓ (stdio/MCP)
MCP Proxy Server
↓ (WebSocket)
Noodles Browser App
External AI Tool (e.g., Claude Code)
↓
WebSocket Client
↓
Bridge Server (ws://localhost:8765)
↓
Web Worker (in Noodles)
↓
Main Thread (Noodles App)
This is the recommended approach for Claude Desktop users.
cd examples/external-control
npm installAdd to your Claude Desktop config file:
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"noodles": {
"command": "node",
"args": ["/absolute/path/to/examples/external-control/mcp-proxy.js"]
}
}
}http://localhost:5173/examples/nyc-taxis?externalControl=true
Now Claude can control Noodles directly! Try asking:
- "List all the nodes in the current project"
- "Create a scatterplot layer showing the pickup locations"
- "Capture a screenshot of the visualization"
Add URL parameters when opening Noodles:
http://localhost:5173/examples/nyc-taxis?externalControl=true&externalControlDebug=true
Parameters:
externalControl=true- Enables the external control systemexternalControlDebug=true- Shows connection status indicator
The bridge server routes messages between external tools and Noodles:
cd examples/external-control
npm install
node server-example.jsimport { NoodlesClient } from 'noodles-editor/src/external-control/client.js'
const client = new NoodlesClient({
host: 'localhost',
port: 8765,
debug: true
})
await client.connect()
// Create a pipeline
const pipeline = await client.createPipeline({
dataSource: { type: 'FileOp', config: { url: 'data.csv' } },
transformations: [
{ type: 'FilterOp', config: { expression: 'd.value > 100' } }
],
output: { type: 'ScatterplotLayerOp', config: {} }
})from noodles_client import NoodlesClient
client = NoodlesClient(debug=True)
client.connect()
# Create a pipeline
pipeline = client.create_pipeline({
"dataSource": {"type": "FileOp", "config": {"url": "data.csv"}},
"transformations": [
{"type": "FilterOp", "config": {"expression": "d.value > 100"}}
],
"output": {"type": "ScatterplotLayerOp", "config": {}}
})Establishes WebSocket connection to Noodles.
await client.connect('localhost', 8765)Closes the connection.
client.disconnect()Creates a complete data pipeline from specification.
const pipeline = await client.createPipeline({
dataSource: {
type: 'FileOp',
config: { url: '@/data.csv', format: 'csv' }
},
transformations: [
{
type: 'FilterOp',
config: { expression: 'd.value > threshold' }
},
{
type: 'MapOp',
config: { expression: '({ ...d, normalized: d.value / 100 })' }
}
],
output: {
type: 'ScatterplotLayerOp',
config: {
getPosition: 'd => [d.lng, d.lat]',
getRadius: 100,
getFillColor: '[255, 0, 0]'
}
}
})Tests a pipeline with sample data.
const result = await client.testPipeline(pipeline.id, [
{ lng: -74.0, lat: 40.7, value: 150 },
{ lng: -73.9, lat: 40.8, value: 200 }
])Validates pipeline connections and configuration.
const validation = await client.validatePipeline(pipeline.id)
if (!validation.valid) {
console.error('Pipeline errors:', validation.errors)
}Adds a new node to the pipeline.
const nodeId = await client.addNode('FilterOp', { x: 100, y: 200 }, {
expression: 'd.value > 0'
})Creates a connection between nodes.
await client.connectNodes(
'/source-node',
'/target-node',
'out.result',
'par.data'
)Removes a node and its connections.
await client.deleteNode('/filter-123')Uploads a data file for use in pipelines.
const url = await client.uploadDataFile(
'mydata.csv',
csvContent,
'text/csv'
)Returns the current project state including all nodes and edges.
const state = await client.getProjectState()
console.log(`Project has ${state.nodes.length} nodes`)Gets the output values of a specific node.
const outputs = await client.getNodeOutputs('/filter-op')
console.log('Filtered data:', outputs)Captures a screenshot of the current visualization.
const screenshot = await client.captureVisualization('png', 0.9)
// screenshot.data contains base64 encoded imageExecutes any registered MCP tool.
// Get console errors
const errors = await client.callTool('getConsoleErrors', { limit: 10 })
// Get render stats
const stats = await client.callTool('getRenderStats', {})
// Apply modifications
await client.callTool('applyModifications', {
modifications: {
nodes: [{ type: 'add', node: newNode }],
edges: [{ type: 'add', edge: newEdge }]
}
})Subscribe to project state changes.
client.onStateChange((state) => {
console.log('State changed:', state)
})Subscribe to error events.
client.onError((error) => {
console.error('Error:', error)
})FileOp- Load CSV, JSON, GeoJSON filesDuckDbOp- SQL queriesNetworkOp- Fetch from URLs
FilterOp- Filter data by conditionMapOp- Transform data itemsGroupByOp- Group and aggregateJoinOp- Join datasetsSortOp- Sort data
ScatterplotLayerOp- Point visualizationsPathLayerOp- Lines and routesArcLayerOp- Arc connectionsHeatmapLayerOp- Density mapsGeoJsonLayerOp- Geographic featuresTextLayerOp- Text labelsTripsLayerOp- Animated paths/trips
See the Operators Guide for complete list.
The External Control API uses a JSON-based message protocol over WebSocket.
interface Message {
id: string // Unique message ID
type: MessageType // Message type
timestamp: number // Unix timestamp
payload: any // Message-specific payload
}connect- Establish connectiondisconnect- Close connectiontool_call- Execute a tooltool_response- Tool execution resultpipeline_create- Create pipelinepipeline_test- Test pipelinestate_change- State update notificationerror- Error message
const pipeline = await client.createPipeline({
dataSource: {
type: 'FileOp',
config: { url: '@/cities.geojson', format: 'geojson' }
},
transformations: [
{
type: 'FilterOp',
config: { expression: 'd.properties.population > 1000000' }
}
],
output: {
type: 'GeoJsonLayerOp',
config: {
getFillColor: '[255, 140, 0]',
getLineColor: '[0, 0, 0]',
lineWidthMinPixels: 2
}
}
})// Create test pipeline
const pipeline = await client.createPipeline(pipelineSpec)
// Validate connections
const validation = await client.validatePipeline(pipeline.id)
assert(validation.valid)
// Test with different datasets
for (const dataset of testDatasets) {
const result = await client.testPipeline(pipeline.id, dataset)
assert(result.success)
// Capture visualization for each test
const screenshot = await client.captureVisualization()
saveScreenshot(screenshot, `test-${dataset.name}.png`)
}
// Check for errors
const errors = await client.callTool('getConsoleErrors', {})
assert(errors.length === 0)// Monitor state changes
client.onStateChange((state) => {
// Update external dashboard
updateDashboard({
nodeCount: state.nodes.length,
edgeCount: state.edges.length
})
})
// Monitor errors
client.onError((error) => {
// Send alert
alertSystem.notify('Noodles Error', error.message)
})
// Periodic health checks
setInterval(async () => {
const stats = await client.callTool('getRenderStats', {})
if (stats.fps < 30) {
console.warn('Low FPS:', stats.fps)
}
}, 5000)Set debug: true when creating the client:
const client = new NoodlesClient({ debug: true })This will log all messages to the console.
The debug indicator in Noodles shows connection status:
- Green: Connected
- Red: Error
- Gray: Disconnected
-
Connection Failed
- Ensure bridge server is running
- Check firewall/network settings
- Verify correct host/port
-
Tool Not Found
- Check tool name spelling
- Verify tool is registered
- See available tools with
listTools()
-
Pipeline Creation Failed
- Validate operator types exist
- Check field connections are compatible
- Review error details in response
-
WebSocket Timeout
- Increase timeout in client config
- Check for long-running operations
- Verify server is responding
The External Control API is designed for local development:
- No Authentication: Currently no auth mechanism
- Local Only: Bind to localhost by default
- No Encryption: WebSocket traffic is unencrypted
- Full Access: Can execute any available tool
For production use, consider:
- Adding authentication tokens
- Using WSS (WebSocket Secure)
- Implementing rate limiting
- Restricting tool access
- Batch Operations: Use
applyModificationsfor multiple changes - Reuse Connections: Keep WebSocket open for multiple operations
- Async Operations: Use promises/async-await for better flow
- Monitor Stats: Check FPS and render times regularly
- Optimize Pipelines: Minimize transformation steps
- Browser-based: Requires Noodles running in browser
- Single Project: Controls one project at a time
- Local Files: Limited to browser file access
- Memory Constraints: Subject to browser memory limits
Planned improvements:
- Native server mode (no browser required)
- Multi-project support
- Remote file access
- Batch processing API
- GraphQL interface
- Plugin system for custom tools
For issues or questions:
- GitHub Issues: noodles.gl/issues
- Documentation: noodles.gl/docs
- Examples:
/examples/external-control/