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
This commit is contained in:
parent
490d76a50b
commit
c8c1a12f22
37 changed files with 7318 additions and 23 deletions
56
packages/bridge/package.json
Normal file
56
packages/bridge/package.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
275
packages/bridge/src/bridge.ts
Normal file
275
packages/bridge/src/bridge.ts
Normal file
|
|
@ -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<string, unknown>;
|
||||
}
|
||||
|
||||
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<BridgeOptions>;
|
||||
private isRunning = false;
|
||||
private pollTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
private heartbeatTimeout: ReturnType<typeof setTimeout> | 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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
232
packages/bridge/src/cli.ts
Normal file
232
packages/bridge/src/cli.ts
Normal file
|
|
@ -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 <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 <key>')}`);
|
||||
console.log(` ${pc.cyan('TPMJS_API_KEY=<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 <name>')
|
||||
.description('Add an MCP server to the config')
|
||||
.option('--command <cmd>', 'Command to run', 'npx')
|
||||
.option('--args <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 <name>')
|
||||
.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 <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();
|
||||
113
packages/bridge/src/config.ts
Normal file
113
packages/bridge/src/config.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
18
packages/bridge/src/index.ts
Normal file
18
packages/bridge/src/index.ts
Normal file
|
|
@ -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';
|
||||
84
packages/bridge/src/types.ts
Normal file
84
packages/bridge/src/types.ts
Normal file
|
|
@ -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<string, unknown>;
|
||||
}
|
||||
| {
|
||||
type: 'ping';
|
||||
}
|
||||
| {
|
||||
type: 'registered';
|
||||
toolCount: number;
|
||||
}
|
||||
| {
|
||||
type: 'error';
|
||||
message: string;
|
||||
};
|
||||
11
packages/bridge/tsconfig.json
Normal file
11
packages/bridge/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "@tpmjs/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"incremental": false,
|
||||
"composite": false
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
21
packages/bridge/tsup.config.ts
Normal file
21
packages/bridge/tsup.config.ts
Normal file
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
]);
|
||||
Loading…
Add table
Add a link
Reference in a new issue