From 0413b9be0ee12e27ad1ef69b0c2f8578e11b8220 Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Tue, 13 Jan 2026 00:59:34 +1000 Subject: [PATCH] feat: complete MCP Bridge implementation - 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 --- apps/web/src/app/api/bridge/route.ts | 241 +++ .../[id]/bridge-tools/[bridgeToolId]/route.ts | 260 ++++ .../collections/[id]/bridge-tools/route.ts | 354 +++++ apps/web/src/app/api/user/bridge/route.ts | 84 ++ .../app/dashboard/settings/bridge/page.tsx | 282 ++++ .../src/app/docs/tutorials/bridge/page.tsx | 712 +++++++++ apps/web/src/app/docs/tutorials/page.tsx | 9 + .../components/dashboard/DashboardLayout.tsx | 1 + apps/web/src/lib/mcp/handlers.ts | 201 ++- apps/web/src/lib/mcp/index.ts | 11 +- apps/web/src/lib/mcp/tool-converter.ts | 72 +- docs/MCP-AGGREGATOR-DESIGN.md | 909 ++++++++++++ docs/MCP-BRIDGE-STATUS.md | 319 ++++ docs/PRD-MCP-BRIDGE.md | 1298 +++++++++++++++++ docs/TPMJS-ARCHITECTURE.md | 930 ++++++++++++ packages/bridge/package.json | 56 + packages/bridge/src/bridge.ts | 275 ++++ packages/bridge/src/cli.ts | 232 +++ packages/bridge/src/config.ts | 113 ++ packages/bridge/src/index.ts | 18 + packages/bridge/src/types.ts | 84 ++ packages/bridge/tsconfig.json | 11 + packages/bridge/tsup.config.ts | 21 + packages/db/prisma/schema.prisma | 77 +- packages/mcp-client/package.json | 48 + packages/mcp-client/src/client-manager.ts | 213 +++ packages/mcp-client/src/index.ts | 8 + packages/mcp-client/src/types.ts | 64 + packages/mcp-client/tsconfig.json | 11 + packages/mcp-client/tsup.config.ts | 9 + packages/tools/test-file-writer/package.json | 37 + packages/tools/test-file-writer/src/index.ts | 3 + packages/tools/test-file-writer/src/server.ts | 249 ++++ packages/tools/test-file-writer/tsconfig.json | 11 + .../tools/test-file-writer/tsup.config.ts | 21 + packages/types/src/collection.ts | 19 + pnpm-lock.yaml | 78 +- 37 files changed, 7318 insertions(+), 23 deletions(-) create mode 100644 apps/web/src/app/api/bridge/route.ts create mode 100644 apps/web/src/app/api/collections/[id]/bridge-tools/[bridgeToolId]/route.ts create mode 100644 apps/web/src/app/api/collections/[id]/bridge-tools/route.ts create mode 100644 apps/web/src/app/api/user/bridge/route.ts create mode 100644 apps/web/src/app/dashboard/settings/bridge/page.tsx create mode 100644 apps/web/src/app/docs/tutorials/bridge/page.tsx create mode 100644 docs/MCP-AGGREGATOR-DESIGN.md create mode 100644 docs/MCP-BRIDGE-STATUS.md create mode 100644 docs/PRD-MCP-BRIDGE.md create mode 100644 docs/TPMJS-ARCHITECTURE.md create mode 100644 packages/bridge/package.json create mode 100644 packages/bridge/src/bridge.ts create mode 100644 packages/bridge/src/cli.ts create mode 100644 packages/bridge/src/config.ts create mode 100644 packages/bridge/src/index.ts create mode 100644 packages/bridge/src/types.ts create mode 100644 packages/bridge/tsconfig.json create mode 100644 packages/bridge/tsup.config.ts create mode 100644 packages/mcp-client/package.json create mode 100644 packages/mcp-client/src/client-manager.ts create mode 100644 packages/mcp-client/src/index.ts create mode 100644 packages/mcp-client/src/types.ts create mode 100644 packages/mcp-client/tsconfig.json create mode 100644 packages/mcp-client/tsup.config.ts create mode 100644 packages/tools/test-file-writer/package.json create mode 100644 packages/tools/test-file-writer/src/index.ts create mode 100644 packages/tools/test-file-writer/src/server.ts create mode 100644 packages/tools/test-file-writer/tsconfig.json create mode 100644 packages/tools/test-file-writer/tsup.config.ts diff --git a/apps/web/src/app/api/bridge/route.ts b/apps/web/src/app/api/bridge/route.ts new file mode 100644 index 0000000..8ddfaf4 --- /dev/null +++ b/apps/web/src/app/api/bridge/route.ts @@ -0,0 +1,241 @@ +import { prisma } from '@tpmjs/db'; +import { type NextRequest, NextResponse } from 'next/server'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +/** + * Bridge Registration & Status API + * + * POST: Register bridge tools + * GET: Get bridge status and pending tool calls + * DELETE: Disconnect bridge + */ + +// Validate API key and get user +async function validateApiKey(token: string | null | undefined) { + if (!token) return null; + + // For now, use session-based auth + // In production, you'd want proper API key validation with encrypted keys + const session = await prisma.session.findUnique({ + where: { token }, + include: { user: true }, + }); + + return session?.user || null; +} + +// POST: Register bridge and its tools +export async function POST(request: NextRequest) { + try { + const authHeader = request.headers.get('authorization'); + const token = authHeader?.replace('Bearer ', ''); + + const user = await validateApiKey(token); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const body = await request.json(); + const { type, tools, callId, result, error: toolError } = body; + + if (type === 'register') { + // Register bridge and tools + await prisma.bridgeConnection.upsert({ + where: { userId: user.id }, + update: { + status: 'connected', + tools: tools || [], + lastSeen: new Date(), + clientVersion: body.clientVersion, + clientOS: body.clientOS, + }, + create: { + userId: user.id, + status: 'connected', + tools: tools || [], + lastSeen: new Date(), + clientVersion: body.clientVersion, + clientOS: body.clientOS, + }, + }); + + return NextResponse.json({ + success: true, + message: `Registered ${tools?.length || 0} tools`, + }); + } + + if (type === 'tool_result') { + // Store tool result for polling + // We use a simple in-memory store for now + // In production, use Redis or similar + const key = `bridge_result:${callId}`; + pendingResults.set(key, { result, error: toolError, timestamp: Date.now() }); + + return NextResponse.json({ success: true }); + } + + if (type === 'heartbeat') { + // Update last seen + await prisma.bridgeConnection.update({ + where: { userId: user.id }, + data: { lastSeen: new Date() }, + }); + + return NextResponse.json({ success: true }); + } + + return NextResponse.json({ error: 'Invalid type' }, { status: 400 }); + } catch (error) { + console.error('Bridge POST error:', error); + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Internal error' }, + { status: 500 } + ); + } +} + +// GET: Get pending tool calls (polling) +export async function GET(request: NextRequest) { + try { + const authHeader = request.headers.get('authorization'); + const token = authHeader?.replace('Bearer ', ''); + + const user = await validateApiKey(token); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // Get pending tool calls for this user + const pendingCalls = Array.from(pendingToolCalls.entries()) + .filter(([key]) => key.startsWith(`${user.id}:`)) + .map(([key, value]) => { + pendingToolCalls.delete(key); // Remove after returning + return value; + }); + + // Update last seen + await prisma.bridgeConnection.update({ + where: { userId: user.id }, + data: { lastSeen: new Date() }, + }); + + return NextResponse.json({ + success: true, + calls: pendingCalls, + }); + } catch (error) { + console.error('Bridge GET error:', error); + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Internal error' }, + { status: 500 } + ); + } +} + +// DELETE: Disconnect bridge +export async function DELETE(request: NextRequest) { + try { + const authHeader = request.headers.get('authorization'); + const token = authHeader?.replace('Bearer ', ''); + + const user = await validateApiKey(token); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + await prisma.bridgeConnection.update({ + where: { userId: user.id }, + data: { status: 'disconnected' }, + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Bridge DELETE error:', error); + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Internal error' }, + { status: 500 } + ); + } +} + +// In-memory stores for tool calls and results +// In production, use Redis or a proper message queue +const pendingToolCalls = new Map< + string, + { + callId: string; + serverId: string; + toolName: string; + args: Record; + timestamp: number; + } +>(); + +const pendingResults = new Map< + string, + { + result?: unknown; + error?: { code: string; message: string }; + timestamp: number; + } +>(); + +// Helper function to queue a tool call for a user's bridge +export function queueBridgeToolCall( + userId: string, + callId: string, + serverId: string, + toolName: string, + args: Record +): void { + const key = `${userId}:${callId}`; + pendingToolCalls.set(key, { + callId, + serverId, + toolName, + args, + timestamp: Date.now(), + }); +} + +// Helper function to wait for a tool result +export async function waitForBridgeResult( + callId: string, + timeoutMs: number = 300000 // 5 minutes +): Promise<{ result?: unknown; error?: { code: string; message: string } }> { + const key = `bridge_result:${callId}`; + const startTime = Date.now(); + + while (Date.now() - startTime < timeoutMs) { + const result = pendingResults.get(key); + if (result) { + pendingResults.delete(key); + return result; + } + // Wait 100ms before checking again + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + return { error: { code: 'TIMEOUT', message: 'Bridge tool call timed out' } }; +} + +// Cleanup old entries periodically +setInterval(() => { + const now = Date.now(); + const maxAge = 5 * 60 * 1000; // 5 minutes + + for (const [key, value] of pendingToolCalls.entries()) { + if (now - value.timestamp > maxAge) { + pendingToolCalls.delete(key); + } + } + + for (const [key, value] of pendingResults.entries()) { + if (now - value.timestamp > maxAge) { + pendingResults.delete(key); + } + } +}, 60000); // Run every minute diff --git a/apps/web/src/app/api/collections/[id]/bridge-tools/[bridgeToolId]/route.ts b/apps/web/src/app/api/collections/[id]/bridge-tools/[bridgeToolId]/route.ts new file mode 100644 index 0000000..7762353 --- /dev/null +++ b/apps/web/src/app/api/collections/[id]/bridge-tools/[bridgeToolId]/route.ts @@ -0,0 +1,260 @@ +import { prisma } from '@tpmjs/db'; +import { UpdateCollectionBridgeToolSchema } from '@tpmjs/types/collection'; +import { headers } from 'next/headers'; +import { type NextRequest, NextResponse } from 'next/server'; +import { auth } from '~/lib/auth'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; +export const maxDuration = 60; + +const API_VERSION = '1.0.0'; + +interface ApiResponse { + success: boolean; + data?: T; + error?: { + code: string; + message: string; + details?: Record; + }; + meta: { + version: string; + timestamp: string; + requestId?: string; + }; +} + +interface RouteContext { + params: Promise<{ id: string; bridgeToolId: string }>; +} + +/** + * PATCH /api/collections/[id]/bridge-tools/[bridgeToolId] + * Update a bridge tool in a collection + */ +export async function PATCH( + request: NextRequest, + context: RouteContext +): Promise> { + const requestId = crypto.randomUUID(); + const { id: collectionId, bridgeToolId } = await context.params; + + try { + // Check authentication + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session) { + return NextResponse.json( + { + success: false, + error: { code: 'UNAUTHORIZED', message: 'Authentication required' }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 401 } + ); + } + + // Verify collection exists and user owns it + const collection = await prisma.collection.findUnique({ + where: { id: collectionId }, + }); + + if (!collection) { + return NextResponse.json( + { + success: false, + error: { code: 'NOT_FOUND', message: 'Collection not found' }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 404 } + ); + } + + if (collection.userId !== session.user.id) { + return NextResponse.json( + { + success: false, + error: { code: 'FORBIDDEN', message: 'Access denied' }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 403 } + ); + } + + // Verify bridge tool exists in collection + const bridgeTool = await prisma.collectionBridgeTool.findFirst({ + where: { + id: bridgeToolId, + collectionId, + }, + }); + + if (!bridgeTool) { + return NextResponse.json( + { + success: false, + error: { code: 'NOT_FOUND', message: 'Bridge tool not found in collection' }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 404 } + ); + } + + // Parse and validate request body + const body = await request.json(); + const parseResult = UpdateCollectionBridgeToolSchema.safeParse(body); + + if (!parseResult.success) { + return NextResponse.json( + { + success: false, + error: { + code: 'VALIDATION_ERROR', + message: 'Invalid request body', + details: { errors: parseResult.error.flatten().fieldErrors }, + }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 400 } + ); + } + + const { displayName, note } = parseResult.data; + + // Update the bridge tool + const updated = await prisma.collectionBridgeTool.update({ + where: { id: bridgeToolId }, + data: { + ...(displayName !== undefined && { displayName }), + ...(note !== undefined && { note }), + }, + }); + + return NextResponse.json({ + success: true, + data: { + id: updated.id, + serverId: updated.serverId, + toolName: updated.toolName, + displayName: updated.displayName, + note: updated.note, + updatedAt: updated.updatedAt, + }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }); + } catch (error) { + console.error('[API Error] PATCH /api/collections/[id]/bridge-tools/[bridgeToolId]:', error); + return NextResponse.json( + { + success: false, + error: { code: 'INTERNAL_ERROR', message: 'Failed to update bridge tool' }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 500 } + ); + } +} + +/** + * DELETE /api/collections/[id]/bridge-tools/[bridgeToolId] + * Remove a bridge tool from a collection + */ +export async function DELETE( + _request: NextRequest, + context: RouteContext +): Promise> { + const requestId = crypto.randomUUID(); + const { id: collectionId, bridgeToolId } = await context.params; + + try { + // Check authentication + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session) { + return NextResponse.json( + { + success: false, + error: { code: 'UNAUTHORIZED', message: 'Authentication required' }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 401 } + ); + } + + // Verify collection exists and user owns it + const collection = await prisma.collection.findUnique({ + where: { id: collectionId }, + }); + + if (!collection) { + return NextResponse.json( + { + success: false, + error: { code: 'NOT_FOUND', message: 'Collection not found' }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 404 } + ); + } + + if (collection.userId !== session.user.id) { + return NextResponse.json( + { + success: false, + error: { code: 'FORBIDDEN', message: 'Access denied' }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 403 } + ); + } + + // Verify bridge tool exists in collection + const bridgeTool = await prisma.collectionBridgeTool.findFirst({ + where: { + id: bridgeToolId, + collectionId, + }, + }); + + if (!bridgeTool) { + return NextResponse.json( + { + success: false, + error: { code: 'NOT_FOUND', message: 'Bridge tool not found in collection' }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 404 } + ); + } + + // Delete the bridge tool from collection + await prisma.collectionBridgeTool.delete({ + where: { id: bridgeToolId }, + }); + + return NextResponse.json({ + success: true, + data: { + deleted: true, + serverId: bridgeTool.serverId, + toolName: bridgeTool.toolName, + }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }); + } catch (error) { + console.error('[API Error] DELETE /api/collections/[id]/bridge-tools/[bridgeToolId]:', error); + return NextResponse.json( + { + success: false, + error: { code: 'INTERNAL_ERROR', message: 'Failed to remove bridge tool' }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 500 } + ); + } +} diff --git a/apps/web/src/app/api/collections/[id]/bridge-tools/route.ts b/apps/web/src/app/api/collections/[id]/bridge-tools/route.ts new file mode 100644 index 0000000..ba05d5a --- /dev/null +++ b/apps/web/src/app/api/collections/[id]/bridge-tools/route.ts @@ -0,0 +1,354 @@ +import { prisma } from '@tpmjs/db'; +import { AddBridgeToolToCollectionSchema, COLLECTION_LIMITS } from '@tpmjs/types/collection'; +import { headers } from 'next/headers'; +import { type NextRequest, NextResponse } from 'next/server'; +import { auth } from '~/lib/auth'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; +export const maxDuration = 60; + +const API_VERSION = '1.0.0'; + +interface ApiResponse { + success: boolean; + data?: T; + error?: { + code: string; + message: string; + details?: Record; + }; + meta: { + version: string; + timestamp: string; + requestId?: string; + }; +} + +interface RouteContext { + params: Promise<{ id: string }>; +} + +/** + * GET /api/collections/[id]/bridge-tools + * List all bridge tools in a collection + */ +export async function GET( + _request: NextRequest, + context: RouteContext +): Promise> { + const requestId = crypto.randomUUID(); + const { id: collectionId } = await context.params; + + try { + // Check authentication + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session) { + return NextResponse.json( + { + success: false, + error: { code: 'UNAUTHORIZED', message: 'Authentication required' }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 401 } + ); + } + + // Verify collection exists and user owns it + const collection = await prisma.collection.findUnique({ + where: { id: collectionId }, + include: { + bridgeTools: { + orderBy: { createdAt: 'asc' }, + }, + user: { + include: { + bridgeConnection: true, + }, + }, + }, + }); + + if (!collection) { + return NextResponse.json( + { + success: false, + error: { code: 'NOT_FOUND', message: 'Collection not found' }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 404 } + ); + } + + if (collection.userId !== session.user.id) { + return NextResponse.json( + { + success: false, + error: { code: 'FORBIDDEN', message: 'Access denied' }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 403 } + ); + } + + // Get available bridge tools from the user's bridge connection + interface BridgeTool { + serverId: string; + serverName: string; + name: string; + description?: string; + inputSchema?: Record; + } + + const availableBridgeTools = + (collection.user.bridgeConnection?.tools as unknown as BridgeTool[]) || []; + const bridgeStatus = collection.user.bridgeConnection?.status || 'disconnected'; + + // Enrich collection bridge tools with definitions + const enrichedTools = collection.bridgeTools.map((bt) => { + const definition = availableBridgeTools.find( + (t) => t.serverId === bt.serverId && t.name === bt.toolName + ); + return { + id: bt.id, + serverId: bt.serverId, + toolName: bt.toolName, + displayName: bt.displayName, + note: bt.note, + createdAt: bt.createdAt, + // Include definition info if available + serverName: definition?.serverName, + description: definition?.description, + available: !!definition && bridgeStatus === 'connected', + }; + }); + + return NextResponse.json({ + success: true, + data: { + bridgeTools: enrichedTools, + bridgeStatus, + availableTools: bridgeStatus === 'connected' ? availableBridgeTools : [], + }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }); + } catch (error) { + console.error('[API Error] GET /api/collections/[id]/bridge-tools:', error); + return NextResponse.json( + { + success: false, + error: { code: 'INTERNAL_ERROR', message: 'Failed to list bridge tools' }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 500 } + ); + } +} + +/** + * POST /api/collections/[id]/bridge-tools + * Add a bridge tool to a collection + */ +export async function POST( + request: NextRequest, + context: RouteContext +): Promise> { + const requestId = crypto.randomUUID(); + const { id: collectionId } = await context.params; + + try { + // Check authentication + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session) { + return NextResponse.json( + { + success: false, + error: { code: 'UNAUTHORIZED', message: 'Authentication required' }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 401 } + ); + } + + // Verify collection exists and user owns it + const collection = await prisma.collection.findUnique({ + where: { id: collectionId }, + include: { + _count: { select: { bridgeTools: true } }, + user: { + include: { + bridgeConnection: true, + }, + }, + }, + }); + + if (!collection) { + return NextResponse.json( + { + success: false, + error: { code: 'NOT_FOUND', message: 'Collection not found' }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 404 } + ); + } + + if (collection.userId !== session.user.id) { + return NextResponse.json( + { + success: false, + error: { code: 'FORBIDDEN', message: 'Access denied' }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 403 } + ); + } + + // Parse and validate request body + const body = await request.json(); + const parseResult = AddBridgeToolToCollectionSchema.safeParse(body); + + if (!parseResult.success) { + return NextResponse.json( + { + success: false, + error: { + code: 'VALIDATION_ERROR', + message: 'Invalid request body', + details: { errors: parseResult.error.flatten().fieldErrors }, + }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 400 } + ); + } + + const { serverId, toolName, displayName, note } = parseResult.data; + + // Check bridge tool limit + if (collection._count.bridgeTools >= COLLECTION_LIMITS.MAX_BRIDGE_TOOLS_PER_COLLECTION) { + return NextResponse.json( + { + success: false, + error: { + code: 'LIMIT_EXCEEDED', + message: `Maximum ${COLLECTION_LIMITS.MAX_BRIDGE_TOOLS_PER_COLLECTION} bridge tools per collection`, + }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 400 } + ); + } + + // Check if the user has a bridge connection with this tool + const bridgeConnection = collection.user.bridgeConnection; + if (!bridgeConnection) { + return NextResponse.json( + { + success: false, + error: { + code: 'NO_BRIDGE', + message: 'No bridge connection found. Please start the bridge CLI first.', + }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 400 } + ); + } + + interface BridgeTool { + serverId: string; + serverName: string; + name: string; + description?: string; + } + + const availableTools = (bridgeConnection.tools as unknown as BridgeTool[]) || []; + const bridgeTool = availableTools.find((t) => t.serverId === serverId && t.name === toolName); + + if (!bridgeTool) { + return NextResponse.json( + { + success: false, + error: { + code: 'TOOL_NOT_FOUND', + message: `Tool ${toolName} from server ${serverId} not found in your bridge connection`, + }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 404 } + ); + } + + // Check if tool is already in collection + const existingEntry = await prisma.collectionBridgeTool.findUnique({ + where: { + collectionId_serverId_toolName: { + collectionId, + serverId, + toolName, + }, + }, + }); + + if (existingEntry) { + return NextResponse.json( + { + success: false, + error: { + code: 'DUPLICATE_TOOL', + message: 'This bridge tool is already in the collection', + }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 409 } + ); + } + + // Add bridge tool to collection + const collectionBridgeTool = await prisma.collectionBridgeTool.create({ + data: { + collectionId, + serverId, + toolName, + displayName: displayName || null, + note: note || null, + }, + }); + + return NextResponse.json( + { + success: true, + data: { + id: collectionBridgeTool.id, + serverId: collectionBridgeTool.serverId, + toolName: collectionBridgeTool.toolName, + displayName: collectionBridgeTool.displayName, + note: collectionBridgeTool.note, + createdAt: collectionBridgeTool.createdAt, + serverName: bridgeTool.serverName, + description: bridgeTool.description, + }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 201 } + ); + } catch (error) { + console.error('[API Error] POST /api/collections/[id]/bridge-tools:', error); + return NextResponse.json( + { + success: false, + error: { code: 'INTERNAL_ERROR', message: 'Failed to add bridge tool' }, + meta: { version: API_VERSION, timestamp: new Date().toISOString(), requestId }, + }, + { status: 500 } + ); + } +} diff --git a/apps/web/src/app/api/user/bridge/route.ts b/apps/web/src/app/api/user/bridge/route.ts new file mode 100644 index 0000000..3549e1a --- /dev/null +++ b/apps/web/src/app/api/user/bridge/route.ts @@ -0,0 +1,84 @@ +import { prisma } from '@tpmjs/db'; +import { headers } from 'next/headers'; +import { NextResponse } from 'next/server'; +import { auth } from '~/lib/auth'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +/** + * GET /api/user/bridge - Get current user's bridge status + */ +export async function GET() { + try { + const headersList = await headers(); + const session = await auth.api.getSession({ headers: headersList }); + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const bridge = await prisma.bridgeConnection.findUnique({ + where: { userId: session.user.id }, + }); + + if (!bridge) { + return NextResponse.json({ + success: true, + data: { + status: 'never_connected', + tools: [], + lastSeen: null, + }, + }); + } + + // Check if bridge is stale (not seen in 2 minutes) + const isStale = bridge.lastSeen ? Date.now() - bridge.lastSeen.getTime() > 2 * 60 * 1000 : true; + + const status = isStale && bridge.status === 'connected' ? 'stale' : bridge.status; + + // Parse tools from JSON + const tools = + (bridge.tools as Array<{ + serverId: string; + serverName: string; + name: string; + description?: string; + }>) || []; + + // Group tools by server + const servers: Record = {}; + for (const tool of tools) { + const server = servers[tool.serverId] ?? { + name: tool.serverName || tool.serverId, + tools: [], + }; + servers[tool.serverId] = server; + server.tools.push(tool.name); + } + + return NextResponse.json({ + success: true, + data: { + status, + lastSeen: bridge.lastSeen?.toISOString() || null, + clientVersion: bridge.clientVersion, + clientOS: bridge.clientOS, + toolCount: tools.length, + servers: Object.entries(servers).map(([id, data]) => ({ + id, + name: data.name, + toolCount: data.tools.length, + tools: data.tools, + })), + }, + }); + } catch (error) { + console.error('Failed to get bridge status:', error); + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Internal error' }, + { status: 500 } + ); + } +} diff --git a/apps/web/src/app/dashboard/settings/bridge/page.tsx b/apps/web/src/app/dashboard/settings/bridge/page.tsx new file mode 100644 index 0000000..4c3acb6 --- /dev/null +++ b/apps/web/src/app/dashboard/settings/bridge/page.tsx @@ -0,0 +1,282 @@ +'use client'; + +import { Badge } from '@tpmjs/ui/Badge/Badge'; +import { Button } from '@tpmjs/ui/Button/Button'; +import { Icon } from '@tpmjs/ui/Icon/Icon'; +import { + Table, + TableBody, + TableCell, + TableEmpty, + TableHead, + TableHeader, + TableRow, +} from '@tpmjs/ui/Table/Table'; +import { useCallback, useEffect, useState } from 'react'; +import { DashboardLayout } from '~/components/dashboard/DashboardLayout'; + +interface BridgeServer { + id: string; + name: string; + toolCount: number; + tools: string[]; +} + +interface BridgeStatus { + status: 'connected' | 'disconnected' | 'stale' | 'never_connected'; + lastSeen: string | null; + clientVersion: string | null; + clientOS: string | null; + toolCount: number; + servers: BridgeServer[]; +} + +function getStatusBadge(status: BridgeStatus['status']) { + switch (status) { + case 'connected': + return Connected; + case 'stale': + return Stale; + case 'disconnected': + return Disconnected; + case 'never_connected': + return Never Connected; + } +} + +function formatLastSeen(lastSeen: string | null): string { + if (!lastSeen) return 'Never'; + const date = new Date(lastSeen); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffSecs = Math.floor(diffMs / 1000); + const diffMins = Math.floor(diffSecs / 60); + const diffHours = Math.floor(diffMins / 60); + + if (diffSecs < 60) return 'Just now'; + if (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? 's' : ''} ago`; + if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`; + return date.toLocaleDateString(); +} + +export default function BridgePage(): React.ReactElement { + const [bridgeStatus, setBridgeStatus] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchStatus = useCallback(async () => { + try { + const response = await fetch('/api/user/bridge'); + const data = await response.json(); + if (data.success) { + setBridgeStatus(data.data); + } else { + setError(data.error || 'Failed to fetch bridge status'); + } + } catch (err) { + console.error('Failed to fetch bridge status:', err); + setError('Failed to fetch bridge status'); + } finally { + setIsLoading(false); + } + }, []); + + useEffect(() => { + fetchStatus(); + // Refresh every 10 seconds + const interval = setInterval(fetchStatus, 10000); + return () => clearInterval(interval); + }, [fetchStatus]); + + if (error) { + return ( + +
+ +

Error

+

{error}

+ +
+
+ ); + } + + return ( + + + Refresh + + } + > + {/* Status Card */} +
+
+

Connection Status

+ {!isLoading && bridgeStatus && getStatusBadge(bridgeStatus.status)} +
+ + {isLoading ? ( +
+
+
+
+ ) : bridgeStatus?.status === 'never_connected' ? ( +
+

+ The TPMJS Bridge allows you to connect local MCP servers (like Chrome DevTools, file + systems, or custom tools) to your TPMJS collections. +

+
+

Quick Start

+
    +
  1. + 1. Install the bridge CLI:{' '} + + npm install -g @tpmjs/bridge + +
  2. +
  3. + 2. Initialize:{' '} + + tpmjs-bridge init + +
  4. +
  5. + 3. Add an MCP server:{' '} + + tpmjs-bridge add chrome-devtools + +
  6. +
  7. + 4. Start the bridge:{' '} + + tpmjs-bridge start + +
  8. +
+
+
+ ) : ( +
+
+
+ Last Seen: + + {formatLastSeen(bridgeStatus?.lastSeen ?? null)} + +
+
+ Tools: + {bridgeStatus?.toolCount ?? 0} +
+ {bridgeStatus?.clientVersion && ( +
+ Version: + {bridgeStatus.clientVersion} +
+ )} + {bridgeStatus?.clientOS && ( +
+ Platform: + {bridgeStatus.clientOS} +
+ )} +
+
+ )} +
+ + {/* Connected Servers Table */} + {!isLoading && bridgeStatus && bridgeStatus.servers.length > 0 && ( +
+
+

Connected MCP Servers

+
+ + + + Server + Tools + Available Tools + + + + {bridgeStatus.servers.map((server) => ( + + +
+
+ +
+ {server.name} +
+
+ + {server.toolCount} + + +
+ {server.tools.slice(0, 5).map((tool) => ( + + {tool} + + ))} + {server.tools.length > 5 && ( + + +{server.tools.length - 5} more + + )} +
+
+
+ ))} +
+
+
+ )} + + {/* Empty State for no servers */} + {!isLoading && + bridgeStatus && + bridgeStatus.status !== 'never_connected' && + bridgeStatus.servers.length === 0 && ( +
+ + + + Connected MCP Servers + + + + + + + } + title="No MCP servers connected" + description="Your bridge is running but no MCP servers are configured. Add a server to make its tools available." + /> + +
+
+ )} + + {/* Help Section */} +
+

+ Bridge tools can be added to your collections and used in agents. When the bridge is + connected, the tools will be available for execution through your local MCP servers. +

+
+ + ); +} diff --git a/apps/web/src/app/docs/tutorials/bridge/page.tsx b/apps/web/src/app/docs/tutorials/bridge/page.tsx new file mode 100644 index 0000000..4bc858f --- /dev/null +++ b/apps/web/src/app/docs/tutorials/bridge/page.tsx @@ -0,0 +1,712 @@ +'use client'; + +import { Button } from '@tpmjs/ui/Button/Button'; +import { Icon } from '@tpmjs/ui/Icon/Icon'; +import Link from 'next/link'; +import { useState } from 'react'; +import { AppFooter } from '~/components/AppFooter'; +import { AppHeader } from '~/components/AppHeader'; + +interface Slide { + id: string; + title: string; + subtitle?: string; + content: React.ReactNode; + icon?: string; +} + +const slides: Slide[] = [ + { + id: 'intro', + title: 'Connect Local Tools to TPMJS', + subtitle: 'Use the Bridge to run MCP servers on your machine', + icon: 'πŸŒ‰', + content: ( +
+

+ The TPMJS Bridge lets you connect local MCP serversβ€”like Chrome DevTools, filesystem + access, or custom toolsβ€”to your TPMJS collections. +

+
+
+ πŸ–₯️ + Local MCP Servers +
+
+ πŸŒ‰ + Bridge CLI +
+
+ ☁️ + TPMJS Cloud +
+
+ πŸ€– + AI Access +
+
+
+

+ Use cases: Chrome automation β€’ Local file access β€’ Database queries β€’ Custom APIs +

+
+
+ ), + }, + { + id: 'why-bridge', + title: 'Why Use the Bridge?', + subtitle: 'Access tools that require local execution', + icon: 'πŸ€”', + content: ( +
+
+
+

Cloud-Only Tools

+
    +
  • + βœ“ + Code execution (sandboxed) +
  • +
  • + βœ“ + Web fetching +
  • +
  • + βœ“ + Web search +
  • +
  • + βœ— + Browser automation +
  • +
  • + βœ— + Local file access +
  • +
+
+
+

With Bridge

+
    +
  • + βœ“ + Chrome DevTools control +
  • +
  • + βœ“ + Read/write local files +
  • +
  • + βœ“ + Local database access +
  • +
  • + βœ“ + Custom internal APIs +
  • +
  • + βœ“ + Any stdio MCP server +
  • +
+
+
+
+

+ The Bridge runs on your machine and securely proxies tool calls from TPMJS. +

+
+
+ ), + }, + { + id: 'how-it-works', + title: 'How It Works', + subtitle: 'A simple proxy between local tools and TPMJS', + icon: 'βš™οΈ', + content: ( +
+
+
+
+              {`β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
+β”‚                     Your Machine                             β”‚
+β”‚                                                              β”‚
+β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
+β”‚  β”‚ Chrome MCP  │────▢│                                 β”‚    β”‚
+β”‚  β”‚   Server    β”‚     β”‚      @tpmjs/bridge CLI          β”‚    β”‚
+β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚                                 β”‚    β”‚
+β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚  β€’ Connects to MCP servers      β”‚    β”‚
+β”‚  β”‚ Filesystem  │────▢│  β€’ Registers tools with TPMJS   β”‚    β”‚
+β”‚  β”‚   Server    β”‚     β”‚  β€’ Polls for tool calls         β”‚    β”‚
+β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚  β€’ Returns results              β”‚    β”‚
+β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚                                 β”‚    β”‚
+β”‚  β”‚ Your Custom │────▢│                                 β”‚    β”‚
+β”‚  β”‚   Server    β”‚     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
+β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                    β”‚                        β”‚
+β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
+                                      β”‚ HTTPS
+                                      β–Ό
+                            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
+                            β”‚   TPMJS Cloud   β”‚
+                            β”‚                 β”‚
+                            β”‚  Your AI uses   β”‚
+                            β”‚  bridge tools   β”‚
+                            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜`}
+            
+
+
+
+

+ The bridge polls TPMJS for tool calls, executes them locally, and returns results. Tools + run on your machine with your permissions. +

+
+
+ ), + }, + { + id: 'step-1-install', + title: 'Step 1: Install the Bridge', + subtitle: 'Install the CLI globally', + icon: 'πŸ“¦', + content: ( +
+

+ Install the TPMJS Bridge CLI using npm, pnpm, or yarn. +

+
+
+
+ $ + Terminal +
+
+              {`# Using npm
+npm install -g @tpmjs/bridge
+
+# Using pnpm
+pnpm add -g @tpmjs/bridge
+
+# Using yarn
+yarn global add @tpmjs/bridge`}
+            
+
+
+

+ Verify installation: Run{' '} + + tpmjs-bridge --version + +

+
+
+
+ ), + }, + { + id: 'step-2-init', + title: 'Step 2: Initialize Configuration', + subtitle: 'Create your bridge config file', + icon: '⚑', + content: ( +
+

+ Initialize the bridge to create a config file at{' '} + ~/.tpmjs/bridge.json +

+
+
+
+ Terminal +
+
+              {`$ tpmjs-bridge init
+
+βœ“ Created config file: ~/.tpmjs/bridge.json
+
+Edit the config file to add your MCP servers, then run:
+  tpmjs-bridge login
+  tpmjs-bridge start`}
+            
+
+
+
+ ), + }, + { + id: 'step-3-add-server', + title: 'Step 3: Add MCP Servers', + subtitle: 'Configure the MCP servers you want to connect', + icon: 'βž•', + content: ( +
+

+ Add MCP servers to your bridge config. Each server runs locally via stdio. +

+
+
+
+ Add Chrome DevTools MCP +
+
+              {`$ tpmjs-bridge add chrome-devtools \\
+    --command "npx" \\
+    --args "-y @anthropic/claude-in-chrome"
+
+βœ“ Added server: chrome-devtools`}
+            
+
+
+
+ + Or edit ~/.tpmjs/bridge.json directly + +
+
+              {`{
+  "servers": [
+    {
+      "id": "chrome-devtools",
+      "name": "Chrome DevTools",
+      "transport": "stdio",
+      "command": "npx",
+      "args": ["-y", "@anthropic/claude-in-chrome"]
+    },
+    {
+      "id": "filesystem",
+      "name": "Filesystem Access",
+      "transport": "stdio",
+      "command": "npx",
+      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/allowed/dir"]
+    }
+  ]
+}`}
+            
+
+
+
+ ), + }, + { + id: 'step-4-login', + title: 'Step 4: Authenticate', + subtitle: 'Connect the bridge to your TPMJS account', + icon: 'πŸ”', + content: ( +
+

+ Log in to associate the bridge with your TPMJS account. +

+
+
+
+ Terminal +
+
+              {`$ tpmjs-bridge login
+
+Opening browser for authentication...
+βœ“ Logged in as yourname@example.com`}
+            
+
+
+

+ Alternative: Pass a token directly with{' '} + + --token YOUR_TOKEN + +

+
+
+
+ ), + }, + { + id: 'step-5-start', + title: 'Step 5: Start the Bridge', + subtitle: 'Run the bridge to connect your tools', + icon: 'πŸš€', + content: ( +
+

+ Start the bridge and it will connect to your MCP servers and register tools with TPMJS. +

+
+
+
+ Terminal +
+
+              {`$ tpmjs-bridge start
+
+[10:30:15] Starting TPMJS Bridge...
+
+[10:30:15] Connecting to MCP servers:
+[10:30:15]   Starting Chrome DevTools...
+[10:30:16]   βœ“ chrome-devtools connected
+[10:30:16]   βœ“ Chrome DevTools: 12 tools
+[10:30:16]     - screenshot
+[10:30:16]     - click_element
+[10:30:16]     - navigate
+[10:30:16]     - ...and 9 more
+
+[10:30:16] Connecting to TPMJS...
+[10:30:17] βœ“ Registered 12 tools with TPMJS
+[10:30:17]
+[10:30:17] Bridge running. Press Ctrl+C to stop.
+[10:30:17] Tools are now available in your TPMJS collections.`}
+            
+
+
+
+

+ Keep the bridge running while you want to use local tools. +

+
+
+ ), + }, + { + id: 'step-6-add-to-collection', + title: 'Step 6: Add Bridge Tools to Collections', + subtitle: 'Include bridge tools in your MCP collections', + icon: 'πŸ“¦', + content: ( +
+

+ Once connected, add bridge tools to your collections from the dashboard. +

+
+
+
+ 1 +
+
+

Go to Dashboard β†’ Bridge

+

+ Verify your bridge is connected and see available tools +

+
+
+
+
+ 2 +
+
+

Open a Collection

+

+ Go to the collection where you want bridge tools +

+
+
+
+
+ 3 +
+
+

Add Bridge Tools

+

+ Select tools from your connected MCP servers to add to the collection +

+
+
+
+
+ + + +
+
+ ), + }, + { + id: 'step-7-use', + title: 'Step 7: Use Your Tools!', + subtitle: 'Bridge tools work just like any other TPMJS tool', + icon: 'πŸŽ‰', + content: ( +
+

+ Now you can use bridge tools from Claude Desktop, Cursor, or any MCP client! +

+
+
+
+
+
+ 🦊 +
+

Claude Desktop

+
+
+
+
+
+

Take a screenshot of the current Chrome tab

+
+
+
+
+

+ Using tool: bridge--chrome-devtools--screenshot +

+

Here's the screenshot:

+
+ [Screenshot image] +
+
+
+
+
+
+
+ + βœ“ Chrome automation + + + βœ“ Local files + + + βœ“ Custom tools + +
+
+ ), + }, + { + id: 'commands', + title: 'CLI Reference', + subtitle: 'All available bridge commands', + icon: 'πŸ“–', + content: ( +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CommandDescription
+ tpmjs-bridge init + Create config file
+ tpmjs-bridge login + Authenticate with TPMJS
+ tpmjs-bridge logout + Remove credentials
+ tpmjs-bridge add <name> + Add an MCP server
+ tpmjs-bridge remove <name> + Remove an MCP server
+ tpmjs-bridge list + List configured servers
+ tpmjs-bridge start + Start the bridge
+ tpmjs-bridge status + Show connection status
+ tpmjs-bridge config + Show config file path
+
+
+
+ ), + }, + { + id: 'complete', + title: 'Bridge Connected!', + subtitle: 'Your local tools are now accessible via TPMJS', + icon: 'πŸŒ‰', + content: ( +
+

+ Your bridge is set up! Local MCP servers can now be used through TPMJS. +

+
+ + + + + + +
+
+

Tips

+
+
+

Keep Bridge Running

+

+ Bridge tools only work while the CLI is running +

+
+
+

Run on Startup

+

+ Add to your shell profile or use a process manager +

+
+
+
+
+ ), + }, +]; + +export default function BridgeTutorialPage(): React.ReactElement { + const [currentSlide, setCurrentSlide] = useState(0); + + const goToSlide = (index: number) => { + setCurrentSlide(Math.max(0, Math.min(slides.length - 1, index))); + }; + + const nextSlide = () => goToSlide(currentSlide + 1); + const prevSlide = () => goToSlide(currentSlide - 1); + + const slide = slides[currentSlide]; + const progress = ((currentSlide + 1) / slides.length) * 100; + + if (!slide) { + return <>; + } + + return ( +
+ + + {/* Progress bar */} +
+
+
+ +
+ {/* Navigation header */} +
+
+ + + Back to Tutorials + +
+ {currentSlide + 1} / {slides.length} +
+
+
+ + {/* Slide content */} +
+
+ {/* Slide header */} +
+ {slide.icon && {slide.icon}} +

{slide.title}

+ {slide.subtitle && ( +

{slide.subtitle}

+ )} +
+ + {/* Slide content */} +
{slide.content}
+
+
+ + {/* Navigation footer */} +
+
+ {/* Slide indicators */} +
+ {slides.map((s, index) => ( +
+ + {/* Navigation buttons */} +
+ + + {currentSlide === slides.length - 1 ? ( + + + + ) : ( + + )} +
+
+
+
+ + +
+ ); +} diff --git a/apps/web/src/app/docs/tutorials/page.tsx b/apps/web/src/app/docs/tutorials/page.tsx index a1e2565..6506bf3 100644 --- a/apps/web/src/app/docs/tutorials/page.tsx +++ b/apps/web/src/app/docs/tutorials/page.tsx @@ -31,6 +31,15 @@ const tutorials: Tutorial[] = [ duration: '4 min', steps: 8, }, + { + title: 'Connect Local Tools with Bridge', + description: + 'Use the TPMJS Bridge to connect local MCP servers like Chrome DevTools, filesystem access, or custom tools to your TPMJS collections.', + icon: 'πŸŒ‰', + href: '/docs/tutorials/bridge', + duration: '6 min', + steps: 11, + }, { title: 'Deploy Your Own Executor', description: diff --git a/apps/web/src/components/dashboard/DashboardLayout.tsx b/apps/web/src/components/dashboard/DashboardLayout.tsx index 631a1f3..147ea44 100644 --- a/apps/web/src/components/dashboard/DashboardLayout.tsx +++ b/apps/web/src/components/dashboard/DashboardLayout.tsx @@ -20,6 +20,7 @@ const navItems: NavItem[] = [ { href: '/dashboard/agents', label: 'Agents', icon: 'terminal' }, { href: '/dashboard/collections', label: 'Collections', icon: 'folder' }, { href: '/dashboard/settings/api-keys', label: 'API Keys', icon: 'key' }, + { href: '/dashboard/settings/bridge', label: 'Bridge', icon: 'link' }, ]; const likesNavItems: NavItem[] = [ diff --git a/apps/web/src/lib/mcp/handlers.ts b/apps/web/src/lib/mcp/handlers.ts index 071b03f..382634d 100644 --- a/apps/web/src/lib/mcp/handlers.ts +++ b/apps/web/src/lib/mcp/handlers.ts @@ -1,7 +1,12 @@ import { prisma } from '@tpmjs/db'; - +import { queueBridgeToolCall, waitForBridgeResult } from '~/app/api/bridge/route'; import { executeWithExecutor, parseExecutorConfig } from '../executors'; -import { convertToMcpTool, parseToolName } from './tool-converter'; +import { + type BridgeTool, + convertBridgeToolToMcp, + convertToMcpTool, + parseToolName, +} from './tool-converter'; const DB_TIMEOUT_MS = 10000; // 10 second timeout for database queries @@ -58,18 +63,49 @@ export async function handleToolsList( include: { tool: { include: { package: true } } }, orderBy: { position: 'asc' }, }, + bridgeTools: true, + user: { + include: { + bridgeConnection: true, + }, + }, }, }), DB_TIMEOUT_MS, 'Database query timed out' ); - const tools = collection?.tools.map((ct) => convertToMcpTool(ct.tool)) ?? []; + // Convert registry tools to MCP format + const registryTools = collection?.tools.map((ct) => convertToMcpTool(ct.tool)) ?? []; + + // Convert bridge tools to MCP format + const bridgeTools: ReturnType[] = []; + + if (collection?.bridgeTools.length && collection.user.bridgeConnection) { + const bridgeConnection = collection.user.bridgeConnection; + const availableBridgeTools = (bridgeConnection.tools as unknown as BridgeTool[]) || []; + + // Only include bridge tools if bridge is connected + if (bridgeConnection.status === 'connected') { + for (const collectionBridgeTool of collection.bridgeTools) { + // Find the tool definition from the bridge connection + const bridgeTool = availableBridgeTools.find( + (bt) => + bt.serverId === collectionBridgeTool.serverId && + bt.name === collectionBridgeTool.toolName + ); + + if (bridgeTool) { + bridgeTools.push(convertBridgeToolToMcp(bridgeTool, collectionBridgeTool.displayName)); + } + } + } + } return { jsonrpc: '2.0', id: requestId, - result: { tools }, + result: { tools: [...registryTools, ...bridgeTools] }, }; } catch (error) { console.error('[MCP tools/list] Error:', error); @@ -104,6 +140,18 @@ export async function handleToolsCall( }; } + // Handle bridge tool calls + if (parsed.type === 'bridge') { + return handleBridgeToolCall( + collectionId, + parsed.serverId, + parsed.toolName, + params.arguments ?? {}, + requestId + ); + } + + // Handle registry tool calls // Verify tool exists in collection and get executor config const collection = await withTimeout( prisma.collection.findUnique({ @@ -181,3 +229,148 @@ export async function handleToolsCall( }; } } + +/** + * Handle a bridge tool call by routing it through the user's bridge connection + */ +async function handleBridgeToolCall( + collectionId: string, + serverId: string, + toolName: string, + args: Record, + requestId: JsonRpcId +): Promise { + try { + // Get the collection's owner and their bridge connection + const collection = await withTimeout( + prisma.collection.findUnique({ + where: { id: collectionId }, + include: { + bridgeTools: { + where: { serverId, toolName }, + }, + user: { + include: { bridgeConnection: true }, + }, + }, + }), + DB_TIMEOUT_MS, + 'Database query timed out' + ); + + if (!collection) { + return { + jsonrpc: '2.0', + id: requestId, + error: { code: -32602, message: 'Collection not found' }, + }; + } + + // Verify the bridge tool exists in the collection + const bridgeTool = collection.bridgeTools[0]; + if (!bridgeTool) { + return { + jsonrpc: '2.0', + id: requestId, + error: { + code: -32602, + message: `Bridge tool not found in collection: ${serverId}/${toolName}`, + }, + }; + } + + // Verify the user has an active bridge connection + const bridgeConnection = collection.user.bridgeConnection; + if (!bridgeConnection || bridgeConnection.status !== 'connected') { + return { + jsonrpc: '2.0', + id: requestId, + result: { + content: [ + { + type: 'text', + text: 'Error: Bridge is not connected. Start the bridge CLI to use bridge tools.', + }, + ], + isError: true, + }, + }; + } + + // Check if the bridge is stale (not seen in 2 minutes) + if (bridgeConnection.lastSeen) { + const staleThreshold = 2 * 60 * 1000; // 2 minutes + if (Date.now() - bridgeConnection.lastSeen.getTime() > staleThreshold) { + return { + jsonrpc: '2.0', + id: requestId, + result: { + content: [ + { + type: 'text', + text: 'Error: Bridge connection appears stale. Please check if the bridge CLI is running.', + }, + ], + isError: true, + }, + }; + } + } + + // Generate a unique call ID + const callId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; + + // Queue the tool call for the bridge to pick up + queueBridgeToolCall(collection.user.id, callId, serverId, toolName, args); + + // Wait for the result (with 5 minute timeout) + const result = await waitForBridgeResult(callId, 300000); + + if (result.error) { + return { + jsonrpc: '2.0', + id: requestId, + result: { + content: [{ type: 'text', text: `Error: ${result.error.message}` }], + isError: true, + }, + }; + } + + // Format the result + const content = result.result as { content?: unknown[]; isError?: boolean } | undefined; + if (content?.content) { + return { + jsonrpc: '2.0', + id: requestId, + result: { + content: content.content, + isError: content.isError, + }, + }; + } + + return { + jsonrpc: '2.0', + id: requestId, + result: { + content: [ + { + type: 'text', + text: + typeof result.result === 'string' + ? result.result + : JSON.stringify(result.result, null, 2), + }, + ], + }, + }; + } catch (error) { + console.error('[MCP bridge tool call] Error:', error); + return { + jsonrpc: '2.0', + id: requestId, + error: { code: -32603, message: error instanceof Error ? error.message : 'Internal error' }, + }; + } +} diff --git a/apps/web/src/lib/mcp/index.ts b/apps/web/src/lib/mcp/index.ts index 476b3fd..c109309 100644 --- a/apps/web/src/lib/mcp/index.ts +++ b/apps/web/src/lib/mcp/index.ts @@ -1,4 +1,9 @@ -export { convertToMcpTool, parseToolName, sanitizeMcpName } from './tool-converter'; -export type { McpToolDefinition } from './tool-converter'; - export { handleInitialize, handleToolsCall, handleToolsList } from './handlers'; +export type { BridgeTool, McpToolDefinition, ParsedToolName } from './tool-converter'; +export { + convertBridgeToolToMcp, + convertToMcpTool, + parseToolName, + sanitizeBridgeToolName, + sanitizeMcpName, +} from './tool-converter'; diff --git a/apps/web/src/lib/mcp/tool-converter.ts b/apps/web/src/lib/mcp/tool-converter.ts index d3a5371..ea91639 100644 --- a/apps/web/src/lib/mcp/tool-converter.ts +++ b/apps/web/src/lib/mcp/tool-converter.ts @@ -6,6 +6,17 @@ export interface McpToolDefinition { inputSchema: Record; } +/** + * Bridge tool definition from the BridgeConnection.tools JSON + */ +export interface BridgeTool { + serverId: string; + serverName: string; + name: string; + description?: string; + inputSchema?: Record; +} + /** * Sanitize package name and tool name into a valid MCP tool name. * MCP tool names must match ^[a-zA-Z0-9_-]+ @@ -17,6 +28,17 @@ export function sanitizeMcpName(packageName: string, toolName: string): string { return `${sanitizedPkg}--${toolName}`; } +/** + * Create MCP name for a bridge tool + * Example: chrome-devtools + screenshot β†’ bridge--chrome-devtools--screenshot + */ +export function sanitizeBridgeToolName(serverId: string, toolName: string): string { + // Sanitize serverId and toolName to only allow valid MCP characters + const sanitizedServer = serverId.replace(/[^a-zA-Z0-9_-]/g, '-'); + const sanitizedTool = toolName.replace(/[^a-zA-Z0-9_-]/g, '-'); + return `bridge--${sanitizedServer}--${sanitizedTool}`; +} + /** * Convert a TPMJS Tool to an MCP tool definition. */ @@ -32,12 +54,31 @@ export function convertToMcpTool(tool: Tool & { package: Package }): McpToolDefi } /** - * Parse an MCP tool name back into package name and tool name. - * Returns null if the name doesn't match the expected format. - * - * Example: tpmjs-hello--helloWorldTool β†’ { packageName: "@tpmjs/hello", toolName: "helloWorldTool" } + * Parsed tool name result - either a registry tool or a bridge tool */ -export function parseToolName(mcpName: string): { packageName: string; toolName: string } | null { +export type ParsedToolName = + | { type: 'registry'; packageName: string; toolName: string } + | { type: 'bridge'; serverId: string; toolName: string }; + +/** + * Parse an MCP tool name back into its components. + * Handles both registry tools and bridge tools. + * + * Registry: tpmjs-hello--helloWorldTool β†’ { type: 'registry', packageName: "@tpmjs/hello", toolName: "helloWorldTool" } + * Bridge: bridge--chrome-devtools--screenshot β†’ { type: 'bridge', serverId: "chrome-devtools", toolName: "screenshot" } + */ +export function parseToolName(mcpName: string): ParsedToolName | null { + // Check if it's a bridge tool + const bridgeMatch = mcpName.match(/^bridge--([^-]+(?:-[^-]+)*)--(.+)$/); + if (bridgeMatch && bridgeMatch[1] && bridgeMatch[2]) { + return { + type: 'bridge', + serverId: bridgeMatch[1], + toolName: bridgeMatch[2], + }; + } + + // Otherwise parse as registry tool const match = mcpName.match(/^(.+)--(.+)$/); if (!match || !match[1] || !match[2]) return null; @@ -48,5 +89,24 @@ export function parseToolName(mcpName: string): { packageName: string; toolName: // tpmjs-hello β†’ @tpmjs/hello (first dash becomes @scope/) const packageName = pkg.includes('-') ? `@${pkg.replace('-', '/')}` : pkg; - return { packageName, toolName }; + return { type: 'registry', packageName, toolName }; +} + +/** + * Convert a bridge tool definition to an MCP tool definition. + */ +export function convertBridgeToolToMcp( + tool: BridgeTool, + displayName?: string | null +): McpToolDefinition { + return { + name: sanitizeBridgeToolName(tool.serverId, tool.name), + description: displayName + ? `[${tool.serverName}] ${displayName}` + : tool.description || `${tool.name} from ${tool.serverName}`, + inputSchema: tool.inputSchema ?? { + type: 'object', + properties: {}, + }, + }; } diff --git a/docs/MCP-AGGREGATOR-DESIGN.md b/docs/MCP-AGGREGATOR-DESIGN.md new file mode 100644 index 0000000..5010014 --- /dev/null +++ b/docs/MCP-AGGREGATOR-DESIGN.md @@ -0,0 +1,909 @@ +# 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 + +1. [The Vision](#the-vision) +2. [Current State](#current-state) +3. [The Challenge](#the-challenge) +4. [Architecture Options](#architecture-options) +5. [Recommended Implementation](#recommended-implementation) +6. [Technical Specifications](#technical-specifications) +7. [User Experience](#user-experience) +8. [Implementation Phases](#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 handshake +- `tools/list` - List all tools in collection +- `tools/call` - Execute a tool + +**Tool Source**: Currently only npm packages synced from the TPMJS registry. + +### What We Need to Add + +1. **MCP Client Capability**: Connect TO other MCP servers +2. **Tool Import**: Pull tool definitions from external MCP servers +3. **Proxy Execution**: Route tool calls to original MCP server +4. **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**: +1. User runs: `npx tpmjs-bridge --servers chrome-devtools,filesystem` +2. Bridge connects to TPMJS via WebSocket +3. Bridge discovers tools from local MCP servers +4. TPMJS receives tool definitions +5. 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. + +```typescript +// 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; +} + +export class MCPClientManager { + private clients: Map = new Map(); + + async connect(config: MCPServerConfig): Promise { + 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 }); + } + + 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. + +```typescript +// 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. + +```typescript +// 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 + +```prisma +// 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 + +```typescript +// apps/web/src/lib/mcp/handlers.ts + +export async function handleToolsList( + collection: CollectionWithTools, + userId: string +): Promise { + 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 { + // 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 { + 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: + +1. **npm** - Fastest, always available +2. **Remote HTTP/SSE** - Fast, usually available +3. **Bridge** - Requires user connection, variable latency + +### Error Handling + +```typescript +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 + +1. **API Key Authentication**: Bridge connections require valid API key +2. **User Isolation**: Each user's bridge is isolated +3. **Tool Whitelisting**: Users explicitly add tools to collections +4. **Encrypted Credentials**: Remote MCP server credentials encrypted at rest +5. **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: + +```json +// ~/.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**: +1. `@tpmjs/mcp-client` package for connecting to MCP servers +2. UI for adding remote MCP server to collection +3. Updated MCP handlers to aggregate remote tools +4. 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**: +1. `@tpmjs/bridge` CLI package +2. WebSocket API for bridge connections (`/api/bridge`) +3. Database schema for bridge connections +4. Bridge status UI in dashboard + +### Phase 3: Tool Discovery & Sync (2 weeks) + +**Goal**: Automatic tool discovery and sync + +**Deliverables**: +1. Auto-discover tools when bridge connects +2. Sync tool definitions periodically +3. Handle schema changes gracefully +4. Tool health monitoring + +### Phase 4: Advanced Features (Ongoing) + +**Goal**: Enhanced reliability and UX + +**Deliverables**: +1. Bridge auto-reconnection +2. Tool execution queuing +3. Offline tool caching +4. Multiple bridge support (different machines) +5. 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 + +--- + +## References + +- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) +- [MCP Protocol Docs](https://modelcontextprotocol.io/docs) +- [Chrome DevTools MCP](https://github.com/anthropics/chrome-devtools-mcp) +- [Browser MCP](https://browsermcp.io/) +- [Claude in Chrome Docs](https://code.claude.com/docs/en/chrome) +- [Vercel AI SDK MCP](https://ai-sdk.dev/docs/ai-sdk-core/mcp-tools) diff --git a/docs/MCP-BRIDGE-STATUS.md b/docs/MCP-BRIDGE-STATUS.md new file mode 100644 index 0000000..41b321b --- /dev/null +++ b/docs/MCP-BRIDGE-STATUS.md @@ -0,0 +1,319 @@ +# MCP Bridge Implementation Status + +**Last Updated:** 2026-01-12 + +## Overview + +The MCP Bridge system allows users to connect local MCP servers (like Chrome DevTools, file systems, or custom tools) to their TPMJS collections. Tools running on the user's machine can be accessed remotely through TPMJS. + +## Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ User's Machine β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ MCP Server │────▢│ @tpmjs/bridge CLI β”‚ β”‚ +β”‚ β”‚ (stdio) β”‚ β”‚ - Connects to local MCP servers β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ - Polls TPMJS for tool calls β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ - Executes tools locally β”‚ β”‚ +β”‚ β”‚ MCP Server │────▢│ - Returns results to TPMJS β”‚ β”‚ +β”‚ β”‚ (stdio) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ HTTP Polling + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ TPMJS Cloud β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ /api/bridge β”‚ β”‚ MCP Handlers β”‚ β”‚ Database β”‚ β”‚ +β”‚ β”‚ - Registration │◀──▢│ - tools/list │◀──▢│ - Bridge β”‚ β”‚ +β”‚ β”‚ - Tool calls β”‚ β”‚ - tools/call β”‚ β”‚ Connectionβ”‚ β”‚ +β”‚ β”‚ - Results β”‚ β”‚ β”‚ β”‚ - Bridge β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ Tools β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Packages Created + +### 1. `@tpmjs/mcp-client` (packages/mcp-client) + +MCP client library for connecting to MCP servers. + +**Features:** +- Connect to MCP servers via stdio transport +- Discover tools from servers +- Execute tool calls +- Manage multiple server connections + +**Usage:** +```typescript +import { MCPClientManager } from '@tpmjs/mcp-client'; + +const manager = new MCPClientManager(); +const tools = await manager.connect({ + id: 'my-server', + name: 'My MCP Server', + transport: 'stdio', + command: 'node', + args: ['./server.js'] +}); + +const result = await manager.callTool('my-server', 'toolName', { arg: 'value' }); +await manager.disconnectAll(); +``` + +### 2. `@tpmjs/bridge` (packages/bridge) + +CLI for users to run on their local machine. + +**Commands:** +```bash +tpmjs-bridge init # Create config file +tpmjs-bridge login # Authenticate with TPMJS +tpmjs-bridge logout # Remove credentials +tpmjs-bridge add # Add an MCP server +tpmjs-bridge remove # Remove an MCP server +tpmjs-bridge list # List configured servers +tpmjs-bridge config # Show config path +tpmjs-bridge start # Start the bridge +tpmjs-bridge status # Show bridge status +``` + +**Config file:** `~/.tpmjs/bridge.json` +```json +{ + "servers": [ + { + "id": "chrome-devtools", + "name": "Chrome DevTools", + "transport": "stdio", + "command": "npx", + "args": ["-y", "@anthropic/claude-in-chrome"] + } + ] +} +``` + +### 3. `@tpmjs/test-file-writer` (packages/tools/test-file-writer) + +Test MCP server for development and testing. + +**Tools:** +- `write_file` - Write content to a file +- `read_file` - Read content from a file +- `list_files` - List all files +- `delete_file` - Delete a file +- `get_info` - Get server info + +**Files stored in:** `~/.tpmjs/test-files/` + +## Database Schema + +### BridgeConnection + +Tracks active bridge connections per user. + +```prisma +model BridgeConnection { + id String @id @default(cuid()) + userId String @unique @map("user_id") + user User @relation(...) + status String @default("disconnected") // 'connected' | 'disconnected' + socketId String? // Internal routing identifier + tools Json @default("[]") // Cached tool definitions + lastSeen DateTime? + clientVersion String? + clientOS String? +} +``` + +### CollectionBridgeTool + +Links bridge tools to collections. + +```prisma +model CollectionBridgeTool { + id String @id @default(cuid()) + collectionId String + collection Collection @relation(...) + serverId String // e.g., "chrome-devtools" + toolName String // e.g., "screenshot" + displayName String? // Custom display name + note String? // User notes +} +``` + +## API Endpoints + +### Bridge Communication API + +**`POST /api/bridge`** - Bridge registration and tool results +```typescript +// Register bridge +{ type: 'register', tools: [...], clientVersion: '0.1.0', clientOS: 'darwin' } + +// Submit tool result +{ type: 'result', callId: 'xxx', result: {...} } + +// Submit tool error +{ type: 'result', callId: 'xxx', error: { message: '...' } } + +// Heartbeat +{ type: 'heartbeat' } +``` + +**`GET /api/bridge`** - Poll for pending tool calls +```typescript +// Response +{ calls: [{ callId: 'xxx', serverId: 'chrome', toolName: 'screenshot', args: {} }] } +``` + +**`DELETE /api/bridge`** - Disconnect bridge + +### User Bridge Status API + +**`GET /api/user/bridge`** - Get bridge status for current user +```typescript +{ + status: 'connected' | 'disconnected' | 'stale' | 'never_connected', + lastSeen: '2026-01-12T...', + clientVersion: '0.1.0', + clientOS: 'darwin', + toolCount: 5, + servers: [{ id: 'chrome', name: 'Chrome', toolCount: 3, tools: [...] }] +} +``` + +### Collection Bridge Tools API + +**`GET /api/collections/[id]/bridge-tools`** - List bridge tools in collection +**`POST /api/collections/[id]/bridge-tools`** - Add bridge tool to collection +**`PATCH /api/collections/[id]/bridge-tools/[id]`** - Update bridge tool +**`DELETE /api/collections/[id]/bridge-tools/[id]`** - Remove bridge tool + +## MCP Handler Integration + +Bridge tools are included in MCP `tools/list` responses when: +1. The collection has bridge tools added +2. The collection owner has an active bridge connection +3. The bridge status is "connected" + +Bridge tool names use the format: `bridge--{serverId}--{toolName}` + +Example: `bridge--chrome-devtools--screenshot` + +## UI + +### Bridge Settings Page + +Location: `/dashboard/settings/bridge` + +Features: +- Shows connection status (connected/disconnected/stale/never_connected) +- Lists connected MCP servers and their tools +- Shows last seen time, client version, platform +- Quick start instructions for new users + +### Navigation + +Bridge link added to dashboard sidebar under "Bridge" with link icon. + +## Testing Results + +All components tested successfully: + +| Component | Status | Notes | +|-----------|--------|-------| +| MCPClientManager | βœ… Pass | Connects, discovers tools, executes calls | +| Test File Writer | βœ… Pass | All 5 tools work correctly | +| Bridge CLI | βœ… Pass | All commands work | +| Bridge Class | βœ… Pass | Connects to servers, registers, polls | +| API Endpoints | βœ… Pass | Auth validation works | +| Type Check | βœ… Pass | No type errors | +| Lint | βœ… Pass | Only pre-existing warnings | + +## What's Working + +1. **Local MCP Server Connection** - Bridge connects to local MCP servers via stdio +2. **Tool Discovery** - Automatically discovers tools from connected servers +3. **Tool Execution** - Can execute tools and return results +4. **HTTP Polling** - Vercel-compatible polling instead of WebSocket +5. **Bridge CLI** - Full CLI with init, login, add, remove, start commands +6. **API Endpoints** - All endpoints implemented with proper auth +7. **Database Schema** - Bridge connections and collection tools stored +8. **MCP Integration** - Bridge tools included in MCP tools/list +9. **UI** - Bridge status page with connection info + +## What Needs Manual Testing + +1. **End-to-End Flow** - Requires logging in via web UI and getting a session token +2. **Real MCP Servers** - Test with Chrome DevTools, filesystem, etc. +3. **Tool Execution via MCP** - Call bridge tools through the MCP protocol +4. **Collection Integration** - Add bridge tools to collections via UI + +## How to Test Locally + +```bash +# 1. Build packages +pnpm --filter=@tpmjs/mcp-client build +pnpm --filter=@tpmjs/bridge build +pnpm --filter=@tpmjs/test-file-writer build + +# 2. Initialize bridge config +node packages/bridge/dist/cli.js init + +# 3. Add test server +node packages/bridge/dist/cli.js add test-file-writer \ + --command "node" \ + --args "$(pwd)/packages/tools/test-file-writer/dist/server.js" + +# 4. Start dev server +pnpm --filter=@tpmjs/web dev + +# 5. Log in via browser, get session token + +# 6. Start bridge (with real token) +node packages/bridge/dist/cli.js start --token + +# 7. Visit /dashboard/settings/bridge to see status +``` + +## Future Improvements + +- [ ] Proper API key authentication (not session tokens) +- [ ] Redis for tool call queuing (production) +- [ ] WebSocket support for lower latency +- [ ] Bridge tool UI in collection editor +- [ ] Tool call logging and debugging +- [ ] Rate limiting for bridge connections +- [ ] Multiple bridge instances per user +- [ ] Bridge health monitoring alerts + +## Files Changed/Created + +### New Packages +- `packages/mcp-client/` - MCP client library +- `packages/bridge/` - Bridge CLI +- `packages/tools/test-file-writer/` - Test MCP server + +### Database +- `packages/db/prisma/schema.prisma` - Added BridgeConnection, CollectionBridgeTool + +### API Routes +- `apps/web/src/app/api/bridge/route.ts` - Bridge API +- `apps/web/src/app/api/user/bridge/route.ts` - User bridge status +- `apps/web/src/app/api/collections/[id]/bridge-tools/route.ts` - Collection bridge tools +- `apps/web/src/app/api/collections/[id]/bridge-tools/[bridgeToolId]/route.ts` - Single bridge tool + +### MCP Integration +- `apps/web/src/lib/mcp/handlers.ts` - Updated to include bridge tools +- `apps/web/src/lib/mcp/tool-converter.ts` - Added bridge tool conversion +- `apps/web/src/lib/mcp/index.ts` - Updated exports + +### UI +- `apps/web/src/app/dashboard/settings/bridge/page.tsx` - Bridge status page +- `apps/web/src/components/dashboard/DashboardLayout.tsx` - Added Bridge nav link + +### Types +- `packages/types/src/collection.ts` - Added bridge tool schemas diff --git a/docs/PRD-MCP-BRIDGE.md b/docs/PRD-MCP-BRIDGE.md new file mode 100644 index 0000000..8db70fe --- /dev/null +++ b/docs/PRD-MCP-BRIDGE.md @@ -0,0 +1,1298 @@ +# PRD: TPMJS MCP Bridge + +**Product Requirements Document** + +| Field | Value | +|-------|-------| +| Author | Ajax Davis | +| Status | Draft | +| Created | 2025-01-12 | +| Last Updated | 2025-01-12 | + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Problem Statement](#problem-statement) +3. [Solution](#solution) +4. [User Stories](#user-stories) +5. [Architecture](#architecture) +6. [User Experience](#user-experience) +7. [Technical Specification](#technical-specification) +8. [Database Schema](#database-schema) +9. [API Specification](#api-specification) +10. [Security Considerations](#security-considerations) +11. [Implementation Phases](#implementation-phases) +12. [Success Metrics](#success-metrics) +13. [Open Questions](#open-questions) + +--- + +## Overview + +### What + +The TPMJS MCP Bridge enables users to connect local MCP (Model Context Protocol) servers to their TPMJS collections. This allows a single TPMJS MCP endpoint to aggregate tools from: +- npm packages (existing) +- Remote MCP servers (HTTP/SSE) +- Local MCP servers (via bridge) + +### Why + +Users currently need to configure multiple MCP servers in their AI tools (Claude Desktop, Cursor, etc.). Each local MCP server (Chrome DevTools, Blender, filesystem, etc.) requires separate configuration. TPMJS can become the single point of control for all tools. + +### Goal + +**One MCP endpoint to rule them all.** Users add one TPMJS MCP server to Claude Desktop and manage all their tools through the TPMJS UI. + +--- + +## Problem Statement + +### Current Pain Points + +1. **Multiple MCP Configurations**: Users must manually configure each MCP server in Claude Desktop's config file +2. **No Central Management**: No UI to manage which tools are available +3. **Local Tools Inaccessible from Cloud**: TPMJS runs in the cloud and cannot access local MCP servers that use stdio transport +4. **Tool Discovery Fragmented**: Users must find and configure each MCP server separately + +### Example: Current State + +```json +// ~/.config/claude/claude_desktop_config.json +{ + "mcpServers": { + "tpmjs": { + "type": "url", + "url": "https://tpmjs.com/api/mcp/ajax/my-tools/http" + }, + "chrome-devtools": { + "command": "npx", + "args": ["-y", "chrome-devtools-mcp"] + }, + "blender": { + "command": "uvx", + "args": ["blender-mcp"] + }, + "filesystem": { + "command": "npx", + "args": ["-y", "@anthropic/mcp-filesystem", "/home/user"] + }, + "slack": { + "command": "npx", + "args": ["-y", "@anthropic/mcp-slack"] + } + } +} +``` + +**Problems:** +- 5 separate server configurations +- Must edit JSON file manually +- No visibility into available tools +- Cannot share configurations easily + +--- + +## Solution + +### The Bridge + +A CLI tool (`@tpmjs/bridge`) that runs on the user's machine and: +1. Connects to local MCP servers (stdio) +2. Discovers their tools +3. Registers tools with TPMJS via WebSocket +4. Proxies tool calls from TPMJS to local MCP servers + +### Target State + +```json +// ~/.config/claude/claude_desktop_config.json +{ + "mcpServers": { + "tpmjs": { + "type": "url", + "url": "https://tpmjs.com/api/mcp/ajax/unified/http" + } + } +} +``` + +**One endpoint** providing access to: +- npm tools +- Chrome DevTools (via bridge) +- Blender (via bridge) +- Filesystem (via bridge) +- Slack (via bridge) +- Any other MCP server + +--- + +## User Stories + +### US-1: Add Local MCP Server to Bridge + +**As a** TPMJS user +**I want to** add a local MCP server (like Blender) to my bridge +**So that** its tools appear in my TPMJS collection + +**Acceptance Criteria:** +- [ ] User can edit bridge config file to add new server +- [ ] Bridge discovers tools when server is added +- [ ] Tools appear in TPMJS dashboard + +### US-2: Select Tools for Collection + +**As a** TPMJS user +**I want to** choose which bridge tools to include in my collection +**So that** I only expose the tools I need + +**Acceptance Criteria:** +- [ ] User sees all available bridge tools in UI +- [ ] User can add/remove tools from collection +- [ ] MCP endpoint only includes selected tools + +### US-3: Use Bridge Tools from Claude + +**As a** Claude Desktop user +**I want to** use bridge tools (like Chrome screenshot) through TPMJS +**So that** I don't need multiple MCP server configurations + +**Acceptance Criteria:** +- [ ] Tool calls route through TPMJS to bridge +- [ ] Results return to Claude Desktop +- [ ] Latency is acceptable (<2s for most operations) + +### US-4: Handle Bridge Disconnection + +**As a** TPMJS user +**I want to** see clear errors when my bridge is offline +**So that** I understand why certain tools aren't working + +**Acceptance Criteria:** +- [ ] UI shows bridge connection status +- [ ] Tool calls return helpful error when bridge is offline +- [ ] Bridge auto-reconnects when possible + +### US-5: Quick CLI Setup + +**As a** developer +**I want to** set up the bridge quickly via CLI +**So that** I can start using local tools immediately + +**Acceptance Criteria:** +- [ ] `npx @tpmjs/bridge init` creates config file +- [ ] `npx @tpmjs/bridge add ` adds common servers +- [ ] `npx @tpmjs/bridge start` connects everything + +--- + +## Architecture + +### System Overview + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ TPMJS Platform β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ MCP Aggregator β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ Tool Sources: β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ npm Registry β”‚ β”‚ Remote MCP β”‚ β”‚ User's Bridge β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ (always on) β”‚ β”‚ (HTTP/SSE) β”‚ β”‚ (when connected) β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β–Ό β–Ό β–Ό β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ Unified Tool Registry β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ npm tools: always available β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ Remote MCP tools: always available β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ Bridge tools: available when bridge connected β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β–Ό β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ MCP Server: /api/mcp/{user}/{collection}/http β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β”‚ WebSocket β”‚ +β”‚ β–Ό β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ + β–Ό β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Claude Desktop β”‚ β”‚ User's Machine β”‚ +β”‚ (MCP Client) β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ Only needs ONE server: β”‚ β”‚ β”‚ @tpmjs/bridge β”‚ β”‚ +β”‚ tpmjs.com/api/mcp/... β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ MCP Clients: β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”œβ”€β”€ chrome β”‚ β”‚ + β”‚ β”‚ β”œβ”€β”€ blender β”‚ β”‚ + β”‚ β”‚ β”œβ”€β”€ filesystem β”‚ β”‚ + β”‚ β”‚ └── slack β”‚ β”‚ + β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + β”‚ β”‚ β”‚ + β”‚ β–Ό β”‚ + β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ + β”‚ β”‚ Local MCP Servers β”‚ β”‚ + β”‚ β”‚ (stdio processes) β”‚ β”‚ + β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Bridge Detail + +``` +User's Machine +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ @tpmjs/bridge β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ MCP Client Manager β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ Maintains connections to local MCP servers: β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ chrome β”‚ β”‚ blender β”‚ β”‚ filesystem β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ devtools β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ stdio ↕ β”‚ β”‚ stdio ↕ β”‚ β”‚ stdio ↕ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β–Ό β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ Tool Registry β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ (aggregated) β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ WebSocket Connection β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ to TPMJS Cloud β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ Registers available tools β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ Receives tool call requests β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ Sends tool results β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ Auto-reconnects on disconnect β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Tool Call Flow + +``` +Step-by-step: User asks Claude to take a screenshot of GitHub + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Claude Desktop β”‚ β”‚ TPMJS Cloud β”‚ β”‚ User's Bridge β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ β”‚ + β”‚ tools/call β”‚ β”‚ + β”‚ "chrome--screenshot" β”‚ β”‚ + β”‚ ─────────────────────▢│ β”‚ + β”‚ β”‚ β”‚ + β”‚ β”‚ Parse tool name β”‚ + β”‚ β”‚ "chrome--screenshot" β”‚ + β”‚ β”‚ β†’ server: chrome β”‚ + β”‚ β”‚ β†’ tool: screenshot β”‚ + β”‚ β”‚ β”‚ + β”‚ β”‚ Lookup server β”‚ + β”‚ β”‚ chrome = bridge type β”‚ + β”‚ β”‚ β”‚ + β”‚ β”‚ Check bridge status β”‚ + β”‚ β”‚ β†’ connected βœ“ β”‚ + β”‚ β”‚ β”‚ + β”‚ β”‚ tool_call β”‚ + β”‚ β”‚ ─────────────────────▢│ + β”‚ β”‚ { β”‚ + β”‚ β”‚ serverId: "chrome",β”‚ + β”‚ β”‚ toolName: "screenshot", + β”‚ β”‚ args: {} β”‚ + β”‚ β”‚ } β”‚ + β”‚ β”‚ β”‚ + β”‚ β”‚ β”‚ Route to chrome + β”‚ β”‚ β”‚ MCP client + β”‚ β”‚ β”‚ + β”‚ β”‚ β”‚ client.callTool({ + β”‚ β”‚ β”‚ name: "screenshot", + β”‚ β”‚ β”‚ arguments: {} + β”‚ β”‚ β”‚ }) + β”‚ β”‚ β”‚ + β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ β”‚ β”‚ Chrome β”‚ + β”‚ β”‚ │──│ DevTools β”‚ + β”‚ β”‚ β”‚ β”‚ Protocol β”‚ + β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ β”‚ β”‚ + β”‚ β”‚ β”‚ Screenshot taken + β”‚ β”‚ β”‚ + β”‚ β”‚ tool_result β”‚ + β”‚ β”‚ ◀─────────────────────│ + β”‚ β”‚ { β”‚ + β”‚ β”‚ callId: "...", β”‚ + β”‚ β”‚ result: { β”‚ + β”‚ β”‚ content: [{ β”‚ + β”‚ β”‚ type: "image", β”‚ + β”‚ β”‚ data: "base64" β”‚ + β”‚ β”‚ }] β”‚ + β”‚ β”‚ } β”‚ + β”‚ β”‚ } β”‚ + β”‚ β”‚ β”‚ + β”‚ MCP result β”‚ β”‚ + β”‚ ◀─────────────────────│ β”‚ + β”‚ { β”‚ β”‚ + β”‚ content: [{ β”‚ β”‚ + β”‚ type: "image", β”‚ β”‚ + β”‚ data: "base64" β”‚ β”‚ + β”‚ }] β”‚ β”‚ + β”‚ } β”‚ β”‚ + β”‚ β”‚ β”‚ + β–Ό β–Ό β–Ό +``` + +--- + +## User Experience + +### Initial Setup + +#### Step 1: Install Bridge + +```bash +npm install -g @tpmjs/bridge +``` + +#### Step 2: Initialize Configuration + +```bash +tpmjs-bridge init + +# Output: +# Created ~/.tpmjs/bridge.json +# Created ~/.tpmjs/credentials.json +# +# Next steps: +# 1. Run: tpmjs-bridge login +# 2. Add MCP servers to ~/.tpmjs/bridge.json +# 3. Run: tpmjs-bridge start +``` + +#### Step 3: Login + +```bash +tpmjs-bridge login + +# Opens browser to tpmjs.com/auth/bridge +# User authenticates +# Token saved to ~/.tpmjs/credentials.json + +# Output: +# βœ“ Logged in as ajax@example.com +``` + +#### Step 4: Add MCP Servers + +```bash +# Add from preset list +tpmjs-bridge add chrome-devtools +tpmjs-bridge add blender +tpmjs-bridge add filesystem --args "/home/user/documents" + +# Or manually edit ~/.tpmjs/bridge.json +``` + +**Config file structure:** + +```json +{ + "servers": [ + { + "id": "chrome-devtools", + "name": "Chrome DevTools", + "command": "npx", + "args": ["-y", "chrome-devtools-mcp"], + "env": {} + }, + { + "id": "blender", + "name": "Blender", + "command": "uvx", + "args": ["blender-mcp"], + "env": {} + }, + { + "id": "filesystem", + "name": "Filesystem", + "command": "npx", + "args": ["-y", "@anthropic/mcp-filesystem", "/home/user/documents"], + "env": {} + } + ] +} +``` + +#### Step 5: Start Bridge + +```bash +tpmjs-bridge start + +# Output: +# Starting MCP servers... +# βœ“ chrome-devtools: 5 tools (navigate, screenshot, click, type, evaluate) +# βœ“ blender: 12 tools (create_object, modify_mesh, render, ...) +# βœ“ filesystem: 4 tools (read_file, write_file, list_directory, search) +# +# Connecting to TPMJS... +# βœ“ Connected as ajax@example.com +# βœ“ Registered 21 tools +# +# Bridge running. Press Ctrl+C to stop. +# +# Your tools are available at: +# https://tpmjs.com/api/mcp/ajax/unified/http +``` + +### Managing Tools in UI + +#### Dashboard View + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Collection: unified β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ Bridge Status: ● Connected Last seen: just now β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Your MCP Endpoint (copy to Claude Desktop config): β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ https://tpmjs.com/api/mcp/ajax/unified/http [Copy] β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ ═══════════════════════════════════════════════════════════════════════ β”‚ +β”‚ β”‚ +β”‚ TOOLS IN COLLECTION (8) β”‚ +β”‚ β”‚ +β”‚ πŸ“¦ npm Tools β”‚ +β”‚ β”œβ”€β”€ @tpmjs/hello / helloWorldTool β”‚ +β”‚ └── @tpmjs/weather / getWeather β”‚ +β”‚ β”‚ +β”‚ πŸ”— chrome-devtools ● Connected β”‚ +β”‚ β”œβ”€β”€ navigate Navigate browser to URL [βˆ’] β”‚ +β”‚ β”œβ”€β”€ screenshot Take page screenshot [βˆ’] β”‚ +β”‚ └── click Click an element [βˆ’] β”‚ +β”‚ β”‚ +β”‚ πŸ”— blender ● Connected β”‚ +β”‚ β”œβ”€β”€ create_object Create 3D object [βˆ’] β”‚ +β”‚ β”œβ”€β”€ render Render scene to image [βˆ’] β”‚ +β”‚ └── export Export to file format [βˆ’] β”‚ +β”‚ β”‚ +β”‚ ═══════════════════════════════════════════════════════════════════════ β”‚ +β”‚ β”‚ +β”‚ AVAILABLE FROM BRIDGE (not in collection) [+ Add All] β”‚ +β”‚ β”‚ +β”‚ πŸ”— chrome-devtools β”‚ +β”‚ β”œβ”€β”€ type Type text into element [+] β”‚ +β”‚ └── evaluate Run JavaScript [+] β”‚ +β”‚ β”‚ +β”‚ πŸ”— blender β”‚ +β”‚ β”œβ”€β”€ modify_mesh Modify mesh geometry [+] β”‚ +β”‚ β”œβ”€β”€ apply_material Apply material [+] β”‚ +β”‚ β”œβ”€β”€ animate Create animation [+] β”‚ +β”‚ └── ... 6 more [+] β”‚ +β”‚ β”‚ +β”‚ πŸ”— filesystem β”‚ +β”‚ β”œβ”€β”€ read_file Read file contents [+] β”‚ +β”‚ β”œβ”€β”€ write_file Write to file [+] β”‚ +β”‚ β”œβ”€β”€ list_directory List directory contents [+] β”‚ +β”‚ └── search Search for files [+] β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +#### Adding a Tool + +User clicks [+] next to a tool: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Add Tool to Collection β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ Tool: blender / modify_mesh β”‚ +β”‚ β”‚ +β”‚ Description: β”‚ +β”‚ Modify the geometry of a mesh object in Blender. β”‚ +β”‚ Supports operations like subdivide, smooth, and β”‚ +β”‚ extrude. β”‚ +β”‚ β”‚ +β”‚ Parameters: β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ object_name (string, required) β”‚ β”‚ +β”‚ β”‚ Name of the mesh object to modify β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ operation (enum, required) β”‚ β”‚ +β”‚ β”‚ One of: subdivide, smooth, extrude, bevel β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ amount (number, optional) β”‚ β”‚ +β”‚ β”‚ Amount/intensity of the operation β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ ⚠️ Requires bridge connection β”‚ +β”‚ β”‚ +β”‚ [Cancel] [Add to Collection] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +#### Offline State + +When bridge is disconnected: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Collection: unified β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ Bridge Status: β—‹ Disconnected Last seen: 2 hours ago β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ ⚠️ Bridge tools unavailable β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ To reconnect, run on your machine: β”‚ β”‚ +β”‚ β”‚ $ tpmjs-bridge start β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ TOOLS IN COLLECTION (8) β”‚ +β”‚ β”‚ +β”‚ πŸ“¦ npm Tools ● Available β”‚ +β”‚ β”œβ”€β”€ @tpmjs/hello / helloWorldTool β”‚ +β”‚ └── @tpmjs/weather / getWeather β”‚ +β”‚ β”‚ +β”‚ πŸ”— chrome-devtools β—‹ Offline β”‚ +β”‚ β”œβ”€β”€ navigate ⚠️ Requires bridge β”‚ +β”‚ β”œβ”€β”€ screenshot ⚠️ Requires bridge β”‚ +β”‚ └── click ⚠️ Requires bridge β”‚ +β”‚ β”‚ +β”‚ πŸ”— blender β—‹ Offline β”‚ +β”‚ β”œβ”€β”€ create_object ⚠️ Requires bridge β”‚ +β”‚ └── render ⚠️ Requires bridge β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Technical Specification + +### Package: `@tpmjs/bridge` + +#### Installation + +```bash +npm install -g @tpmjs/bridge +# or +npx @tpmjs/bridge +``` + +#### CLI Commands + +| Command | Description | +|---------|-------------| +| `init` | Create config files | +| `login` | Authenticate with TPMJS | +| `logout` | Remove credentials | +| `add ` | Add MCP server from preset | +| `remove ` | Remove MCP server | +| `list` | List configured servers | +| `start` | Start bridge daemon | +| `stop` | Stop bridge daemon | +| `status` | Show connection status | +| `config` | Open config file in editor | + +#### Config Files + +**~/.tpmjs/bridge.json** +```json +{ + "servers": [ + { + "id": "chrome-devtools", + "name": "Chrome DevTools", + "command": "npx", + "args": ["-y", "chrome-devtools-mcp"], + "env": { + "CHROME_PATH": "/Applications/Google Chrome.app" + }, + "disabled": false + } + ] +} +``` + +**~/.tpmjs/credentials.json** +```json +{ + "apiKey": "tpmjs_xxxxxxxxxxxxxxxxxxxx", + "userId": "user_abc123", + "email": "user@example.com", + "expiresAt": "2026-01-12T00:00:00Z" +} +``` + +#### Presets + +Built-in presets for common MCP servers: + +| Preset | Package | Description | +|--------|---------|-------------| +| `chrome-devtools` | `chrome-devtools-mcp` | Chrome browser automation | +| `browser-mcp` | `@anthropic/browser-mcp` | Puppeteer-based automation | +| `filesystem` | `@anthropic/mcp-filesystem` | File system access | +| `git` | `@anthropic/mcp-git` | Git operations | +| `slack` | `@anthropic/mcp-slack` | Slack integration | +| `blender` | `blender-mcp` | Blender 3D automation | +| `postgres` | `@anthropic/mcp-postgres` | PostgreSQL access | + +### Package: `@tpmjs/mcp-client` + +Internal library for connecting to MCP servers. + +```typescript +import { MCPClientManager } from '@tpmjs/mcp-client'; + +const manager = new MCPClientManager(); + +// Connect to a stdio-based MCP server +await manager.connect({ + id: 'chrome', + transport: 'stdio', + command: 'npx', + args: ['-y', 'chrome-devtools-mcp'], +}); + +// List tools +const tools = await manager.listTools('chrome'); + +// Call a tool +const result = await manager.callTool('chrome', 'screenshot', { + fullPage: true +}); + +// Disconnect +await manager.disconnect('chrome'); +``` + +### WebSocket Protocol + +#### Connection + +``` +wss://tpmjs.com/api/bridge?token=tpmjs_xxxx +``` + +#### Messages: Bridge β†’ TPMJS + +**Register Tools** +```json +{ + "type": "register", + "tools": [ + { + "serverId": "chrome-devtools", + "serverName": "Chrome DevTools", + "name": "screenshot", + "description": "Take a screenshot of the current page", + "inputSchema": { + "type": "object", + "properties": { + "fullPage": { + "type": "boolean", + "description": "Capture full scrollable page" + } + } + } + } + ] +} +``` + +**Tool Result** +```json +{ + "type": "tool_result", + "callId": "call_abc123", + "result": { + "content": [ + { + "type": "image", + "mimeType": "image/png", + "data": "base64..." + } + ] + } +} +``` + +**Tool Error** +```json +{ + "type": "tool_error", + "callId": "call_abc123", + "error": { + "code": "EXECUTION_FAILED", + "message": "Chrome is not running" + } +} +``` + +**Heartbeat** +```json +{ + "type": "heartbeat", + "timestamp": 1705123456789 +} +``` + +#### Messages: TPMJS β†’ Bridge + +**Tool Call** +```json +{ + "type": "tool_call", + "callId": "call_abc123", + "serverId": "chrome-devtools", + "toolName": "screenshot", + "args": { + "fullPage": true + } +} +``` + +**Ping** +```json +{ + "type": "ping" +} +``` + +--- + +## Database Schema + +### New Models + +```prisma +// Track active bridge connections +model BridgeConnection { + id String @id @default(cuid()) + userId String @unique + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // Connection state + status String @default("disconnected") // "connected" | "disconnected" + socketId String? // Internal socket identifier for routing + + // Cached tool definitions from bridge + tools Json @default("[]") + + // Metadata + lastSeen DateTime? + clientVersion String? + clientOS String? + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([status]) +} + +// Track which bridge tools are added to collections +model CollectionBridgeTool { + id String @id @default(cuid()) + collectionId String + collection Collection @relation(fields: [collectionId], references: [id], onDelete: Cascade) + + // Reference to bridge tool + serverId String // e.g., "chrome-devtools" + toolName String // e.g., "screenshot" + + // Display customization + displayName String? // Override tool name in MCP + note String? // User notes + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([collectionId, serverId, toolName]) + @@index([collectionId]) +} + +// Update Collection model +model Collection { + // ... existing fields ... + + bridgeTools CollectionBridgeTool[] +} +``` + +### Migration + +```sql +-- CreateTable +CREATE TABLE "BridgeConnection" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'disconnected', + "socketId" TEXT, + "tools" JSONB NOT NULL DEFAULT '[]', + "lastSeen" TIMESTAMP(3), + "clientVersion" TEXT, + "clientOS" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "BridgeConnection_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CollectionBridgeTool" ( + "id" TEXT NOT NULL, + "collectionId" TEXT NOT NULL, + "serverId" TEXT NOT NULL, + "toolName" TEXT NOT NULL, + "displayName" TEXT, + "note" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "CollectionBridgeTool_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "BridgeConnection_userId_key" ON "BridgeConnection"("userId"); +CREATE INDEX "BridgeConnection_status_idx" ON "BridgeConnection"("status"); +CREATE UNIQUE INDEX "CollectionBridgeTool_collectionId_serverId_toolName_key" + ON "CollectionBridgeTool"("collectionId", "serverId", "toolName"); + +-- AddForeignKey +ALTER TABLE "BridgeConnection" ADD CONSTRAINT "BridgeConnection_userId_fkey" + FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +ALTER TABLE "CollectionBridgeTool" ADD CONSTRAINT "CollectionBridgeTool_collectionId_fkey" + FOREIGN KEY ("collectionId") REFERENCES "Collection"("id") ON DELETE CASCADE ON UPDATE CASCADE; +``` + +--- + +## API Specification + +### Bridge WebSocket Endpoint + +**Endpoint**: `GET /api/bridge` + +**Query Parameters**: +- `token` (required): User's API key + +**Upgrade**: WebSocket + +**Authentication**: Validates API key, returns 401 if invalid + +### Bridge Status API + +**Endpoint**: `GET /api/user/bridge` + +**Response**: +```json +{ + "success": true, + "data": { + "status": "connected", + "lastSeen": "2025-01-12T10:30:00Z", + "toolCount": 21, + "servers": [ + { + "id": "chrome-devtools", + "name": "Chrome DevTools", + "toolCount": 5, + "tools": ["navigate", "screenshot", "click", "type", "evaluate"] + }, + { + "id": "blender", + "name": "Blender", + "toolCount": 12, + "tools": ["create_object", "modify_mesh", "render", "..."] + } + ] + } +} +``` + +### Collection Bridge Tools API + +**Add Tool**: `POST /api/collections/{id}/bridge-tools` + +```json +{ + "serverId": "chrome-devtools", + "toolName": "screenshot" +} +``` + +**Remove Tool**: `DELETE /api/collections/{id}/bridge-tools/{toolId}` + +**List Tools**: `GET /api/collections/{id}/bridge-tools` + +```json +{ + "success": true, + "data": [ + { + "id": "cbt_abc123", + "serverId": "chrome-devtools", + "toolName": "screenshot", + "displayName": null, + "note": null, + "available": true + } + ] +} +``` + +### Updated MCP Handlers + +**tools/list** now includes bridge tools: + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "tools": [ + { + "name": "tpmjs-hello--helloWorldTool", + "description": "Says hello world", + "inputSchema": { ... } + }, + { + "name": "chrome--screenshot", + "description": "Take a screenshot", + "inputSchema": { ... } + }, + { + "name": "blender--create_object", + "description": "Create 3D object", + "inputSchema": { ... } + } + ] + } +} +``` + +**tools/call** routes appropriately: + +```typescript +async function handleToolsCall(toolName: string, args: unknown) { + const [prefix, actualName] = parseToolName(toolName); + + if (isNpmTool(prefix)) { + // Route to sandbox executor + return executeNpmTool(prefix, actualName, args); + } + + if (isBridgeTool(prefix)) { + // Route to user's bridge + const bridge = await getBridgeConnection(userId); + if (!bridge || bridge.status !== 'connected') { + throw new Error('Bridge not connected. Run `tpmjs-bridge start` to connect.'); + } + return callBridgeTool(bridge, prefix, actualName, args); + } +} +``` + +--- + +## Security Considerations + +### Authentication + +1. **Bridge Authentication**: API key required for WebSocket connection +2. **Key Rotation**: Support key rotation without disconnecting active bridges +3. **Session Tokens**: Short-lived session tokens for active connections + +### Authorization + +1. **User Isolation**: Each user's bridge is isolated +2. **Collection Scoping**: Bridge tools only accessible in user's own collections +3. **Tool Whitelisting**: Users explicitly add tools to collections + +### Data Protection + +1. **No Credential Storage**: Bridge stores credentials locally only +2. **Encrypted Transport**: WSS required (TLS) +3. **Tool Arguments**: Logged but redacted for sensitive fields + +### Rate Limiting + +1. **Connection Limit**: 1 bridge per user +2. **Tool Call Rate**: 60 calls/minute per bridge +3. **Reconnection Backoff**: Exponential backoff on repeated failures + +--- + +## Implementation Phases + +### Phase 1: Bridge Core (3-4 weeks) + +**Goal**: Basic bridge functionality + +**Deliverables**: +- [ ] `@tpmjs/mcp-client` package +- [ ] `@tpmjs/bridge` CLI with `init`, `start`, `stop` +- [ ] WebSocket endpoint `/api/bridge` +- [ ] `BridgeConnection` database model +- [ ] Basic UI showing bridge status + +**Acceptance Criteria**: +- Bridge can connect to TPMJS +- Bridge can spawn local MCP servers +- Tools are registered with TPMJS +- Tool calls route through bridge + +### Phase 2: Collection Integration (2 weeks) + +**Goal**: Add bridge tools to collections + +**Deliverables**: +- [ ] `CollectionBridgeTool` model +- [ ] API for adding/removing bridge tools +- [ ] UI for managing bridge tools in collections +- [ ] Updated MCP handlers for bridge tools + +**Acceptance Criteria**: +- Users can add bridge tools to collections +- MCP endpoint includes selected bridge tools +- Tool calls execute correctly + +### Phase 3: Polish & Presets (2 weeks) + +**Goal**: Great user experience + +**Deliverables**: +- [ ] `tpmjs-bridge add ` command +- [ ] Preset library for common MCP servers +- [ ] Auto-reconnection logic +- [ ] Better error messages +- [ ] Connection health monitoring + +**Acceptance Criteria**: +- New users can set up in <5 minutes +- Bridge handles disconnections gracefully +- Clear feedback when things go wrong + +### Phase 4: Advanced Features (Ongoing) + +**Goal**: Power user features + +**Deliverables**: +- [ ] Multiple bridge support (different machines) +- [ ] Bridge groups/profiles +- [ ] Tool filtering/search in UI +- [ ] Usage analytics +- [ ] Browser extension alternative to CLI + +--- + +## Success Metrics + +### Adoption + +| Metric | Target (3 months) | +|--------|-------------------| +| Bridge installs | 1,000 | +| Daily active bridges | 200 | +| Tools registered via bridge | 5,000 | + +### Reliability + +| Metric | Target | +|--------|--------| +| Bridge uptime | 99% (when running) | +| Tool call success rate | 95% | +| Reconnection success | 99% | + +### Performance + +| Metric | Target | +|--------|--------| +| Tool call latency (p50) | <500ms | +| Tool call latency (p95) | <2000ms | +| Bridge startup time | <10s | + +--- + +## Open Questions + +### Q1: Multiple Bridges? + +**Question**: Should we support multiple bridges per user (e.g., work laptop + home desktop)? + +**Options**: +1. One bridge per user (simpler) +2. Multiple bridges with naming (more flexible) +3. Bridge "profiles" that can be switched + +**Recommendation**: Start with one, add multiple in Phase 4 + +### Q2: Offline Tool Caching? + +**Question**: Should bridge tools show in MCP list when bridge is offline? + +**Options**: +1. Hide tools when offline +2. Show tools but return error on call +3. Cache last-known tools, show with warning + +**Recommendation**: Option 3 - better UX, clear expectations + +### Q3: Tool Permissions? + +**Question**: Should we add permission scopes to bridge tools? + +**Options**: +1. All-or-nothing access +2. Per-tool permissions +3. Capability-based (read, write, execute) + +**Recommendation**: Start with all-or-nothing, add granular later + +### Q4: Daemon vs On-Demand? + +**Question**: Should bridge run as a daemon or on-demand? + +**Options**: +1. Daemon (always running) +2. On-demand (start when needed) +3. Hybrid (start on login, sleep when idle) + +**Recommendation**: Daemon for now, easier to reason about + +--- + +## Appendix + +### A: Example Bridge Session + +``` +$ tpmjs-bridge start --verbose + +[10:30:00] Loading config from ~/.tpmjs/bridge.json +[10:30:00] Found 3 servers configured + +[10:30:00] Starting chrome-devtools... +[10:30:01] Spawning: npx -y chrome-devtools-mcp +[10:30:03] Connected via stdio +[10:30:03] Discovering tools... +[10:30:03] Found 5 tools: navigate, screenshot, click, type, evaluate + +[10:30:03] Starting blender... +[10:30:03] Spawning: uvx blender-mcp +[10:30:05] Connected via stdio +[10:30:05] Discovering tools... +[10:30:05] Found 12 tools: create_object, modify_mesh, render, ... + +[10:30:05] Starting filesystem... +[10:30:05] Spawning: npx -y @anthropic/mcp-filesystem /home/user +[10:30:06] Connected via stdio +[10:30:06] Discovering tools... +[10:30:06] Found 4 tools: read_file, write_file, list_directory, search + +[10:30:06] Connecting to TPMJS... +[10:30:06] WebSocket: wss://tpmjs.com/api/bridge +[10:30:07] Authenticated as ajax@example.com +[10:30:07] Registering 21 tools... +[10:30:07] Registration complete + +[10:30:07] Bridge ready! +[10:30:07] Tools available at: https://tpmjs.com/api/mcp/ajax/unified/http + +[10:32:15] Tool call: chrome-devtools/navigate +[10:32:15] Args: { url: "https://github.com" } +[10:32:16] Result: Success (1.2s) + +[10:32:18] Tool call: chrome-devtools/screenshot +[10:32:18] Args: { fullPage: true } +[10:32:20] Result: Success (1.8s) + +^C +[10:45:00] Shutting down... +[10:45:00] Disconnecting from TPMJS +[10:45:00] Stopping chrome-devtools +[10:45:00] Stopping blender +[10:45:00] Stopping filesystem +[10:45:01] Bridge stopped +``` + +### B: Claude Desktop Configuration + +**Before (multiple servers)**: +```json +{ + "mcpServers": { + "tpmjs": { + "type": "url", + "url": "https://tpmjs.com/api/mcp/ajax/tools/http" + }, + "chrome": { + "command": "npx", + "args": ["-y", "chrome-devtools-mcp"] + }, + "blender": { + "command": "uvx", + "args": ["blender-mcp"] + }, + "files": { + "command": "npx", + "args": ["-y", "@anthropic/mcp-filesystem", "/home/user"] + } + } +} +``` + +**After (single server)**: +```json +{ + "mcpServers": { + "tpmjs": { + "type": "url", + "url": "https://tpmjs.com/api/mcp/ajax/unified/http" + } + } +} +``` + +### C: Error Messages + +| Scenario | Error Message | +|----------|---------------| +| Bridge not connected | "Bridge not connected. Run `tpmjs-bridge start` on your machine to enable local tools." | +| Tool not found | "Tool 'blender--render' not found. Make sure it's added to your collection." | +| Server not responding | "The blender MCP server is not responding. Check that Blender is running." | +| Timeout | "Tool call timed out after 5 minutes. The operation may still be running." | +| Auth failed | "Bridge authentication failed. Run `tpmjs-bridge login` to refresh credentials." | diff --git a/docs/TPMJS-ARCHITECTURE.md b/docs/TPMJS-ARCHITECTURE.md new file mode 100644 index 0000000..425f506 --- /dev/null +++ b/docs/TPMJS-ARCHITECTURE.md @@ -0,0 +1,930 @@ +# TPMJS: Tool Platform for Model Junctions + +A comprehensive guide to how TPMJS works, its architecture, and strategies for handling local/computer-controlling tools in a remote execution environment. + +--- + +## Table of Contents + +1. [What is TPMJS?](#what-is-tpmjs) +2. [Core Architecture](#core-architecture) +3. [Tool Execution Flow](#tool-execution-flow) +4. [The Local Tool Challenge](#the-local-tool-challenge) +5. [Solution Strategies](#solution-strategies) +6. [Implementation Roadmap](#implementation-roadmap) + +--- + +## What is TPMJS? + +TPMJS (Tool Platform for Model Junctions) is an open platform for discovering, sharing, and executing AI agent tools. Think of it as "npm for AI tools" - developers publish tool packages to npm with a special `tpmjs` field, and the platform automatically discovers, catalogs, and makes them executable through AI agents. + +### Key Capabilities + +- **Tool Discovery**: Automatically syncs with npm to find packages with the `tpmjs` keyword +- **Tool Registry**: Catalogs tools with metadata, quality scores, and health checks +- **Agent Builder**: Create AI agents with custom tool collections +- **Remote Execution**: Execute npm package tools in isolated sandbox environments +- **Multi-Provider Support**: Works with OpenAI, Anthropic, Google, Groq, Mistral, and more +- **MCP Protocol Support**: Expose collections as MCP servers for use with Claude Desktop, etc. + +### How Tools Get Published + +Developers add a `tpmjs` field to their package.json: + +```json +{ + "name": "@company/my-tool", + "keywords": ["tpmjs"], + "tpmjs": { + "tools": { + "myTool": { + "description": "Does something useful", + "export": "myTool" + } + } + } +} +``` + +The platform discovers this via npm's changes feed and keyword search, validates the package, and adds it to the registry. + +--- + +## Core Architecture + +### System Components + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ TPMJS Platform β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Web App β”‚ β”‚ Playground β”‚ β”‚ NPM Registry β”‚ β”‚ +β”‚ β”‚ (Next.js) β”‚ β”‚ (Testing) β”‚ β”‚ (Package Source) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β–Ό β–Ό β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ +β”‚ β”‚ API Layer β”‚β”‚ +β”‚ β”‚ β€’ /api/chat - Agent conversations β”‚β”‚ +β”‚ β”‚ β€’ /api/sync - NPM package discovery β”‚β”‚ +β”‚ β”‚ β€’ /api/agents - Agent CRUD β”‚β”‚ +β”‚ β”‚ β€’ /api/mcp - MCP protocol endpoints β”‚β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ +β”‚ β”‚ Tool Execution Layer β”‚β”‚ +β”‚ β”‚ β”‚β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚β”‚ +β”‚ β”‚ β”‚ Sandbox β”‚ β”‚ Custom β”‚ β”‚ Local Executor β”‚ β”‚β”‚ +β”‚ β”‚ β”‚ Executor β”‚ β”‚ Executor β”‚ β”‚ (Future) β”‚ β”‚β”‚ +β”‚ β”‚ β”‚ (Default) β”‚ β”‚ (User URL) β”‚ β”‚ β”‚ β”‚β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Database Schema (Key Models) + +``` +Package (npm-level metadata) + β”œβ”€β”€ Tool (individual tool within package) + β”‚ β”œβ”€β”€ inputSchema (JSON Schema) + β”‚ β”œβ”€β”€ health status (HEALTHY/BROKEN/UNKNOWN) + β”‚ └── quality score + β”‚ +Agent (user-created AI agent) + β”œβ”€β”€ Collections (grouped tools) + β”‚ β”œβ”€β”€ CollectionTool (join table) + β”‚ β”œβ”€β”€ executorConfig + β”‚ └── envVars + β”œβ”€β”€ Individual Tools + β”œβ”€β”€ Conversations + β”‚ └── Messages (USER/ASSISTANT/TOOL) + └── Configuration + β”œβ”€β”€ provider, modelId + β”œβ”€β”€ systemPrompt + β”œβ”€β”€ executorType, executorConfig + └── envVars +``` + +### Executor Types + +1. **Sandbox Executor (Default)** + - Remote service that loads npm packages dynamically + - Isolated execution environment + - 5-minute timeout per execution + - Supports environment variables + +2. **Custom Executor** + - User-provided URL endpoint + - Optional API key authentication + - Same interface as sandbox executor + - Useful for private tools or specialized environments + +3. **Configuration Cascade** + ``` + Agent Config β†’ Collection Config β†’ System Default + ``` + +--- + +## Tool Execution Flow + +### End-to-End Request Flow + +``` +User Message + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Chat API Endpoint β”‚ +β”‚ /api/chat/[user]/[agent]/conversation β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Agent Resolution β”‚ +β”‚ β€’ Fetch agent with collections/tools β”‚ +β”‚ β€’ Resolve executor config β”‚ +β”‚ β€’ Merge environment variables β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Build Tool Definitions β”‚ +β”‚ β€’ Convert TPMJS tools β†’ AI SDK tools β”‚ +β”‚ β€’ Create execute functions with config β”‚ +β”‚ β€’ Inject env vars into executors β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ AI Provider Stream β”‚ +β”‚ β€’ Stream text response β”‚ +β”‚ β€’ Intercept tool calls β”‚ +β”‚ β€’ Execute tools and stream results β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Tool Execution β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ executeWithExecutor() β”‚ β”‚ +β”‚ β”‚ β†’ resolveExecutorConfig() β”‚ β”‚ +β”‚ β”‚ β†’ executePackage() [sandbox] β”‚ β”‚ +β”‚ β”‚ OR β”‚ β”‚ +β”‚ β”‚ β†’ executeWithCustomUrl() β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Remote Sandbox Service β”‚ +β”‚ β€’ Dynamic import via esm.sh β”‚ +β”‚ β€’ Execute tool function β”‚ +β”‚ β€’ Return result β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Response & Persistence β”‚ +β”‚ β€’ Stream result to client (SSE) β”‚ +β”‚ β€’ Save messages to database β”‚ +β”‚ β€’ Track token usage β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### SSE Event Types + +```typescript +// During streaming, clients receive these events: +{ type: 'chunk', content: 'AI response text...' } +{ type: 'tool_call', toolCallId, toolName, args } +{ type: 'tool_result', toolCallId, toolName, result } +{ type: 'tokens', inputTokens, outputTokens } +{ type: 'complete' } +``` + +--- + +## The Local Tool Challenge + +### The Problem + +Many powerful AI tools require access to the user's local environment: + +| Tool Type | Examples | Why Local? | +|-----------|----------|------------| +| **Browser Automation** | Chrome control, Puppeteer, Playwright | Needs access to user's browser, sessions, cookies | +| **File System** | Read/write local files | Operates on user's documents | +| **Desktop Automation** | Mouse/keyboard control, screenshots | Interacts with user's desktop | +| **Development Tools** | Git, terminal, IDE | Operates in user's dev environment | +| **System Utilities** | Clipboard, notifications, system settings | Requires OS-level access | +| **Database Access** | Local PostgreSQL, SQLite | Connects to local database servers | + +### Current TPMJS Limitation + +TPMJS executes tools in a **remote sandbox environment**: + +``` +User's Machine TPMJS Cloud +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ β”‚ β”‚ +β”‚ Browser β”‚ ──HTTP POST───▢ β”‚ Sandbox Executor β”‚ +β”‚ (Chat UI) β”‚ β”‚ (Isolated VM) β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ Chrome β”‚ β”‚ βœ— No access to β”‚ +β”‚ Files β”‚ β”‚ user's Chrome β”‚ +β”‚ Desktop β”‚ β”‚ βœ— No access to β”‚ +β”‚ β”‚ β”‚ user's files β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +Tools that need local access simply **cannot work** in the remote sandbox because: + +1. **No Network Path**: The sandbox cannot "reach back" to the user's machine +2. **Security Isolation**: Sandboxes are intentionally isolated for security +3. **Session State**: User's browser sessions, cookies, and auth state are local +4. **Hardware Access**: Screen, mouse, keyboard are local peripherals + +### MCP: A Partial Solution + +The **Model Context Protocol (MCP)** addresses this by running tools locally: + +``` +User's Machine +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Claude β”‚ β”‚ MCP Server β”‚ β”‚ +β”‚ β”‚ Desktop │◀──▢│ (Local) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Chrome β”‚ β”‚ +β”‚ β”‚ Files β”‚ β”‚ +β”‚ β”‚ Desktop β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**But MCP has limitations:** +- Only works with MCP-compatible clients (Claude Desktop, some IDEs) +- Cannot be used from web interfaces +- Requires manual server setup per user +- No centralized tool discovery/registry + +--- + +## Solution Strategies + +The goal is to enable local tool execution while maintaining TPMJS's web-based, shareable agent experience. Here are potential approaches: + +### Strategy 1: Hybrid Executor Bridge + +**Concept**: User runs a lightweight agent on their machine that bridges TPMJS to local tools. + +``` +User's Machine TPMJS Cloud +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Local Bridge │◀─┼─WSS──▢│ β”‚ TPMJS API β”‚ β”‚ +β”‚ β”‚ Agent β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ Tool execution β”‚ +β”‚ β–Ό β”‚ β”‚ request comes in β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ Local Tools β”‚ β”‚ β”‚ β–Ό β”‚ +β”‚ β”‚ β€’ Chrome β”‚ β”‚ β”‚ If local tool: β”‚ +β”‚ β”‚ β€’ Files β”‚ β”‚ β”‚ β†’ Forward to β”‚ +β”‚ β”‚ β€’ Desktop β”‚ β”‚ β”‚ user's bridge β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ Else: β”‚ +β”‚ β”‚ β”‚ β†’ Use sandbox β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Implementation Details:** + +1. **Bridge Agent**: + - Electron app, CLI tool, or background service + - Maintains WebSocket connection to TPMJS + - Listens for tool execution requests + - Executes local tools and returns results + +2. **Routing Logic**: + - Tools marked with `local: true` in metadata + - TPMJS routes these to user's connected bridge + - Falls back to remote execution for non-local tools + +3. **Authentication**: + - Bridge authenticates with user's TPMJS API key + - Each bridge registered to specific user/agent + - Secure tunnel for sensitive operations + +**Pros:** +- Works from web UI +- Mix of local and remote tools +- User controls what's exposed + +**Cons:** +- Requires user to install/run software +- Bridge must stay connected +- Adds latency for local calls + +--- + +### Strategy 2: Browser Extension with Native Messaging + +**Concept**: Browser extension handles local tool execution via native messaging host. + +``` +Browser (TPMJS Chat) +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚ +β”‚ β”‚ TPMJS β”‚ β”‚ TPMJS Extension β”‚β”‚ +β”‚ β”‚ Web App │◀──▢│ β€’ Intercepts local calls β”‚β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β€’ Native messaging β”‚β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Native Messaging Host β”‚ + β”‚ (Python/Node process) β”‚ + β”‚ β”‚ + β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ + β”‚ β”‚ Local Tool Executors β”‚ β”‚ + β”‚ β”‚ β€’ Puppeteer β”‚ β”‚ + β”‚ β”‚ β€’ File system β”‚ β”‚ + β”‚ β”‚ β€’ Shell commands β”‚ β”‚ + β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Implementation Details:** + +1. **Browser Extension**: + - Injects into TPMJS pages + - Intercepts tool execution for local-marked tools + - Communicates via native messaging + +2. **Native Messaging Host**: + - Installed separately on user's machine + - Registered with browser for extension communication + - Executes actual local operations + +3. **Tool Routing**: + - Extension registers available local tools + - TPMJS checks for local tool availability + - Routes appropriately + +**Pros:** +- Seamless web experience +- No separate app window needed +- Browser handles connection management + +**Cons:** +- Chrome/Firefox only (browser dependency) +- Complex installation (extension + native host) +- Native messaging has message size limits + +--- + +### Strategy 3: Local-First with Cloud Sync + +**Concept**: Run agent locally with cloud sync for sharing/collaboration. + +``` +User's Machine (Primary) TPMJS Cloud +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ TPMJS Desktop │◀─┼──sync──▢│ β”‚ Agent Config β”‚ β”‚ +β”‚ β”‚ (Electron/Tauri) β”‚ β”‚ β”‚ β”‚ Conversationsβ”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ Tool Registryβ”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β–Ό β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ For sharing: β”‚ +β”‚ β”‚ Local Execution β”‚ β”‚ β”‚ Expose via URL β”‚ +β”‚ β”‚ β€’ All tools run β”‚ β”‚ β”‚ with remote exec β”‚ +β”‚ β”‚ locally β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Implementation Details:** + +1. **Desktop Application**: + - Full TPMJS experience in native app + - All tool execution happens locally + - Syncs agent configs and conversations to cloud + +2. **Sharing Mode**: + - Public agents can run from cloud + - Non-local tools execute remotely + - Local tools marked as "requires desktop app" + +3. **Hybrid Operation**: + - Use web when away from main machine + - Use desktop for full local access + - Conversations sync between both + +**Pros:** +- Full local access +- Works offline +- Best performance for local tools + +**Cons:** +- Requires desktop app installation +- Sync complexity +- Different experience web vs desktop + +--- + +### Strategy 4: Tunnel Service (ngrok-style) + +**Concept**: User runs local executor and exposes it via secure tunnel. + +``` +User's Machine TPMJS Cloud +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Local Executor β”‚ β”‚ β”‚ β”‚ Tunnel Service β”‚ β”‚ +β”‚ β”‚ + TPMJS Tunnel │──┼────▢│ β”‚ user123.tpmjs.tunnel β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Local Resources β”‚ β”‚ β”‚ β”‚ Agent routes local β”‚ β”‚ +β”‚ β”‚ β€’ Chrome β”‚ β”‚ β”‚ β”‚ tools to tunnel URL β”‚ β”‚ +β”‚ β”‚ β€’ Files β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Implementation Details:** + +1. **Tunnel CLI**: + ```bash + npx tpmjs-tunnel --port 3847 --token + ``` + - Starts local executor service + - Connects to TPMJS tunnel service + - Gets assigned a unique tunnel URL + +2. **Agent Configuration**: + - User sets executor type to "tunnel" + - TPMJS routes tool calls to their tunnel URL + - Tunnel forwards to local executor + +3. **Security**: + - Authenticated tunnel connection + - HTTPS everywhere + - User can whitelist specific tools + +**Pros:** +- Simple CLI-based setup +- Works with any tools +- User controls exposure + +**Cons:** +- Tunnel must stay connected +- Potential latency +- Costs for tunnel infrastructure + +--- + +### Strategy 5: WebRTC Peer Connection + +**Concept**: Direct peer-to-peer connection between browser and local executor. + +``` +Browser (TPMJS Chat) User's Machine +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ β”‚ β”‚ +β”‚ TPMJS Web App │◀─WebRTC─▢│ Local Executor β”‚ +β”‚ with WebRTC client β”‚ β”‚ with WebRTC server β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Tool call comes inβ”‚ β”‚ β”‚ β”‚ Execute local β”‚ β”‚ +β”‚ β”‚ Check: is local? β”‚ β”‚ β”‚ β”‚ tool, return β”‚ β”‚ +β”‚ β”‚ Yes β†’ Send P2P β”‚ β”‚ β”‚ β”‚ result P2P β”‚ β”‚ +β”‚ β”‚ No β†’ Send cloud β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”‚ Signaling + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ TPMJS Signaling Server β”‚ +β”‚ (Connection setup only)β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Implementation Details:** + +1. **WebRTC Setup**: + - TPMJS provides signaling server + - Browser and local executor establish P2P connection + - Data channel for tool calls/results + +2. **Local Executor**: + - Desktop app or CLI with WebRTC support + - Advertises available local tools + - Handles incoming tool calls + +3. **Connection Flow**: + - User opens TPMJS, local executor connects + - Signaling exchanges connection info + - Direct P2P connection established + - Tool calls bypass cloud entirely + +**Pros:** +- Very low latency +- No tunnel infrastructure needed +- Direct, secure connection + +**Cons:** +- WebRTC complexity (NAT traversal) +- May not work on all networks +- Both ends need WebRTC support + +--- + +### Strategy 6: Container-Based Local Executor + +**Concept**: User runs a Docker container that connects to TPMJS. + +```bash +docker run -v /home:/home \ + -e TPMJS_API_KEY=xxx \ + ghcr.io/tpmjs/local-executor +``` + +``` +User's Machine (Docker) TPMJS Cloud +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ TPMJS Container │◀─┼─WSS─▢│ β”‚ TPMJS API β”‚ β”‚ +β”‚ β”‚ β€’ Pre-installed β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ tools β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β€’ Mount user dirs β”‚ β”‚ β”‚ Routes local tools β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ to container β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ +β”‚ β”‚ Mounted Volumes β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β€’ /home (files) β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β€’ /var/run/docker β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ (nested Docker) β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Implementation Details:** + +1. **Container Image**: + - Pre-installed common tools (Puppeteer, etc.) + - WebSocket client to TPMJS + - Configurable volume mounts + +2. **Tool Execution**: + - Container receives tool calls via WebSocket + - Executes with access to mounted volumes + - Returns results + +3. **Browser Automation**: + - Container could run headless Chrome + - Or use browser running on host via port mapping + - VNC for visual debugging + +**Pros:** +- Consistent environment +- Easy distribution via Docker Hub +- Isolated yet with controlled access + +**Cons:** +- Docker dependency +- Limited GUI access +- Complex browser automation setup + +--- + +### Strategy 7: Agent-to-Agent Delegation + +**Concept**: Cloud agent delegates local tasks to user's local agent. + +``` +TPMJS Cloud User's Machine +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Cloud Agent β”‚ β”‚ β”‚ β”‚ Local Agent β”‚ β”‚ +β”‚ β”‚ (Primary) β”‚ β”‚ β”‚ β”‚ (MCP Server) β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ When local tool │──┼────▢│ β”‚ Receives task, β”‚ β”‚ +β”‚ β”‚ needed, delegate to β”‚ β”‚ β”‚ β”‚ executes locally β”‚ β”‚ +β”‚ β”‚ local agent │◀─┼─────│ β”‚ returns result β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β–Ό β”‚ +β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ β”‚ β”‚ Chrome, Files β”‚ β”‚ +β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Implementation Details:** + +1. **Local Agent**: + - Runs as MCP server + - Connected to cloud agent via tool + - Advertises local capabilities + +2. **Delegation Tool**: + ```typescript + delegateToLocal({ + task: "Take a screenshot of the current page", + context: { ... } + }) + ``` + +3. **Execution Flow**: + - Cloud agent determines task needs local access + - Uses delegation tool to send to local agent + - Local agent executes and returns result + - Cloud agent incorporates result + +**Pros:** +- Clean separation of concerns +- Cloud agent coordinates, local executes +- Scales well conceptually + +**Cons:** +- Adds complexity (two agents) +- Potential context loss between agents +- Requires sophisticated delegation logic + +--- + +### Strategy 8: Progressive Enhancement + +**Concept**: Same tools work in cloud (limited) and local (full), with graceful degradation. + +```typescript +// Tool definition with progressive capability +{ + name: "readFile", + capabilities: { + remote: { + description: "Read files from sandboxed storage", + restrictions: ["sandbox-only", "size-limit-1mb"] + }, + local: { + description: "Read any accessible file", + restrictions: [] + } + }, + execute: async (input, context) => { + if (context.isLocal) { + return fs.readFile(input.path); + } else { + return sandboxFs.readFile(input.sandboxPath); + } + } +} +``` + +**Implementation Details:** + +1. **Tool Metadata**: + - Tools declare remote and local capabilities + - Different restrictions per environment + - Same function name, different behaviors + +2. **UI Indication**: + - Show which capabilities are available + - Prompt user to connect local executor for full access + - Graceful fallback to remote when local unavailable + +3. **Runtime Detection**: + - Check for local executor connection + - Route to appropriate implementation + - Surface limitations in tool output + +**Pros:** +- Works everywhere, better locally +- Clear capability communication +- No hard failures + +**Cons:** +- Dual implementation complexity +- User confusion about capabilities +- Tool authors must handle both cases + +--- + +### Strategy 9: Cloudflare Workers + Durable Objects + +**Concept**: Edge execution with persistent state, user provides API access. + +``` +User configures API credentials + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Cloudflare Edge β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Durable Object (per user) β”‚ β”‚ +β”‚ β”‚ β€’ Persistent WebSocket to user services β”‚ β”‚ +β”‚ β”‚ β€’ Cached credentials (encrypted) β”‚ β”‚ +β”‚ β”‚ β€’ Session state for browser automation β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β–Ό β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Tool Exec β”‚ β”‚ Browser (remote) β”‚ β”‚ +β”‚ β”‚ (fast) β”‚ β”‚ via Browserless.io β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +User's configured services +(if they have public APIs) +``` + +**Implementation Details:** + +1. **Edge Functions**: + - Execute tools at edge, close to user + - Durable Objects maintain state + - Low latency for most operations + +2. **Remote Browser Services**: + - Integrate with Browserless, Browserbase, etc. + - User provides API keys for these services + - Browser runs "close enough" to cloud + +3. **User's Services**: + - If user has self-hosted services with APIs + - Configure credentials in TPMJS + - Edge function calls user's services + +**Pros:** +- Low latency edge execution +- No local installation required +- Scales with Cloudflare infrastructure + +**Cons:** +- Still remote execution +- Requires paid browser services +- Not truly local access + +--- + +### Strategy 10: Sandboxed Local VM + +**Concept**: TPMJS provisions a secure VM on user's machine. + +``` +User's Machine +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β”‚ +β”‚ Host OS β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ TPMJS Sandbox VM β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ (Firecracker/gVisor/WASM) β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ Controlled network access β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ Mounted specific directories β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ Pre-approved tools only β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ Resource limits (CPU, RAM, time) β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ User approves: β”‚ β”‚ +β”‚ β”‚ βœ“ Mount ~/Documents (read-only) β”‚ β”‚ +β”‚ β”‚ βœ“ Allow outbound HTTPS β”‚ β”‚ +β”‚ β”‚ βœ— Deny keylogger access β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Implementation Details:** + +1. **Micro-VM Technology**: + - Firecracker for lightweight VMs + - gVisor for container sandboxing + - WebAssembly for in-browser sandboxing + +2. **Capability-Based Security**: + - User explicitly grants permissions + - File system mounts with restrictions + - Network access whitelisting + - Hardware access controls + +3. **Tool Verification**: + - Only signed/verified tools can run + - Code review for local-capable tools + - Sandboxed execution even locally + +**Pros:** +- Security through isolation +- Fine-grained permissions +- Local but controlled + +**Cons:** +- Complex implementation +- Performance overhead +- Still limited vs native access + +--- + +## Implementation Roadmap + +Based on complexity, impact, and user experience, here's a suggested prioritization: + +### Phase 1: Foundation (Weeks 1-4) + +**Strategy 4: Tunnel Service** +- Lowest friction entry point +- Works with existing TPMJS architecture +- Users already familiar with ngrok-style tools + +**Deliverables:** +1. `tpmjs-tunnel` CLI package +2. Tunnel relay service on tpmjs.com +3. Agent executor type "tunnel" +4. Documentation and getting started guide + +### Phase 2: Better UX (Weeks 5-8) + +**Strategy 2: Browser Extension** +- Eliminates CLI requirement for web users +- Seamless web experience +- Works on any platform with Chrome/Firefox + +**Deliverables:** +1. TPMJS Browser Extension +2. Native messaging host installer +3. Local tool capability detection +4. Extension distribution (Chrome Web Store, Firefox Add-ons) + +### Phase 3: Power Users (Weeks 9-12) + +**Strategy 3: Local-First Desktop App** +- Full power for power users +- Offline support +- Best performance + +**Deliverables:** +1. TPMJS Desktop (Electron or Tauri) +2. Sync protocol for agents/conversations +3. Hybrid mode (web fallback) + +### Phase 4: Advanced (Future) + +**Strategy 8: Progressive Enhancement** +- Make existing tools smarter +- Better capability communication +- Graceful degradation + +**Strategy 6: Container-Based Executor** +- For DevOps/engineer users +- Reproducible environments +- CI/CD integration + +--- + +## Summary + +TPMJS's remote execution model works well for stateless, API-based tools but faces challenges with local/computer-controlling tools. The solution isn't one-size-fits-all: + +| User Type | Best Strategy | Why | +|-----------|---------------|-----| +| **Casual User** | Browser Extension | No CLI, just install extension | +| **Developer** | Tunnel Service | Familiar CLI workflow | +| **Power User** | Desktop App | Full control, best performance | +| **Enterprise** | Container + Custom Executor | Controlled, auditable | + +The key insight is that **local execution isn't a single feature but a spectrum** of approaches, each with different trade-offs between: +- Ease of setup +- Security +- Performance +- Capability breadth + +TPMJS should support multiple approaches, letting users choose based on their needs and comfort level. diff --git a/packages/bridge/package.json b/packages/bridge/package.json new file mode 100644 index 0000000..647b3f3 --- /dev/null +++ b/packages/bridge/package.json @@ -0,0 +1,56 @@ +{ + "name": "@tpmjs/bridge", + "version": "0.1.0", + "description": "Bridge CLI for connecting local MCP servers to TPMJS", + "author": "TPMJS", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/tpmjs/tpmjs.git", + "directory": "packages/bridge" + }, + "homepage": "https://tpmjs.com", + "keywords": [ + "tpmjs", + "mcp", + "bridge", + "cli", + "model-context-protocol" + ], + "type": "module", + "bin": { + "tpmjs-bridge": "./dist/cli.js" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "start": "node dist/cli.js", + "type-check": "tsc --noEmit", + "clean": "rm -rf dist .turbo" + }, + "dependencies": { + "@tpmjs/mcp-client": "workspace:*", + "commander": "^14.0.0", + "picocolors": "^1.1.1", + "ws": "^8.18.2" + }, + "devDependencies": { + "@tpmjs/tsconfig": "workspace:*", + "@types/node": "^22.15.29", + "@types/ws": "^8.18.1", + "tsup": "^8.5.1", + "typescript": "^5.9.3" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/bridge/src/bridge.ts b/packages/bridge/src/bridge.ts new file mode 100644 index 0000000..116a390 --- /dev/null +++ b/packages/bridge/src/bridge.ts @@ -0,0 +1,275 @@ +import type { MCPServerConfig } from '@tpmjs/mcp-client'; +import { MCPClientManager } from '@tpmjs/mcp-client'; +import pc from 'picocolors'; + +interface BridgeToolCall { + callId: string; + serverId: string; + toolName: string; + args: Record; +} + +export interface BridgeOptions { + /** TPMJS API key */ + apiKey: string; + /** TPMJS API URL */ + apiUrl?: string; + /** MCP servers to connect to */ + servers: MCPServerConfig[]; + /** Poll interval in ms */ + pollInterval?: number; + /** Heartbeat interval in ms */ + heartbeatInterval?: number; + /** Verbose logging */ + verbose?: boolean; +} + +export class Bridge { + private mcpManager: MCPClientManager; + private options: Required; + private isRunning = false; + private pollTimeout: ReturnType | null = null; + private heartbeatTimeout: ReturnType | null = null; + + constructor(options: BridgeOptions) { + this.options = { + apiUrl: 'https://tpmjs.com', + pollInterval: 1000, // Poll every 1 second + heartbeatInterval: 30000, // Heartbeat every 30 seconds + verbose: false, + ...options, + }; + + this.mcpManager = new MCPClientManager({ + onStatusChange: (serverId, status, error) => { + if (this.options.verbose) { + if (status === 'connected') { + this.log(` ${pc.green('βœ“')} ${serverId} connected`); + } else if (status === 'error') { + this.log(` ${pc.red('βœ—')} ${serverId} error: ${error}`); + } + } + }, + }); + } + + /** + * Start the bridge + */ + async start(): Promise { + this.isRunning = true; + + this.log(pc.bold('Starting TPMJS Bridge...\n')); + + // 1. Connect to all local MCP servers + this.log('Connecting to MCP servers:'); + for (const server of this.options.servers) { + try { + this.log(` Starting ${pc.cyan(server.name)}...`); + const tools = await this.mcpManager.connect(server); + this.log(` ${pc.green('βœ“')} ${server.name}: ${tools.length} tools`); + if (this.options.verbose) { + for (const tool of tools) { + this.log(` - ${tool.name}`); + } + } + } catch (error) { + this.log(` ${pc.red('βœ—')} ${server.name}: ${(error as Error).message}`); + } + } + + // 2. Register with TPMJS + this.log('\nConnecting to TPMJS...'); + await this.registerTools(); + + // 3. Start polling for tool calls + this.startPolling(); + this.startHeartbeat(); + } + + /** + * Stop the bridge + */ + async stop(): Promise { + this.isRunning = false; + + this.log('\nShutting down...'); + + // Clear timers + if (this.pollTimeout) { + clearTimeout(this.pollTimeout); + this.pollTimeout = null; + } + if (this.heartbeatTimeout) { + clearTimeout(this.heartbeatTimeout); + this.heartbeatTimeout = null; + } + + // Notify TPMJS we're disconnecting + try { + await fetch(`${this.options.apiUrl}/api/bridge`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${this.options.apiKey}`, + }, + }); + } catch { + // Ignore disconnect errors + } + + // Disconnect all MCP servers + await this.mcpManager.disconnectAll(); + + this.log('Bridge stopped'); + } + + private async registerTools(): Promise { + const allTools = this.mcpManager.listAllTools(); + + const response = await fetch(`${this.options.apiUrl}/api/bridge`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.options.apiKey}`, + }, + body: JSON.stringify({ + type: 'register', + tools: allTools.map(({ serverId, serverName, tool }) => ({ + serverId, + serverName, + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema, + })), + clientVersion: '0.1.0', + clientOS: process.platform, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to register: ${error}`); + } + + this.log(`${pc.green('βœ“')} Connected to TPMJS`); + this.log(`Registered ${allTools.length} tools`); + } + + private startPolling(): void { + const poll = async () => { + if (!this.isRunning) return; + + try { + const response = await fetch(`${this.options.apiUrl}/api/bridge`, { + method: 'GET', + headers: { + Authorization: `Bearer ${this.options.apiKey}`, + }, + }); + + if (response.ok) { + const data = (await response.json()) as { calls?: BridgeToolCall[] }; + const calls = data.calls || []; + + // Process each tool call + for (const call of calls) { + await this.handleToolCall(call); + } + } + } catch (error) { + if (this.options.verbose) { + this.log(`${pc.yellow('!')} Poll error: ${(error as Error).message}`); + } + } + + // Schedule next poll + if (this.isRunning) { + this.pollTimeout = setTimeout(poll, this.options.pollInterval); + } + }; + + poll(); + } + + private startHeartbeat(): void { + const heartbeat = async () => { + if (!this.isRunning) return; + + try { + await fetch(`${this.options.apiUrl}/api/bridge`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.options.apiKey}`, + }, + body: JSON.stringify({ type: 'heartbeat' }), + }); + } catch { + // Ignore heartbeat errors + } + + if (this.isRunning) { + this.heartbeatTimeout = setTimeout(heartbeat, this.options.heartbeatInterval); + } + }; + + this.heartbeatTimeout = setTimeout(heartbeat, this.options.heartbeatInterval); + } + + private async handleToolCall(call: BridgeToolCall): Promise { + const { callId, serverId, toolName, args } = call; + + if (this.options.verbose) { + this.log(`Tool call: ${serverId}/${toolName}`); + } + + try { + const result = await this.mcpManager.callTool(serverId, toolName, args); + + await fetch(`${this.options.apiUrl}/api/bridge`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.options.apiKey}`, + }, + body: JSON.stringify({ + type: 'tool_result', + callId, + result: { + content: result.content, + isError: result.isError, + }, + }), + }); + + if (this.options.verbose) { + this.log(` ${pc.green('βœ“')} Result sent`); + } + } catch (error) { + await fetch(`${this.options.apiUrl}/api/bridge`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.options.apiKey}`, + }, + body: JSON.stringify({ + type: 'tool_result', + callId, + error: { + code: 'EXECUTION_FAILED', + message: (error as Error).message, + }, + }), + }); + + if (this.options.verbose) { + this.log(` ${pc.red('βœ—')} Error: ${(error as Error).message}`); + } + } + } + + private log(message: string): void { + const timestamp = new Date().toLocaleTimeString(); + console.log(`${pc.dim(`[${timestamp}]`)} ${message}`); + } +} diff --git a/packages/bridge/src/cli.ts b/packages/bridge/src/cli.ts new file mode 100644 index 0000000..82c3c26 --- /dev/null +++ b/packages/bridge/src/cli.ts @@ -0,0 +1,232 @@ +import type { MCPServerConfig } from '@tpmjs/mcp-client'; +import { Command } from 'commander'; +import pc from 'picocolors'; +import { Bridge } from './bridge.js'; +import { + createDefaultConfig, + deleteCredentials, + ensureConfigDir, + getConfigPath, + loadConfig, + loadCredentials, + saveConfig, + saveCredentials, +} from './config.js'; + +const program = new Command(); + +program.name('tpmjs-bridge').description('Bridge local MCP servers to TPMJS').version('0.1.0'); + +// Init command +program + .command('init') + .description('Initialize bridge configuration') + .action(() => { + ensureConfigDir(); + const config = loadConfig(); + + if (config.servers.length === 0) { + createDefaultConfig(); + console.log(`${pc.green('βœ“')} Created config file: ${getConfigPath()}`); + console.log('\nEdit the config file to add your MCP servers, then run:'); + console.log(` ${pc.cyan('tpmjs-bridge login')}`); + console.log(` ${pc.cyan('tpmjs-bridge start')}`); + } else { + console.log(`Config file already exists: ${getConfigPath()}`); + } + }); + +// Login command +program + .command('login') + .description('Authenticate with TPMJS') + .option('--api-key ', 'API key (or set TPMJS_API_KEY env var)') + .action((options) => { + const apiKey = options.apiKey || process.env.TPMJS_API_KEY; + + if (!apiKey) { + console.log(`${pc.red('βœ—')} No API key provided`); + console.log('\nProvide an API key via:'); + console.log(` ${pc.cyan('tpmjs-bridge login --api-key ')}`); + console.log(` ${pc.cyan('TPMJS_API_KEY= tpmjs-bridge login')}`); + console.log('\nGet your API key at: https://tpmjs.com/dashboard/settings/api-keys'); + process.exit(1); + } + + saveCredentials({ apiKey }); + console.log(`${pc.green('βœ“')} API key saved`); + }); + +// Logout command +program + .command('logout') + .description('Remove saved credentials') + .action(() => { + deleteCredentials(); + console.log(`${pc.green('βœ“')} Credentials removed`); + }); + +// Add command +program + .command('add ') + .description('Add an MCP server to the config') + .option('--command ', 'Command to run', 'npx') + .option('--args ', 'Arguments (comma-separated)', '') + .action((name, options) => { + const config = loadConfig(); + + // Check if already exists + if (config.servers.some((s) => s.id === name)) { + console.log(`${pc.yellow('!')} Server "${name}" already exists`); + return; + } + + const server: MCPServerConfig = { + id: name, + name: name, + transport: 'stdio', + command: options.command, + args: options.args ? options.args.split(',') : [], + }; + + config.servers.push(server); + saveConfig(config); + console.log(`${pc.green('βœ“')} Added server: ${name}`); + }); + +// Remove command +program + .command('remove ') + .description('Remove an MCP server from the config') + .action((name) => { + const config = loadConfig(); + const index = config.servers.findIndex((s) => s.id === name); + + if (index === -1) { + console.log(`${pc.yellow('!')} Server "${name}" not found`); + return; + } + + config.servers.splice(index, 1); + saveConfig(config); + console.log(`${pc.green('βœ“')} Removed server: ${name}`); + }); + +// List command +program + .command('list') + .description('List configured MCP servers') + .action(() => { + const config = loadConfig(); + + if (config.servers.length === 0) { + console.log('No servers configured'); + console.log(`\nRun ${pc.cyan('tpmjs-bridge init')} to create a config file`); + return; + } + + console.log('Configured MCP servers:\n'); + for (const server of config.servers) { + console.log(` ${pc.cyan(server.id)}`); + console.log(` Name: ${server.name}`); + console.log(` Command: ${server.command} ${(server.args || []).join(' ')}`); + console.log(); + } + }); + +// Config command +program + .command('config') + .description('Show config file path') + .action(() => { + console.log(`Config file: ${getConfigPath()}`); + }); + +// Start command +program + .command('start') + .description('Start the bridge') + .option('-v, --verbose', 'Verbose output') + .option('--url ', 'Custom WebSocket URL') + .action(async (options) => { + const config = loadConfig(); + const credentials = loadCredentials(); + + // Check for API key + const apiKey = credentials?.apiKey || process.env.TPMJS_API_KEY; + if (!apiKey) { + console.log(`${pc.red('βœ—')} Not authenticated`); + console.log(`\nRun ${pc.cyan('tpmjs-bridge login')} first`); + process.exit(1); + } + + // Check for servers + if (config.servers.length === 0) { + console.log(`${pc.yellow('!')} No MCP servers configured`); + console.log(`\nEdit ${getConfigPath()} to add servers`); + process.exit(1); + } + + // Filter out example servers + const servers = config.servers.filter((s) => s.id !== 'example'); + if (servers.length === 0) { + console.log(`${pc.yellow('!')} Only example server configured`); + console.log(`\nEdit ${getConfigPath()} to add real servers`); + process.exit(1); + } + + const bridge = new Bridge({ + apiKey, + servers, + verbose: options.verbose, + apiUrl: options.url, + }); + + // Handle shutdown + const shutdown = async () => { + await bridge.stop(); + process.exit(0); + }; + + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); + + try { + await bridge.start(); + console.log(`\n${pc.green('Bridge running.')} Press Ctrl+C to stop.\n`); + } catch (error) { + console.log(`${pc.red('βœ—')} Failed to start bridge: ${(error as Error).message}`); + process.exit(1); + } + }); + +// Status command +program + .command('status') + .description('Show bridge status') + .action(() => { + const config = loadConfig(); + const credentials = loadCredentials(); + + console.log('Bridge Status\n'); + + // Auth status + if (credentials?.apiKey) { + console.log(` Auth: ${pc.green('βœ“')} Logged in`); + } else { + console.log(` Auth: ${pc.red('βœ—')} Not logged in`); + } + + // Config status + console.log(` Config: ${getConfigPath()}`); + console.log(` Servers: ${config.servers.length}`); + + if (config.servers.length > 0) { + console.log('\n Configured servers:'); + for (const server of config.servers) { + console.log(` - ${server.name} (${server.id})`); + } + } + }); + +program.parse(); diff --git a/packages/bridge/src/config.ts b/packages/bridge/src/config.ts new file mode 100644 index 0000000..5a1f6b7 --- /dev/null +++ b/packages/bridge/src/config.ts @@ -0,0 +1,113 @@ +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import type { BridgeConfig, BridgeCredentials } from './types.js'; + +const CONFIG_DIR = path.join(os.homedir(), '.tpmjs'); +const CONFIG_FILE = path.join(CONFIG_DIR, 'bridge.json'); +const CREDENTIALS_FILE = path.join(CONFIG_DIR, 'credentials.json'); + +/** + * Ensure the config directory exists + */ +export function ensureConfigDir(): void { + if (!fs.existsSync(CONFIG_DIR)) { + fs.mkdirSync(CONFIG_DIR, { recursive: true }); + } +} + +/** + * Load bridge configuration + */ +export function loadConfig(): BridgeConfig { + ensureConfigDir(); + + if (!fs.existsSync(CONFIG_FILE)) { + return { servers: [] }; + } + + try { + const content = fs.readFileSync(CONFIG_FILE, 'utf-8'); + return JSON.parse(content) as BridgeConfig; + } catch { + return { servers: [] }; + } +} + +/** + * Save bridge configuration + */ +export function saveConfig(config: BridgeConfig): void { + ensureConfigDir(); + fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); +} + +/** + * Load credentials + */ +export function loadCredentials(): BridgeCredentials | null { + ensureConfigDir(); + + if (!fs.existsSync(CREDENTIALS_FILE)) { + return null; + } + + try { + const content = fs.readFileSync(CREDENTIALS_FILE, 'utf-8'); + return JSON.parse(content) as BridgeCredentials; + } catch { + return null; + } +} + +/** + * Save credentials + */ +export function saveCredentials(credentials: BridgeCredentials): void { + ensureConfigDir(); + fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), { + mode: 0o600, // Only owner can read/write + }); +} + +/** + * Delete credentials + */ +export function deleteCredentials(): void { + if (fs.existsSync(CREDENTIALS_FILE)) { + fs.unlinkSync(CREDENTIALS_FILE); + } +} + +/** + * Get config file path + */ +export function getConfigPath(): string { + return CONFIG_FILE; +} + +/** + * Get credentials file path + */ +export function getCredentialsPath(): string { + return CREDENTIALS_FILE; +} + +/** + * Create default config file + */ +export function createDefaultConfig(): void { + const defaultConfig: BridgeConfig = { + servers: [ + { + id: 'example', + name: 'Example MCP Server', + transport: 'stdio', + command: 'npx', + args: ['-y', '@example/mcp-server'], + }, + ], + }; + + saveConfig(defaultConfig); +} diff --git a/packages/bridge/src/index.ts b/packages/bridge/src/index.ts new file mode 100644 index 0000000..fc8bb37 --- /dev/null +++ b/packages/bridge/src/index.ts @@ -0,0 +1,18 @@ +export { Bridge, type BridgeOptions } from './bridge.js'; +export { + createDefaultConfig, + deleteCredentials, + ensureConfigDir, + getConfigPath, + getCredentialsPath, + loadConfig, + loadCredentials, + saveConfig, + saveCredentials, +} from './config.js'; +export type { + BridgeConfig, + BridgeCredentials, + BridgeToServerMessage, + ServerToBridgeMessage, +} from './types.js'; diff --git a/packages/bridge/src/types.ts b/packages/bridge/src/types.ts new file mode 100644 index 0000000..fa3e341 --- /dev/null +++ b/packages/bridge/src/types.ts @@ -0,0 +1,84 @@ +import type { MCPServerConfig, MCPTool } from '@tpmjs/mcp-client'; + +/** + * Bridge configuration file structure + */ +export interface BridgeConfig { + /** MCP servers to connect to */ + servers: MCPServerConfig[]; +} + +/** + * Credentials file structure + */ +export interface BridgeCredentials { + /** TPMJS API key */ + apiKey: string; + /** User ID */ + userId?: string; + /** User email */ + email?: string; +} + +/** + * Message from bridge to TPMJS + */ +export type BridgeToServerMessage = + | { + type: 'register'; + tools: Array<{ + serverId: string; + serverName: string; + name: string; + description?: string; + inputSchema: MCPTool['inputSchema']; + }>; + } + | { + type: 'tool_result'; + callId: string; + result: { + content: Array<{ + type: string; + text?: string; + mimeType?: string; + data?: string; + }>; + isError?: boolean; + }; + } + | { + type: 'tool_error'; + callId: string; + error: { + code: string; + message: string; + }; + } + | { + type: 'heartbeat'; + timestamp: number; + }; + +/** + * Message from TPMJS to bridge + */ +export type ServerToBridgeMessage = + | { + type: 'tool_call'; + callId: string; + serverId: string; + toolName: string; + args: Record; + } + | { + type: 'ping'; + } + | { + type: 'registered'; + toolCount: number; + } + | { + type: 'error'; + message: string; + }; diff --git a/packages/bridge/tsconfig.json b/packages/bridge/tsconfig.json new file mode 100644 index 0000000..6521d56 --- /dev/null +++ b/packages/bridge/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@tpmjs/tsconfig/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "incremental": false, + "composite": false + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/bridge/tsup.config.ts b/packages/bridge/tsup.config.ts new file mode 100644 index 0000000..badbc4c --- /dev/null +++ b/packages/bridge/tsup.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig([ + { + entry: ['src/index.ts'], + format: ['esm'], + dts: true, + clean: true, + sourcemap: true, + }, + { + entry: ['src/cli.ts'], + format: ['esm'], + dts: false, + clean: false, + sourcemap: true, + banner: { + js: '#!/usr/bin/env node', + }, + }, +]); diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index cf4510b..d39c4a4 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -337,15 +337,16 @@ model User { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") - sessions Session[] - accounts Account[] - collections Collection[] - agents Agent[] - apiKeys UserApiKey[] - toolLikes ToolLike[] - collectionLikes CollectionLike[] - agentLikes AgentLike[] - activities UserActivity[] + sessions Session[] + accounts Account[] + collections Collection[] + agents Agent[] + apiKeys UserApiKey[] + toolLikes ToolLike[] + collectionLikes CollectionLike[] + agentLikes AgentLike[] + activities UserActivity[] + bridgeConnection BridgeConnection? @@index([username]) @@map("users") @@ -438,6 +439,7 @@ model Collection { tools CollectionTool[] agents AgentCollection[] likes CollectionLike[] + bridgeTools CollectionBridgeTool[] // Unique constraint: user can't have duplicate collection slugs @@unique([userId, slug]) @@ -827,3 +829,60 @@ model EndpointHealthReport { @@index([source]) @@map("endpoint_health_reports") } + +// ============================================================================ +// MCP Bridge Models +// ============================================================================ + +/// BridgeConnection - tracks active bridge connections from users +model BridgeConnection { + id String @id @default(cuid()) + + // Owner relationship + userId String @unique @map("user_id") + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + // Connection state + status String @default("disconnected") @db.VarChar(20) // 'connected' | 'disconnected' + socketId String? @map("socket_id") @db.VarChar(100) // Internal socket identifier for routing + + // Cached tool definitions from bridge + tools Json @default("[]") @db.JsonB + + // Metadata + lastSeen DateTime? @map("last_seen") + clientVersion String? @map("client_version") @db.VarChar(20) + clientOS String? @map("client_os") @db.VarChar(50) + + // Timestamps + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@index([status]) + @@map("bridge_connections") +} + +/// CollectionBridgeTool - tracks which bridge tools are added to collections +model CollectionBridgeTool { + id String @id @default(cuid()) + + // Collection relationship + collectionId String @map("collection_id") + collection Collection @relation(fields: [collectionId], references: [id], onDelete: Cascade) + + // Bridge tool reference + serverId String @map("server_id") @db.VarChar(100) // e.g., "chrome-devtools" + toolName String @map("tool_name") @db.VarChar(100) // e.g., "screenshot" + + // Display customization + displayName String? @map("display_name") @db.VarChar(100) // Override tool name in MCP + note String? @db.VarChar(500) // User notes + + // Timestamps + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@unique([collectionId, serverId, toolName]) + @@index([collectionId]) + @@map("collection_bridge_tools") +} diff --git a/packages/mcp-client/package.json b/packages/mcp-client/package.json new file mode 100644 index 0000000..b59ae04 --- /dev/null +++ b/packages/mcp-client/package.json @@ -0,0 +1,48 @@ +{ + "name": "@tpmjs/mcp-client", + "version": "0.1.0", + "description": "MCP client library for connecting to Model Context Protocol servers", + "author": "TPMJS", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/tpmjs/tpmjs.git", + "directory": "packages/mcp-client" + }, + "homepage": "https://tpmjs.com", + "keywords": [ + "tpmjs", + "mcp", + "model-context-protocol", + "ai", + "tools" + ], + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rm -rf dist .turbo" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0" + }, + "devDependencies": { + "@tpmjs/tsconfig": "workspace:*", + "@types/node": "^22.15.29", + "tsup": "^8.5.1", + "typescript": "^5.9.3" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/mcp-client/src/client-manager.ts b/packages/mcp-client/src/client-manager.ts new file mode 100644 index 0000000..28f5b35 --- /dev/null +++ b/packages/mcp-client/src/client-manager.ts @@ -0,0 +1,213 @@ +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import type { + ConnectedServer, + MCPClientStatus, + MCPServerConfig, + MCPTool, + MCPToolResult, +} from './types.js'; + +interface ManagedClient { + config: MCPServerConfig; + client: Client; + transport: StdioClientTransport; + status: MCPClientStatus; + tools: MCPTool[]; + error?: string; +} + +/** + * Manages connections to multiple MCP servers + */ +export class MCPClientManager { + private clients: Map = new Map(); + private onStatusChange?: (serverId: string, status: MCPClientStatus, error?: string) => void; + + constructor(options?: { + onStatusChange?: (serverId: string, status: MCPClientStatus, error?: string) => void; + }) { + this.onStatusChange = options?.onStatusChange; + } + + /** + * Connect to an MCP server + */ + async connect(config: MCPServerConfig): Promise { + // Disconnect existing connection if any + if (this.clients.has(config.id)) { + await this.disconnect(config.id); + } + + this.updateStatus(config.id, 'connecting'); + + try { + const client = new Client({ + name: 'tpmjs-bridge', + version: '1.0.0', + }); + + const transport = new StdioClientTransport({ + command: config.command, + args: config.args || [], + env: config.env, + }); + + await client.connect(transport); + + // Discover tools + const toolsResult = await client.listTools(); + const tools: MCPTool[] = toolsResult.tools.map((tool) => ({ + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema as MCPTool['inputSchema'], + })); + + this.clients.set(config.id, { + config, + client, + transport, + status: 'connected', + tools, + }); + + this.updateStatus(config.id, 'connected'); + return tools; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + this.updateStatus(config.id, 'error', errorMessage); + throw error; + } + } + + /** + * Disconnect from an MCP server + */ + async disconnect(serverId: string): Promise { + const managed = this.clients.get(serverId); + if (!managed) return; + + try { + await managed.client.close(); + } catch { + // Ignore close errors + } + + this.clients.delete(serverId); + this.updateStatus(serverId, 'disconnected'); + } + + /** + * Disconnect from all servers + */ + async disconnectAll(): Promise { + const ids = Array.from(this.clients.keys()); + await Promise.all(ids.map((id) => this.disconnect(id))); + } + + /** + * List tools from a specific server + */ + listTools(serverId: string): MCPTool[] { + const managed = this.clients.get(serverId); + if (!managed) { + throw new Error(`Server ${serverId} not connected`); + } + return managed.tools; + } + + /** + * List all tools from all connected servers + */ + listAllTools(): Array<{ serverId: string; serverName: string; tool: MCPTool }> { + const allTools: Array<{ serverId: string; serverName: string; tool: MCPTool }> = []; + + for (const [serverId, managed] of this.clients) { + if (managed.status === 'connected') { + for (const tool of managed.tools) { + allTools.push({ + serverId, + serverName: managed.config.name, + tool, + }); + } + } + } + + return allTools; + } + + /** + * Call a tool on a specific server + */ + async callTool( + serverId: string, + toolName: string, + args: Record + ): Promise { + const managed = this.clients.get(serverId); + if (!managed) { + throw new Error(`Server ${serverId} not connected`); + } + + if (managed.status !== 'connected') { + throw new Error(`Server ${serverId} is not connected (status: ${managed.status})`); + } + + const result = await managed.client.callTool({ + name: toolName, + arguments: args, + }); + + return { + content: result.content as MCPToolResult['content'], + isError: result.isError === true, + }; + } + + /** + * Get status of all servers + */ + getServers(): ConnectedServer[] { + return Array.from(this.clients.values()).map((managed) => ({ + id: managed.config.id, + name: managed.config.name, + status: managed.status, + tools: managed.tools, + error: managed.error, + })); + } + + /** + * Get status of a specific server + */ + getServer(serverId: string): ConnectedServer | undefined { + const managed = this.clients.get(serverId); + if (!managed) return undefined; + + return { + id: managed.config.id, + name: managed.config.name, + status: managed.status, + tools: managed.tools, + error: managed.error, + }; + } + + /** + * Check if a server is connected + */ + isConnected(serverId: string): boolean { + const managed = this.clients.get(serverId); + return managed?.status === 'connected'; + } + + private updateStatus(serverId: string, status: MCPClientStatus, error?: string): void { + const managed = this.clients.get(serverId); + if (managed) { + managed.status = status; + managed.error = error; + } + this.onStatusChange?.(serverId, status, error); + } +} diff --git a/packages/mcp-client/src/index.ts b/packages/mcp-client/src/index.ts new file mode 100644 index 0000000..86c221b --- /dev/null +++ b/packages/mcp-client/src/index.ts @@ -0,0 +1,8 @@ +export { MCPClientManager } from './client-manager.js'; +export type { + ConnectedServer, + MCPClientStatus, + MCPServerConfig, + MCPTool, + MCPToolResult, +} from './types.js'; diff --git a/packages/mcp-client/src/types.ts b/packages/mcp-client/src/types.ts new file mode 100644 index 0000000..889d9f6 --- /dev/null +++ b/packages/mcp-client/src/types.ts @@ -0,0 +1,64 @@ +/** + * Configuration for an MCP server connection + */ +export interface MCPServerConfig { + /** Unique identifier for this server */ + id: string; + /** Display name for the server */ + name: string; + /** Transport type */ + transport: 'stdio'; + /** Command to run the MCP server */ + command: string; + /** Arguments to pass to the command */ + args?: string[]; + /** Environment variables to set */ + env?: Record; +} + +/** + * MCP tool definition + */ +export interface MCPTool { + /** Tool name */ + name: string; + /** Tool description */ + description?: string; + /** JSON Schema for input parameters */ + inputSchema: { + type: 'object'; + properties?: Record; + required?: string[]; + [key: string]: unknown; + }; +} + +/** + * Result from an MCP tool call + */ +export interface MCPToolResult { + content: Array<{ + type: 'text' | 'image' | 'resource'; + text?: string; + mimeType?: string; + data?: string; + [key: string]: unknown; + }>; + isError?: boolean; +} + +/** + * Status of an MCP client connection + */ +export type MCPClientStatus = 'disconnected' | 'connecting' | 'connected' | 'error'; + +/** + * Information about a connected server + */ +export interface ConnectedServer { + id: string; + name: string; + status: MCPClientStatus; + tools: MCPTool[]; + error?: string; +} diff --git a/packages/mcp-client/tsconfig.json b/packages/mcp-client/tsconfig.json new file mode 100644 index 0000000..6521d56 --- /dev/null +++ b/packages/mcp-client/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@tpmjs/tsconfig/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "incremental": false, + "composite": false + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/mcp-client/tsup.config.ts b/packages/mcp-client/tsup.config.ts new file mode 100644 index 0000000..535937f --- /dev/null +++ b/packages/mcp-client/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + dts: true, + clean: true, + sourcemap: true, +}); diff --git a/packages/tools/test-file-writer/package.json b/packages/tools/test-file-writer/package.json new file mode 100644 index 0000000..8e7d96b --- /dev/null +++ b/packages/tools/test-file-writer/package.json @@ -0,0 +1,37 @@ +{ + "name": "@tpmjs/test-file-writer", + "version": "0.1.0", + "description": "Test MCP server for writing files - used to test the TPMJS bridge", + "author": "TPMJS", + "license": "MIT", + "type": "module", + "bin": { + "test-file-writer": "./dist/server.js" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "start": "node dist/server.js", + "type-check": "tsc --noEmit", + "clean": "rm -rf dist .turbo" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0" + }, + "devDependencies": { + "@tpmjs/tsconfig": "workspace:*", + "@types/node": "^22.15.29", + "tsup": "^8.5.1", + "typescript": "^5.9.3" + }, + "private": true +} diff --git a/packages/tools/test-file-writer/src/index.ts b/packages/tools/test-file-writer/src/index.ts new file mode 100644 index 0000000..361b4b5 --- /dev/null +++ b/packages/tools/test-file-writer/src/index.ts @@ -0,0 +1,3 @@ +// Re-export for programmatic use +export const SERVER_NAME = 'test-file-writer'; +export const SERVER_VERSION = '0.1.0'; diff --git a/packages/tools/test-file-writer/src/server.ts b/packages/tools/test-file-writer/src/server.ts new file mode 100644 index 0000000..ea8d762 --- /dev/null +++ b/packages/tools/test-file-writer/src/server.ts @@ -0,0 +1,249 @@ +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; + +// Default directory for file operations +const DEFAULT_DIR = path.join(os.homedir(), '.tpmjs', 'test-files'); + +// Ensure default directory exists +if (!fs.existsSync(DEFAULT_DIR)) { + fs.mkdirSync(DEFAULT_DIR, { recursive: true }); +} + +const server = new Server( + { + name: 'test-file-writer', + version: '0.1.0', + }, + { + capabilities: { + tools: {}, + }, + } +); + +// List available tools +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: 'write_file', + description: 'Write content to a file in the test directory', + inputSchema: { + type: 'object', + properties: { + filename: { + type: 'string', + description: 'Name of the file to write (will be created in ~/.tpmjs/test-files/)', + }, + content: { + type: 'string', + description: 'Content to write to the file', + }, + }, + required: ['filename', 'content'], + }, + }, + { + name: 'read_file', + description: 'Read content from a file in the test directory', + inputSchema: { + type: 'object', + properties: { + filename: { + type: 'string', + description: 'Name of the file to read (from ~/.tpmjs/test-files/)', + }, + }, + required: ['filename'], + }, + }, + { + name: 'list_files', + description: 'List all files in the test directory', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + { + name: 'delete_file', + description: 'Delete a file from the test directory', + inputSchema: { + type: 'object', + properties: { + filename: { + type: 'string', + description: 'Name of the file to delete (from ~/.tpmjs/test-files/)', + }, + }, + required: ['filename'], + }, + }, + { + name: 'get_info', + description: 'Get information about the test file writer MCP server', + inputSchema: { + type: 'object', + properties: {}, + }, + }, + ], + }; +}); + +// Handle tool calls +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + switch (name) { + case 'write_file': { + const { filename, content } = args as { filename: string; content: string }; + const safeName = path.basename(filename); // Prevent path traversal + const filePath = path.join(DEFAULT_DIR, safeName); + + fs.writeFileSync(filePath, content, 'utf-8'); + + return { + content: [ + { + type: 'text', + text: `Successfully wrote ${content.length} bytes to ${safeName}`, + }, + ], + }; + } + + case 'read_file': { + const { filename } = args as { filename: string }; + const safeName = path.basename(filename); + const filePath = path.join(DEFAULT_DIR, safeName); + + if (!fs.existsSync(filePath)) { + return { + content: [ + { + type: 'text', + text: `Error: File '${safeName}' not found`, + }, + ], + isError: true, + }; + } + + const content = fs.readFileSync(filePath, 'utf-8'); + + return { + content: [ + { + type: 'text', + text: content, + }, + ], + }; + } + + case 'list_files': { + const files = fs.readdirSync(DEFAULT_DIR); + + if (files.length === 0) { + return { + content: [ + { + type: 'text', + text: 'No files in test directory', + }, + ], + }; + } + + const fileInfos = files.map((file) => { + const filePath = path.join(DEFAULT_DIR, file); + const stats = fs.statSync(filePath); + return `- ${file} (${stats.size} bytes, modified ${stats.mtime.toISOString()})`; + }); + + return { + content: [ + { + type: 'text', + text: `Files in ${DEFAULT_DIR}:\n${fileInfos.join('\n')}`, + }, + ], + }; + } + + case 'delete_file': { + const { filename } = args as { filename: string }; + const safeName = path.basename(filename); + const filePath = path.join(DEFAULT_DIR, safeName); + + if (!fs.existsSync(filePath)) { + return { + content: [ + { + type: 'text', + text: `Error: File '${safeName}' not found`, + }, + ], + isError: true, + }; + } + + fs.unlinkSync(filePath); + + return { + content: [ + { + type: 'text', + text: `Successfully deleted ${safeName}`, + }, + ], + }; + } + + case 'get_info': { + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + name: 'test-file-writer', + version: '0.1.0', + directory: DEFAULT_DIR, + platform: process.platform, + nodeVersion: process.version, + }, + null, + 2 + ), + }, + ], + }; + } + + default: + return { + content: [ + { + type: 'text', + text: `Unknown tool: ${name}`, + }, + ], + isError: true, + }; + } +}); + +// Start the server +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error('Test File Writer MCP Server running on stdio'); +} + +main().catch(console.error); diff --git a/packages/tools/test-file-writer/tsconfig.json b/packages/tools/test-file-writer/tsconfig.json new file mode 100644 index 0000000..6521d56 --- /dev/null +++ b/packages/tools/test-file-writer/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@tpmjs/tsconfig/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "incremental": false, + "composite": false + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/tools/test-file-writer/tsup.config.ts b/packages/tools/test-file-writer/tsup.config.ts new file mode 100644 index 0000000..0ed2bd8 --- /dev/null +++ b/packages/tools/test-file-writer/tsup.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig([ + { + entry: ['src/index.ts'], + format: ['esm'], + dts: true, + clean: true, + sourcemap: true, + }, + { + entry: ['src/server.ts'], + format: ['esm'], + dts: false, + clean: false, + sourcemap: true, + banner: { + js: '#!/usr/bin/env node', + }, + }, +]); diff --git a/packages/types/src/collection.ts b/packages/types/src/collection.ts index d7ed739..8c96698 100644 --- a/packages/types/src/collection.ts +++ b/packages/types/src/collection.ts @@ -67,6 +67,22 @@ export const ReorderToolsSchema = z.object({ toolIds: z.array(z.string().min(1)), }); +// ============================================================================ +// Bridge Tool Schemas +// ============================================================================ + +export const AddBridgeToolToCollectionSchema = z.object({ + serverId: z.string().min(1, 'Server ID is required').max(100), + toolName: z.string().min(1, 'Tool name is required').max(100), + displayName: z.string().max(100).optional(), + note: z.string().max(500, 'Note must be 500 characters or less').optional(), +}); + +export const UpdateCollectionBridgeToolSchema = z.object({ + displayName: z.string().max(100).nullable().optional(), + note: z.string().max(500, 'Note must be 500 characters or less').nullable().optional(), +}); + // ============================================================================ // Clone Schemas // ============================================================================ @@ -127,6 +143,8 @@ export type AddToolToCollectionInput = z.infer export type UpdateCollectionToolInput = z.infer; export type ReorderToolsInput = z.infer; export type CloneCollectionInput = z.infer; +export type AddBridgeToolToCollectionInput = z.infer; +export type UpdateCollectionBridgeToolInput = z.infer; export type Collection = z.infer; export type CollectionTool = z.infer; export type CollectionWithTools = z.infer; @@ -138,6 +156,7 @@ export type CollectionWithTools = z.infer; export const COLLECTION_LIMITS = { MAX_COLLECTIONS_PER_USER: 50, MAX_TOOLS_PER_COLLECTION: 100, + MAX_BRIDGE_TOOLS_PER_COLLECTION: 50, MAX_NAME_LENGTH: 100, MAX_DESCRIPTION_LENGTH: 500, MAX_NOTE_LENGTH: 500, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98ce745..1e29bf6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -391,6 +391,37 @@ importers: specifier: ^4.0.16 version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(happy-dom@20.1.0)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.30.2)(msw@2.12.7(@types/node@25.0.3)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.2) + packages/bridge: + dependencies: + '@tpmjs/mcp-client': + specifier: workspace:* + version: link:../mcp-client + commander: + specifier: ^14.0.0 + version: 14.0.2 + picocolors: + specifier: ^1.1.1 + version: 1.1.1 + ws: + specifier: ^8.18.2 + version: 8.19.0 + devDependencies: + '@tpmjs/tsconfig': + specifier: workspace:* + version: link:../config/tsconfig + '@types/node': + specifier: ^22.15.29 + version: 22.19.5 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + tsup: + specifier: ^8.5.1 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + packages/config: dependencies: zod: @@ -487,6 +518,25 @@ importers: specifier: ^5.9.3 version: 5.9.3 + packages/mcp-client: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.12.0 + version: 1.25.2(hono@4.10.6)(zod@4.3.5) + devDependencies: + '@tpmjs/tsconfig': + specifier: workspace:* + version: link:../config/tsconfig + '@types/node': + specifier: ^22.15.29 + version: 22.19.5 + tsup: + specifier: ^8.5.1 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + packages/mocks: dependencies: msw: @@ -3493,6 +3543,25 @@ importers: specifier: ^5.9.3 version: 5.9.3 + packages/tools/test-file-writer: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.12.0 + version: 1.25.2(hono@4.10.6)(zod@4.3.5) + devDependencies: + '@tpmjs/tsconfig': + specifier: workspace:* + version: link:../../config/tsconfig + '@types/node': + specifier: ^22.15.29 + version: 22.19.5 + tsup: + specifier: ^8.5.1 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + packages/tools/unsandbox: dependencies: '@thomasdavis/unsandbox': @@ -5767,6 +5836,9 @@ packages: '@types/node@20.19.27': resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==} + '@types/node@22.19.5': + resolution: {integrity: sha512-HfF8+mYcHPcPypui3w3mvzuIErlNOh2OAG+BCeBZCEwyiD5ls2SiCwEyT47OELtf7M3nHxBdu0FsmzdKxkN52Q==} + '@types/node@25.0.3': resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} @@ -12981,6 +13053,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@22.19.5': + dependencies: + undici-types: 6.21.0 + '@types/node@25.0.3': dependencies: undici-types: 7.16.0 @@ -13036,7 +13112,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 20.19.27 + '@types/node': 25.0.3 '@typescript-eslint/eslint-plugin@8.52.0(@typescript-eslint/parser@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: