Skip to content

Local Network MCPs/Photons

Problem

With Phase 4 network isolation, the default security policy blocks access to:

  • Private IPs (192.168.x.x, 10.x.x.x, 172.16.x.x)
  • Localhost (127.0.0.1)

This breaks MCPs that need to communicate with local devices, such as:

Solution: Bindings Pattern

MCPs that need local network access should use the bindings pattern (Phase 3). Bindings execute in the main thread where they have full Node.js network access.

Architecture

┌─────────────────────────────────────────┐
│         Worker Thread (Sandboxed)        │
│                                          │
│  await fetch('http://192.168.1.1')      │
│  ❌ BLOCKED (network policy)            │
│                                          │
│  await lgRemote.sendCommand('POWER_ON') │
│  ✅ ALLOWED (binding call)              │
│  └─→ Executes in main thread ───────────┼───┐
└──────────────────────────────────────────┘   │

┌──────────────────────────────────────────────┼──┐
│         Main Thread (Full Access)            ↓  │
│                                                 │
│  lgRemote.sendCommand() {                      │
│    fetch('http://192.168.1.100:3000/power')   │
│    ✅ Native Node.js fetch - NO restrictions  │
│  }                                             │
└────────────────────────────────────────────────┘

Implementation Example: LG Remote

typescript
// 1. Create binding with network policy declaration
bindingsManager.createBinding(
  'lgRemote',
  'local-network',  // Type indicates local network access
  ['sendCommand', 'getStatus', 'setVolume'],
  {
    // Network policy declaration (for documentation/permissions)
    allowPrivateIPs: true,
    allowedDomains: [],  // No external domains needed
    maxRequestSize: 1024,
    timeout: 5000
  }
);

// 2. Create authenticated client (executes in main thread)
bindingsManager.createAuthenticatedClient('lgRemote', (credential) => {
  const tvIP = credential.data.custom.tvIP || '192.168.1.100';

  return {
    // Native Node.js fetch - NO network policy restrictions
    sendCommand: async (command: string) => {
      const response = await fetch(`http://${tvIP}:3000/command`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ command })
      });
      return await response.json();
    },

    getStatus: async () => {
      const response = await fetch(`http://${tvIP}:3000/status`);
      return await response.json();
    },

    setVolume: async (level: number) => {
      const response = await fetch(`http://${tvIP}:3000/volume`, {
        method: 'POST',
        body: JSON.stringify({ level })
      });
      return await response.json();
    }
  };
});

// 3. User code (in sandbox) calls binding
const code = `
  // Call binding - executes in main thread with full network access
  const status = await lgRemote.getStatus();
  console.log('TV Power:', status.power);

  if (status.power === 'off') {
    await lgRemote.sendCommand('POWER_ON');
  }

  await lgRemote.setVolume(20);

  // Direct fetch still blocked!
  try {
    await fetch('http://192.168.1.100:3000/status');
  } catch (error) {
    // Error: Network request blocked: Private IP access is not allowed
  }
`;

Security Guarantees

Worker code cannot bypass network policy

  • Direct fetch() calls are restricted
  • Cannot access private IPs or localhost

Only approved bindings have local network access

  • Bindings are created by trusted code (not user)
  • User approves binding during MCP installation

Binding network access is scoped

  • Each binding has specific methods
  • Cannot be used to access arbitrary endpoints

Network policy declaration is auditable

  • Each binding declares its network needs
  • User can see what network access an MCP requires

Benefits

  1. Security: User code in sandbox cannot access local network
  2. Flexibility: MCPs can access local devices when needed
  3. Transparency: Network requirements declared upfront
  4. Control: Only approved bindings have local access

Other Use Cases

Philips Hue Binding

typescript
bindingsManager.createBinding(
  'philipsHue',
  'local-network',
  ['setLight', 'getStatus', 'setScene'],
  { allowPrivateIPs: true }
);

bindingsManager.createAuthenticatedClient('philipsHue', (credential) => ({
  setLight: async (id, state) => {
    await fetch(`http://192.168.1.50/api/${credential.data.apiKey}/lights/${id}/state`, {
      method: 'PUT',
      body: JSON.stringify(state)
    });
  }
}));

Home Assistant Binding

typescript
bindingsManager.createBinding(
  'homeAssistant',
  'local-network',
  ['callService', 'getState'],
  { allowPrivateIPs: true, allowAllLocalhost: true }
);

bindingsManager.createAuthenticatedClient('homeAssistant', (credential) => ({
  callService: async (domain, service, data) => {
    await fetch(`http://homeassistant.local:8123/api/services/${domain}/${service}`, {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${credential.data.token}` },
      body: JSON.stringify(data)
    });
  }
}));

Development Server Binding

typescript
bindingsManager.createBinding(
  'devServer',
  'local-network',
  ['getData', 'postData'],
  { allowAllLocalhost: true }
);

bindingsManager.createAuthenticatedClient('devServer', (credential) => ({
  getData: async (endpoint) => {
    const response = await fetch(`http://localhost:3000${endpoint}`);
    return await response.json();
  }
}));

Summary

Local network MCPs/Photons work perfectly with Code-Mode security:

  • Use bindings pattern (Phase 3)
  • Bindings execute in main thread (full network access)
  • Worker code stays restricted (cannot bypass policy)
  • Network requirements declared and auditable
  • Security maintained, flexibility preserved

Released under the Elastic License 2.0.