- Add @tpmjs/mcp-client package for connecting to MCP servers - Add @tpmjs/bridge CLI for bridging local MCP servers to TPMJS - Add @tpmjs/test-file-writer test MCP server - Add BridgeConnection and CollectionBridgeTool database models - Add /api/bridge endpoints for bridge communication - Add /api/collections/[id]/bridge-tools API for managing bridge tools - Update MCP handlers to include bridge tools in tools/list - Add bridge status UI at /dashboard/settings/bridge - Add interactive bridge tutorial at /docs/tutorials/bridge
39 KiB
TPMJS MCP Aggregator: One MCP Server to Rule Them All
A design document for importing tools from external MCP servers into TPMJS collections, enabling a single unified MCP endpoint.
Table of Contents
- The Vision
- Current State
- The Challenge
- Architecture Options
- Recommended Implementation
- Technical Specifications
- User Experience
- Implementation Phases
The Vision
Goal: Add one MCP server to Claude Desktop and control ALL your tools from TPMJS.
Before (Current State):
┌─────────────────────────────────────────┐
│ Claude Desktop / Cursor / Claude Code │
│ │
│ MCP Servers: │
│ ├── tpmjs.com/mcp/user/my-tools │ ← TPMJS collection
│ ├── chrome-devtools-mcp │ ← Local stdio
│ ├── browser-mcp │ ← Local stdio
│ ├── filesystem-mcp │ ← Local stdio
│ └── slack-mcp │ ← Local stdio
└─────────────────────────────────────────┘
After (With Aggregator):
┌─────────────────────────────────────────┐
│ Claude Desktop / Cursor / Claude Code │
│ │
│ MCP Servers: │
│ └── tpmjs.com/mcp/user/unified │ ← ONE server with ALL tools
│ │
│ Contains: │
│ ├── npm tools (remote) │
│ ├── chrome tools (via bridge) │
│ ├── browser tools (via bridge) │
│ ├── filesystem tools (via bridge) │
│ └── slack tools (via bridge) │
└─────────────────────────────────────────┘
Benefits:
- Single MCP configuration
- Centralized tool management via TPMJS UI
- Mix remote npm tools with local MCP tools
- Easy sharing of tool configurations
- Unified environment variable management
Current State
TPMJS as MCP Server
TPMJS already exposes collections as MCP servers:
Endpoint: /api/mcp/{username}/{slug}/{transport}
Transport: HTTP or SSE
Protocol: JSON-RPC 2.0
Supported Methods:
initialize- Server handshaketools/list- List all tools in collectiontools/call- Execute a tool
Tool Source: Currently only npm packages synced from the TPMJS registry.
What We Need to Add
- MCP Client Capability: Connect TO other MCP servers
- Tool Import: Pull tool definitions from external MCP servers
- Proxy Execution: Route tool calls to original MCP server
- Bridge Infrastructure: Handle local stdio-based servers
The Challenge
Transport Mismatch
Most powerful MCP servers use stdio transport which requires local execution:
| MCP Server | Transport | Why |
|---|---|---|
| Chrome DevTools MCP | stdio | Controls local Chrome via DevTools Protocol |
| Claude in Chrome | Native Messaging | Controls user's browser via Chrome extension |
| Browser MCP | stdio + extension | Puppeteer on user's machine |
| Filesystem MCP | stdio | Reads/writes local files |
| Git MCP | stdio | Operates on local git repos |
Problem: TPMJS runs in the cloud. It cannot directly connect to stdio-based MCP servers on user's machines.
The Bridge Requirement
User's Machine TPMJS Cloud
┌────────────────────────────┐ ┌─────────────────────────────┐
│ │ │ │
│ ┌──────────────────────┐ │ │ ┌───────────────────────┐ │
│ │ Chrome DevTools MCP │ │ │ │ TPMJS cannot reach │ │
│ │ (stdio) │ │ │ │ local stdio servers │ │
│ └──────────────────────┘ │ ✗ │ │ directly │ │
│ ┌──────────────────────┐ │────────│ │ │ │
│ │ Filesystem MCP │ │ │ └───────────────────────┘ │
│ │ (stdio) │ │ │ │
│ └──────────────────────┘ │ │ │
└────────────────────────────┘ └─────────────────────────────┘
NEED: A BRIDGE
Architecture Options
Option A: Full Cloud (Limited)
Only support MCP servers that expose HTTP/SSE endpoints.
TPMJS Cloud
┌─────────────────────────────────────────────────────────────┐
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ TPMJS MCP Aggregator │ │
│ │ │ │
│ │ Connects to: │ │
│ │ ├── Remote MCP Server A (HTTP) ✓ │ │
│ │ ├── Remote MCP Server B (SSE) ✓ │ │
│ │ └── Local MCP Server (stdio) ✗ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Pros: Simple, no user setup Cons: Can't use Chrome, filesystem, or other local tools
Option B: User-Hosted Bridge (CLI)
User runs a bridge CLI that connects local MCP servers to TPMJS.
User's Machine TPMJS Cloud
┌────────────────────────────────┐ ┌─────────────────────────────┐
│ │ │ │
│ ┌──────────────────────────┐ │ │ ┌───────────────────────┐ │
│ │ tpmjs-bridge CLI │◀─┼── WSS ──┼─▶│ TPMJS API │ │
│ │ │ │ │ │ │ │
│ │ Connects to local MCP: │ │ │ │ Routes tool calls │ │
│ │ ├── chrome-devtools │ │ │ │ to user's bridge │ │
│ │ ├── filesystem │ │ │ │ │ │
│ │ └── custom servers │ │ │ └───────────────────────┘ │
│ └──────────────────────────┘ │ │ │
│ │ │ │ │
│ ▼ │ │ │
│ ┌──────────────────────────┐ │ │ │
│ │ Local MCP Servers │ │ │ │
│ │ (stdio) │ │ │ │
│ └──────────────────────────┘ │ │ │
└────────────────────────────────┘ └─────────────────────────────┘
Flow:
- User runs:
npx tpmjs-bridge --servers chrome-devtools,filesystem - Bridge connects to TPMJS via WebSocket
- Bridge discovers tools from local MCP servers
- TPMJS receives tool definitions
- Tool calls route: TPMJS → Bridge → Local MCP → Result → Bridge → TPMJS
Pros: Full local tool access, works with any MCP server Cons: Requires CLI running, connection management
Option C: Browser Extension Bridge
Use browser extension with native messaging for bridge functionality.
Browser (with TPMJS Extension)
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ TPMJS Web App TPMJS Extension │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ │◀─ msgs ─▶│ Native Messaging Host │ │
│ │ Tool Management │ │ ┌───────────────────────┐ │ │
│ │ UI │ │ │ Connects to MCP │ │ │
│ └─────────────────────┘ │ │ servers via stdio │ │ │
│ │ └───────────────────────┘ │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────┐
│ Local MCP │
│ Servers (stdio) │
└───────────────────┘
Pros: No CLI needed, browser-native Cons: Complex setup, browser-dependent
Option D: Hybrid Approach (Recommended)
Combine cloud + bridge for best of both worlds:
┌────────────────────────────────────────────────────────────────────────┐
│ TPMJS Platform │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ MCP Aggregator Service │ │
│ │ │ │
│ │ Tool Sources: │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ npm Registry │ │ Remote MCP │ │ User Bridge │ │ │
│ │ │ (always avail) │ │ (HTTP/SSE) │ │ (when online) │ │ │
│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌────────────────────────────────────────────────────────────┐ │ │
│ │ │ Unified Tool Registry │ │ │
│ │ │ │ │ │
│ │ │ Tools: │ │ │
│ │ │ ├── @tpmjs/hello.helloWorld [npm] ✓ always │ │ │
│ │ │ ├── slack.postMessage [remote] ✓ always │ │ │
│ │ │ ├── chrome.navigate [bridge] ? online │ │ │
│ │ │ └── filesystem.readFile [bridge] ? online │ │ │
│ │ └────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌────────────────────────────────────────────────────────────┐ │ │
│ │ │ MCP Server Endpoint │ │ │
│ │ │ /api/mcp/{user}/{collection}/http │ │ │
│ │ └────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────┘
│ ▲
▼ │
┌─────────────────────────┐ WebSocket │
│ MCP Client │─────────────────┘
│ (Claude Desktop, etc.) │
└─────────────────────────┘
User's Machine
┌─────────────────────────────────────┐
│ │
│ ┌───────────────────────────────┐ │
│ │ tpmjs-bridge │ │
│ │ Connected to TPMJS via WSS │◀──── (WebSocket)
│ │ │ │
│ │ Local MCP Servers: │ │
│ │ ├── chrome-devtools (stdio) │ │
│ │ ├── filesystem (stdio) │ │
│ │ └── custom (stdio) │ │
│ └───────────────────────────────┘ │
│ │
└─────────────────────────────────────┘
Recommended Implementation
Core Components
1. MCP Client Library (@tpmjs/mcp-client)
A package that can connect to MCP servers and proxy their tools.
// packages/mcp-client/src/index.ts
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
export interface MCPServerConfig {
id: string;
name: string;
transport: 'stdio' | 'http' | 'sse';
// For stdio
command?: string;
args?: string[];
// For http/sse
url?: string;
headers?: Record<string, string>;
}
export class MCPClientManager {
private clients: Map<string, Client> = new Map();
async connect(config: MCPServerConfig): Promise<void> {
const client = new Client({
name: 'tpmjs-aggregator',
version: '1.0.0',
});
let transport;
if (config.transport === 'stdio') {
transport = new StdioClientTransport({
command: config.command!,
args: config.args || [],
});
} else {
transport = new StreamableHTTPClientTransport(
new URL(config.url!),
{ headers: config.headers }
);
}
await client.connect(transport);
this.clients.set(config.id, client);
}
async listTools(serverId: string) {
const client = this.clients.get(serverId);
if (!client) throw new Error(`Server ${serverId} not connected`);
return client.listTools();
}
async callTool(serverId: string, name: string, args: unknown) {
const client = this.clients.get(serverId);
if (!client) throw new Error(`Server ${serverId} not connected`);
return client.callTool({ name, arguments: args as Record<string, unknown> });
}
async disconnect(serverId: string) {
const client = this.clients.get(serverId);
if (client) {
await client.close();
this.clients.delete(serverId);
}
}
}
2. Bridge CLI (tpmjs-bridge)
Runs on user's machine, connects local MCP servers to TPMJS.
// packages/tpmjs-bridge/src/index.ts
#!/usr/bin/env node
import { MCPClientManager, MCPServerConfig } from '@tpmjs/mcp-client';
import WebSocket from 'ws';
interface BridgeConfig {
apiKey: string;
tpmjsUrl: string;
servers: MCPServerConfig[];
}
class TPMJSBridge {
private mcpManager: MCPClientManager;
private ws: WebSocket | null = null;
private config: BridgeConfig;
constructor(config: BridgeConfig) {
this.config = config;
this.mcpManager = new MCPClientManager();
}
async start() {
// 1. Connect to all local MCP servers
for (const server of this.config.servers) {
console.log(`Connecting to ${server.name}...`);
await this.mcpManager.connect(server);
}
// 2. Gather all tools from connected servers
const allTools = [];
for (const server of this.config.servers) {
const { tools } = await this.mcpManager.listTools(server.id);
allTools.push(...tools.map(t => ({
...t,
serverId: server.id,
serverName: server.name,
})));
}
// 3. Connect to TPMJS WebSocket
this.ws = new WebSocket(
`${this.config.tpmjsUrl}/api/bridge?token=${this.config.apiKey}`
);
this.ws.on('open', () => {
console.log('Connected to TPMJS');
// Register available tools
this.ws!.send(JSON.stringify({
type: 'register',
tools: allTools,
}));
});
this.ws.on('message', async (data) => {
const message = JSON.parse(data.toString());
if (message.type === 'tool_call') {
// Execute tool via local MCP server
const result = await this.mcpManager.callTool(
message.serverId,
message.toolName,
message.args
);
// Send result back
this.ws!.send(JSON.stringify({
type: 'tool_result',
callId: message.callId,
result,
}));
}
});
this.ws.on('close', () => {
console.log('Disconnected from TPMJS, reconnecting...');
setTimeout(() => this.start(), 5000);
});
}
}
// CLI entry point
const config = loadConfig(); // from ~/.tpmjs/bridge.json
const bridge = new TPMJSBridge(config);
bridge.start();
3. Bridge WebSocket API (/api/bridge)
Server-side handler for bridge connections.
// apps/web/src/app/api/bridge/route.ts
import { prisma } from '@tpmjs/db';
export const runtime = 'nodejs';
// WebSocket upgrade handler
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const token = searchParams.get('token');
// Validate API key
const user = await validateApiKey(token);
if (!user) {
return new Response('Unauthorized', { status: 401 });
}
// Upgrade to WebSocket
const { socket, response } = Deno.upgradeWebSocket(request);
socket.onmessage = async (event) => {
const message = JSON.parse(event.data);
if (message.type === 'register') {
// Store bridge tools in database
await prisma.bridgeConnection.upsert({
where: { userId: user.id },
update: {
tools: message.tools,
lastSeen: new Date(),
status: 'connected',
},
create: {
userId: user.id,
tools: message.tools,
lastSeen: new Date(),
status: 'connected',
},
});
}
if (message.type === 'tool_result') {
// Forward result to waiting request
pendingCalls.get(message.callId)?.resolve(message.result);
}
};
socket.onclose = async () => {
await prisma.bridgeConnection.update({
where: { userId: user.id },
update: { status: 'disconnected' },
});
};
return response;
}
4. Database Schema Updates
// packages/db/prisma/schema.prisma
// Track connected bridges
model BridgeConnection {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id])
tools Json // Array of tool definitions from bridge
status String // 'connected' | 'disconnected'
lastSeen DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Track external MCP servers added to collections
model ExternalMCPServer {
id String @id @default(cuid())
collectionId String
collection Collection @relation(fields: [collectionId], references: [id])
name String
transport String // 'http' | 'sse' | 'bridge'
// For HTTP/SSE
url String?
headers Json? // Encrypted headers
// For bridge (tool IDs from user's connected bridge)
bridgeToolIds String[]
// Cached tool definitions
tools Json?
lastSync DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Update Collection to include external servers
model Collection {
// ... existing fields ...
externalServers ExternalMCPServer[]
}
5. Enhanced MCP Handlers
// apps/web/src/lib/mcp/handlers.ts
export async function handleToolsList(
collection: CollectionWithTools,
userId: string
): Promise<MCPToolsListResult> {
const tools: MCPTool[] = [];
// 1. Add npm-based tools (existing)
for (const ct of collection.tools) {
tools.push(convertToMCPTool(ct.tool));
}
// 2. Add remote MCP server tools
for (const server of collection.externalServers) {
if (server.transport === 'http' || server.transport === 'sse') {
const serverTools = await fetchRemoteMCPTools(server);
tools.push(...serverTools.map(t => ({
...t,
name: `${server.name}--${t.name}`, // Namespace by server
})));
}
}
// 3. Add bridge tools (if user has connected bridge)
const bridge = await prisma.bridgeConnection.findUnique({
where: { userId },
});
if (bridge?.status === 'connected') {
for (const server of collection.externalServers) {
if (server.transport === 'bridge') {
const bridgeTools = bridge.tools.filter(
t => server.bridgeToolIds.includes(t.id)
);
tools.push(...bridgeTools.map(t => ({
...t,
name: `${server.name}--${t.name}`,
})));
}
}
}
return { tools };
}
export async function handleToolsCall(
collection: CollectionWithTools,
userId: string,
toolName: string,
args: unknown
): Promise<MCPToolResult> {
// Parse namespaced tool name
const [serverName, actualToolName] = toolName.split('--');
// Find the server
const server = collection.externalServers.find(s => s.name === serverName);
if (!server) {
// Must be an npm tool, use existing logic
return executeNpmTool(collection, toolName, args);
}
if (server.transport === 'http' || server.transport === 'sse') {
// Call remote MCP server directly
return callRemoteMCPTool(server, actualToolName, args);
}
if (server.transport === 'bridge') {
// Route through user's bridge
return callBridgeTool(userId, server, actualToolName, args);
}
}
async function callBridgeTool(
userId: string,
server: ExternalMCPServer,
toolName: string,
args: unknown
): Promise<MCPToolResult> {
const bridge = await getBridgeConnection(userId);
if (!bridge || bridge.status !== 'connected') {
throw new Error('Bridge not connected. Run `npx tpmjs-bridge` to connect.');
}
// Send tool call through WebSocket
const callId = generateId();
const result = await new Promise((resolve, reject) => {
pendingCalls.set(callId, { resolve, reject });
bridge.socket.send(JSON.stringify({
type: 'tool_call',
callId,
serverId: server.bridgeServerId,
toolName,
args,
}));
// Timeout after 5 minutes
setTimeout(() => {
pendingCalls.delete(callId);
reject(new Error('Bridge tool call timed out'));
}, 300000);
});
return result;
}
Technical Specifications
Tool Naming Convention
To avoid collisions when aggregating from multiple sources:
{source}--{originalName}
Examples:
- npm--@tpmjs/hello--helloWorldTool (npm package)
- chrome-devtools--navigate (remote MCP)
- bridge--filesystem--readFile (bridge MCP)
Transport Priority
When a tool exists in multiple sources:
- npm - Fastest, always available
- Remote HTTP/SSE - Fast, usually available
- Bridge - Requires user connection, variable latency
Error Handling
interface ToolExecutionError {
code: 'BRIDGE_DISCONNECTED' | 'REMOTE_TIMEOUT' | 'TOOL_NOT_FOUND';
message: string;
suggestion?: string;
}
// Examples:
{
code: 'BRIDGE_DISCONNECTED',
message: 'Cannot execute chrome.navigate - bridge not connected',
suggestion: 'Run `npx tpmjs-bridge` to connect your local tools'
}
Security Considerations
- API Key Authentication: Bridge connections require valid API key
- User Isolation: Each user's bridge is isolated
- Tool Whitelisting: Users explicitly add tools to collections
- Encrypted Credentials: Remote MCP server credentials encrypted at rest
- WebSocket Security: WSS (TLS) required for bridge connections
User Experience
Adding Remote MCP Tools via UI
┌────────────────────────────────────────────────────────────────────┐
│ Collection: My Dev Tools │
├────────────────────────────────────────────────────────────────────┤
│ │
│ Tools (12) [+ Add Tools ▼] │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ + Add from npm registry │ │
│ │ + Add from remote MCP server (HTTP/SSE) │ │
│ │ + Add from local bridge │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ ───────────────────────────────────────────────────────────── │
│ │
│ 📦 npm Tools │
│ ├── @tpmjs/hello / helloWorldTool [Remove] │
│ └── @tpmjs/weather / getWeather [Remove] │
│ │
│ 🌐 Remote MCP: slack-mcp (https://slack-mcp.com) │
│ ├── postMessage [Remove] │
│ └── listChannels [Remove] │
│ │
│ 🔗 Bridge: chrome-devtools ● Connected │
│ ├── navigate [Remove] │
│ ├── screenshot [Remove] │
│ └── evaluate [Remove] │
│ │
│ 🔗 Bridge: filesystem ● Connected │
│ └── readFile [Remove] │
│ │
└────────────────────────────────────────────────────────────────────┘
Bridge Setup Flow
┌────────────────────────────────────────────────────────────────────┐
│ Connect Local Tools │
├────────────────────────────────────────────────────────────────────┤
│ │
│ Your local MCP servers can be accessed through TPMJS. │
│ │
│ Step 1: Install the bridge │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ npm install -g @tpmjs/bridge │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ Step 2: Configure your MCP servers │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ tpmjs-bridge init │ │
│ │ │ │
│ │ # This creates ~/.tpmjs/bridge.json with: │ │
│ │ { │ │
│ │ "servers": [ │ │
│ │ { │ │
│ │ "name": "chrome-devtools", │ │
│ │ "command": "npx", │ │
│ │ "args": ["-y", "chrome-devtools-mcp"] │ │
│ │ } │ │
│ │ ] │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ Step 3: Start the bridge │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ tpmjs-bridge start │ │
│ │ │ │
│ │ ✓ Connected to chrome-devtools (5 tools) │ │
│ │ ✓ Connected to TPMJS │ │
│ │ Bridge running. Press Ctrl+C to stop. │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Bridge Status: ● Connected │ │
│ │ Tools Available: 5 │ │
│ │ Last Seen: Just now │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────┘
Unified MCP Configuration
After setup, user only needs ONE MCP server in their config:
// ~/.config/claude/claude_desktop_config.json
{
"mcpServers": {
"tpmjs": {
"type": "url",
"url": "https://tpmjs.com/api/mcp/username/all-my-tools/http"
}
}
}
This single endpoint provides access to:
- All npm tools in the collection
- All remote MCP tools configured
- All local tools via connected bridge
Implementation Phases
Phase 1: Remote MCP Import (2-3 weeks)
Goal: Import tools from remote HTTP/SSE MCP servers
Deliverables:
@tpmjs/mcp-clientpackage for connecting to MCP servers- UI for adding remote MCP server to collection
- Updated MCP handlers to aggregate remote tools
- Tool execution routing for remote servers
No bridge needed - works with any public HTTP MCP server.
Phase 2: Bridge Foundation (3-4 weeks)
Goal: Enable local tool access via bridge
Deliverables:
@tpmjs/bridgeCLI package- WebSocket API for bridge connections (
/api/bridge) - Database schema for bridge connections
- Bridge status UI in dashboard
Phase 3: Tool Discovery & Sync (2 weeks)
Goal: Automatic tool discovery and sync
Deliverables:
- Auto-discover tools when bridge connects
- Sync tool definitions periodically
- Handle schema changes gracefully
- Tool health monitoring
Phase 4: Advanced Features (Ongoing)
Goal: Enhanced reliability and UX
Deliverables:
- Bridge auto-reconnection
- Tool execution queuing
- Offline tool caching
- Multiple bridge support (different machines)
- Browser extension alternative to CLI
Summary
The MCP Aggregator transforms TPMJS from a tool registry into a universal tool hub:
| Feature | Before | After |
|---|---|---|
| Tool Sources | npm only | npm + remote MCP + local MCP |
| MCP Servers | One per collection | One unified endpoint |
| Local Tools | Not possible | Via bridge |
| Chrome/Browser | Not possible | Via bridge |
| Configuration | Multiple MCP entries | Single TPMJS entry |
The hybrid approach (cloud + bridge) provides:
- Always-on npm and remote MCP tools
- When-connected local tools via bridge
- Graceful degradation when bridge is offline
- Single point of management for all tools