refactor: improved clis, setup command, readme

This commit is contained in:
gzuuus
2025-02-28 19:51:37 +01:00
parent 16caaac124
commit 6e7055c601
9 changed files with 557 additions and 79 deletions

View File

@@ -6,36 +6,76 @@ A monorepo containing packages that bridge Model Context Protocol (MCP) servers
This monorepo contains the following packages: This monorepo contains the following packages:
### [@dvmcp-bridge](./packages/dvmcp-bridge) ### [@dvmcp/bridge](./packages/dvmcp-bridge)
The bridge implementation let's you connect MCP servers to Nostr's DVM ecosystem. Handles tool announcement, execution, and status updates. The bridge implementation that connects MCP servers to Nostr's DVM ecosystem. Handles tool announcement, execution, and status updates.
### [@dvmcp-discovery](./packages/dvmcp-discovery) ### [@dvmcp/discovery](./packages/dvmcp-discovery)
A MCP server, discovery service that aggregates MCP tools from DVMs, and make their tools available A MCP server/discovery service that aggregates MCP tools from DVMs and makes their tools available.
### [@commons](./packages/commons) ### [@dvmcp/commons](./packages/dvmcp-commons)
Shared utilities and components used across DVMCP packages. Shared utilities and components used across DVMCP packages.
## Getting Started ## Installation & Usage
### Quick Start with NPX (No Installation)
You can run the packages directly using `npx` without installing them:
1. Install dependencies:
```bash ```bash
bun install # Run the bridge
npx @dvmcp/bridge
# Run the discovery service
npx @dvmcp/discovery
``` ```
2. Set up configurations: The interactive CLI will guide you through configuration setup on first run.
```bash
# For the bridge
cp packages/dvmcp-bridge/config.example.yml packages/dvmcp-bridge/config.yml
# For the discovery service ### Global Installation
cp packages/dvmcp-discovery/config.example.yml packages/dvmcp-discovery/config.yml
```bash
# Install the packages globally
npm install -g @dvmcp/bridge @dvmcp/discovery
# Run the commands
dvmcp-bridge
dvmcp-discovery
``` ```
3. Edit the configuration files according to your needs. ## Setting Up a Bridge
To expose your MCP server as a DVM on Nostr:
1. Navigate to the directory where you want to configure the bridge
2. Run: `npx @dvmcp/bridge`
3. Follow the interactive setup to configure:
- Your MCP server path
- Nostr private key (or generate a new one)
- Relays to connect to
4. The bridge will start and begin proxying requests between Nostr and your MCP server
## Setting Up a Discovery Service
To aggregate MCP tools from DVMs:
1. Navigate to your desired directory
2. Run: `npx @dvmcp/discovery`
3. Follow the setup to configure:
- Nostr private key
- Relays to monitor
## Development ## Development
For contributors to this repository:
```bash ```bash
# Clone the repo
git clone https://github.com/gzuuus/dvmcp.git
cd dvmcp
# Install dependencies
bun install
# Start the bridge in development mode # Start the bridge in development mode
bun run dev --cwd packages/dvmcp-bridge bun run dev --cwd packages/dvmcp-bridge
@@ -43,22 +83,12 @@ bun run dev --cwd packages/dvmcp-bridge
bun run dev --cwd packages/dvmcp-discovery bun run dev --cwd packages/dvmcp-discovery
``` ```
## Production
```bash
# Start the bridge
bun run start --cwd packages/dvmcp-bridge
# Start the discovery service
bun run start --cwd packages/dvmcp-discovery
```
## Documentation ## Documentation
- [DVMCP Specification](./docs/dvmcp-spec.md) - [DVMCP Specification](./docs/dvmcp-spec.md)
- [Bridge Package](./packages/dvmcp-bridge/README.md) - [Bridge Package](./packages/dvmcp-bridge/README.md)
- [Discovery Package](./packages/dvmcp-discovery/README.md) - [Discovery Package](./packages/dvmcp-discovery/README.md)
- [Commons Package](./packages/commons/README.md) - [Commons Package](./packages/dvmcp-commons/README.md)
## Contributing ## Contributing

View File

@@ -3,7 +3,7 @@
"scripts": { "scripts": {
"start:mcp-dvm": "cd packages/mcp-dvm-bridge && bun start", "start:mcp-dvm": "cd packages/mcp-dvm-bridge && bun start",
"format": "prettier --write \"packages/**/*.{ts,tsx,js,jsx,json,md}\"", "format": "prettier --write \"packages/**/*.{ts,tsx,js,jsx,json,md}\"",
"publish:commons": "cd packages/commons && npm publish --access public", "publish:commons": "cd packages/dvmcp-commons && npm publish --access public",
"publish:bridge": "cd packages/dvmcp-bridge && npm publish --access public", "publish:bridge": "cd packages/dvmcp-bridge && npm publish --access public",
"publish:discovery": "cd packages/dvmcp-discovery && npm publish --access public", "publish:discovery": "cd packages/dvmcp-discovery && npm publish --access public",
"publish:all": "npm run publish:commons && npm run publish:bridge && npm run publish:discovery" "publish:all": "npm run publish:commons && npm run publish:bridge && npm run publish:discovery"

View File

@@ -1,31 +1,229 @@
#!/usr/bin/env bun import { randomBytes } from 'node:crypto';
import { createInterface } from 'node:readline';
import { writeFileSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path'; import { join, dirname } from 'node:path';
import { existsSync, copyFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { parse, stringify } from 'yaml';
import type { Config } from './src/types.js';
// Ensure we can run from any directory
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const isNpxRun = !__dirname.includes(process.cwd());
if (!isNpxRun) {
process.chdir(__dirname); process.chdir(__dirname);
}
// Check for config file
const configPath = join(process.cwd(), 'config.yml'); const configPath = join(process.cwd(), 'config.yml');
const configExamplePath = join(process.cwd(), 'config.example.yml'); const configExamplePath = join(__dirname, 'config.example.yml');
if (!existsSync(configPath)) { async function setupConfig() {
console.log('Configuration file not found at config.yml'); console.log('🔧 DVMCP Bridge Configuration Setup 🔧');
console.log('You can create one by copying the example:');
console.log('cp config.example.yml config.yml');
// Automatically copy example config if it exists // If config exists, ask user if they want to reconfigure
if (existsSync(configExamplePath)) { if (existsSync(configPath)) {
console.log('Creating config.yml from example...'); const shouldReconfigure = await promptYesNo(
copyFileSync(configExamplePath, configPath); 'Configuration file already exists. Do you want to reconfigure it?'
console.log(
'✅ Created config.yml - please edit this file with your settings!'
); );
if (!shouldReconfigure) {
console.log('Using existing configuration.');
return;
} }
} }
// Run the application // Load example config as template
import './index.js'; if (!existsSync(configExamplePath)) {
console.error('Error: Example configuration file not found!');
process.exit(1);
}
// Read and parse example config
const exampleConfigContent = await Bun.file(configExamplePath).text();
const config: Config = parse(exampleConfigContent);
// Ensure config structure matches example
config.nostr = config.nostr || {};
config.mcp = config.mcp || {};
config.mcp.servers = config.mcp.servers || [];
console.log('\n🔑 Nostr Configuration:');
const useExistingKey = await promptYesNo(
'Do you have an existing Nostr private key?'
);
if (useExistingKey) {
config.nostr.privateKey = await prompt(
'Enter your Nostr private key (nsec or hex):',
config.nostr.privateKey || ''
);
} else {
// Generate a random key
config.nostr.privateKey = Buffer.from(randomBytes(32)).toString('hex');
console.log(`Generated new private key: ${config.nostr.privateKey}`);
}
console.log('\n🔄 Relay Configuration:');
let relayUrls = config.nostr.relayUrls || [];
console.log('Current relays:');
if (relayUrls.length > 0) {
relayUrls.forEach((relay, i) => console.log(` ${i + 1}. ${relay}`));
} else {
console.log(' No relays configured yet.');
}
const addRelays = await promptYesNo('Would you like to add more relays?');
if (addRelays) {
let addingRelays = true;
while (addingRelays) {
const relay = await prompt(
'Enter relay URL (or leave empty to finish):',
''
);
if (relay) {
relayUrls.push(relay);
} else {
addingRelays = false;
}
}
}
config.nostr.relayUrls = relayUrls;
console.log('\n🌐 MCP Service Configuration:');
config.mcp.name = await prompt(
'Service name:',
config.mcp.name || 'DVM MCP Bridge'
);
config.mcp.about = await prompt(
'Service description:',
config.mcp.about || 'MCP-enabled DVM providing AI and computational tools'
);
config.mcp.clientName = await prompt(
'Client name:',
config.mcp.clientName || 'DVM MCP Bridge Client'
);
config.mcp.clientVersion = await prompt(
'Client version:',
config.mcp.clientVersion || '1.0.0'
);
console.log('\n🖥 MCP Servers Configuration:');
console.log('Current configured servers:');
if (config.mcp.servers.length > 0) {
config.mcp.servers.forEach((server, i) => {
console.log(
` ${i + 1}. ${server.name} (${server.command} ${server.args.join(' ')})`
);
});
} else {
console.log(' No servers configured yet.');
}
const configureServers = await promptYesNo(
'Would you like to configure MCP servers?'
);
if (configureServers) {
let configuringServers = true;
while (configuringServers) {
console.log('\nConfiguring a new server:');
const name = await prompt('Server name (or leave empty to finish):', '');
if (!name) {
configuringServers = false;
continue;
}
const command = await prompt('Command to run server:', 'node');
const argsStr = await prompt('Command arguments (space-separated):', '');
const args = argsStr ? argsStr.split(' ') : [];
config.mcp.servers.push({ name, command, args });
}
}
console.log('\n📝 Whitelist Configuration:');
const useWhitelist = await promptYesNo(
'Would you like to configure a public key whitelist?'
);
if (useWhitelist) {
config.whitelist = config.whitelist || {};
config.whitelist.allowedPubkeys = config.whitelist.allowedPubkeys;
console.log('Current allowed public keys:');
if (
config.whitelist.allowedPubkeys &&
config.whitelist.allowedPubkeys.size > 0
) {
config.whitelist.allowedPubkeys.forEach((pubkey, i) =>
console.log(` ${i + 1}. ${pubkey}`)
);
} else {
console.log(' No public keys whitelisted yet.');
}
let addingPubkeys = true;
while (addingPubkeys) {
const pubkey = await prompt(
'Enter public key to whitelist (or leave empty to finish):',
''
);
if (pubkey) {
if (!config.whitelist.allowedPubkeys) {
config.whitelist.allowedPubkeys = new Set<string>();
}
config.whitelist.allowedPubkeys.add(pubkey);
} else {
addingPubkeys = false;
}
}
} else if (config.whitelist) {
// If user doesn't want a whitelist but it exists in config, remove it
config.whitelist.allowedPubkeys = undefined;
}
// Save the config
writeFileSync(configPath, stringify(config));
console.log(`\n✅ Configuration saved to ${configPath}`);
}
async function prompt(question: string, defaultValue = ''): Promise<string> {
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(
`${question}${defaultValue ? ` (${defaultValue})` : ''} `,
(answer) => {
rl.close();
resolve(answer || defaultValue);
}
);
});
}
async function promptYesNo(
question: string,
defaultValue = false
): Promise<boolean> {
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
const defaultIndicator = defaultValue ? 'Y/n' : 'y/N';
return new Promise((resolve) => {
rl.question(`${question} (${defaultIndicator}) `, (answer) => {
rl.close();
if (answer.trim() === '') {
resolve(defaultValue);
} else {
resolve(answer.toLowerCase().startsWith('y'));
}
});
});
}
await setupConfig();
import('./index.js');

View File

@@ -19,6 +19,7 @@
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
"dev": "bun --watch index.ts", "dev": "bun --watch index.ts",
"start": "bun run index.ts", "start": "bun run index.ts",
"setup": "bun run cli.ts",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"lint": "bun run typecheck && bun run format", "lint": "bun run typecheck && bun run format",
"test": "bun test", "test": "bun test",

View File

@@ -2,11 +2,11 @@ import { parse } from 'yaml';
import { join } from 'path'; import { join } from 'path';
import { existsSync, readFileSync } from 'fs'; import { existsSync, readFileSync } from 'fs';
import { HEX_KEYS_REGEX } from '@dvmcp/commons/constants'; import { HEX_KEYS_REGEX } from '@dvmcp/commons/constants';
import type { AppConfig, MCPServerConfig } from './types'; import type { Config, MCPServerConfig } from './types';
const CONFIG_PATH = join(process.cwd(), 'config.yml'); const CONFIG_PATH = join(process.cwd(), 'config.yml');
const TEST_CONFIG: AppConfig = { const TEST_CONFIG: Config = {
nostr: { nostr: {
privateKey: privateKey:
'd4d4d7aae7857054596c4c0976b22a73acac3a10d30bf56db35ee038bbf0dd44', 'd4d4d7aae7857054596c4c0976b22a73acac3a10d30bf56db35ee038bbf0dd44',
@@ -80,7 +80,7 @@ function validateMCPServers(servers: any): MCPServerConfig[] {
}); });
} }
function loadConfig(): AppConfig { function loadConfig(): Config {
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
return TEST_CONFIG; return TEST_CONFIG;
} }
@@ -95,7 +95,7 @@ function loadConfig(): AppConfig {
const configFile = readFileSync(CONFIG_PATH, 'utf8'); const configFile = readFileSync(CONFIG_PATH, 'utf8');
const rawConfig = parse(configFile); const rawConfig = parse(configFile);
const config: AppConfig = { const config: Config = {
nostr: { nostr: {
privateKey: validateRequiredField( privateKey: validateRequiredField(
rawConfig.nostr?.privateKey, rawConfig.nostr?.privateKey,

View File

@@ -15,7 +15,7 @@ export interface WhitelistConfig {
allowedPubkeys: Set<string> | undefined; allowedPubkeys: Set<string> | undefined;
} }
export interface AppConfig { export interface Config {
nostr: NostrConfig; nostr: NostrConfig;
mcp: MCPConfig; mcp: MCPConfig;
whitelist: WhitelistConfig; whitelist: WhitelistConfig;

View File

@@ -1,31 +1,279 @@
#!/usr/bin/env bun import { randomBytes } from 'node:crypto';
import { createInterface } from 'node:readline';
import { writeFileSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path'; import { join, dirname } from 'node:path';
import { existsSync, copyFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { parse, stringify } from 'yaml';
import { HEX_KEYS_REGEX } from '@dvmcp/commons/constants';
import type { Config } from './src/config';
// Ensure we can run from any directory
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const isNpxRun = !__dirname.includes(process.cwd());
if (!isNpxRun) {
process.chdir(__dirname); process.chdir(__dirname);
}
// Check for config file
const configPath = join(process.cwd(), 'config.yml'); const configPath = join(process.cwd(), 'config.yml');
const configExamplePath = join(process.cwd(), 'config.example.yml'); const configExamplePath = join(__dirname, 'config.example.yml');
if (!existsSync(configPath)) { async function setupConfig() {
console.log('Configuration file not found at config.yml'); console.log('🔧 DVMCP Discovery Configuration Setup 🔧');
console.log('You can create one by copying the example:');
console.log('cp config.example.yml config.yml');
// Automatically copy example config if it exists // If config exists, ask user if they want to reconfigure
if (existsSync(configExamplePath)) { if (existsSync(configPath)) {
console.log('Creating config.yml from example...'); const shouldReconfigure = await promptYesNo(
copyFileSync(configExamplePath, configPath); 'Configuration file already exists. Do you want to reconfigure it?'
console.log( );
'✅ Created config.yml - please edit this file with your settings!' if (!shouldReconfigure) {
console.log('Using existing configuration.');
return;
}
}
// Load example config as template
if (!existsSync(configExamplePath)) {
console.error('Error: Example configuration file not found!');
process.exit(1);
}
// Read and parse example config
const exampleConfigContent = await Bun.file(configExamplePath).text();
let config: Config = parse(exampleConfigContent);
// If existing config, load it instead of the example
if (existsSync(configPath)) {
const existingConfigContent = await Bun.file(configPath).text();
try {
config = parse(existingConfigContent);
} catch (error) {
console.warn(
'Warning: Could not parse existing config. Using example config as base.'
); );
} }
} }
// Run the application config.nostr = config.nostr || {};
import './index.js'; config.mcp = config.mcp || {};
config.whitelist = config.whitelist || {};
console.log('\n🔑 Nostr Configuration:');
// Check if private key exists and is valid
const hasValidKey =
config.nostr.privateKey &&
HEX_KEYS_REGEX.test(config.nostr.privateKey) &&
config.nostr.privateKey !== 'your_private_key_here';
const useExistingKey = hasValidKey
? await promptYesNo('Use existing private key?', true)
: false;
if (!useExistingKey) {
const useCustomKey = await promptYesNo(
'Would you like to enter a custom private key?'
);
if (useCustomKey) {
let validKey = false;
while (!validKey) {
const defaultKey = hasValidKey ? config.nostr.privateKey : '';
config.nostr.privateKey = await prompt(
'Enter your Nostr private key (hex format):',
defaultKey
);
if (HEX_KEYS_REGEX.test(config.nostr.privateKey)) {
validKey = true;
} else {
console.log(
'❌ Invalid key format. Please enter a 32-byte hex string.'
);
}
}
} else {
// Generate a random key
config.nostr.privateKey = Buffer.from(randomBytes(32)).toString('hex');
console.log(`Generated new private key: ${config.nostr.privateKey}`);
}
}
console.log('\n🔄 Relay Configuration:');
let relayUrls = config.nostr.relayUrls || [];
console.log('Current relays:');
if (relayUrls.length > 0) {
relayUrls.forEach((relay, i) => console.log(` ${i + 1}. ${relay}`));
} else {
console.log(' No relays configured yet.');
}
const addRelays = await promptYesNo('Would you like to add more relays?');
if (addRelays) {
let addingRelays = true;
while (addingRelays) {
const relay = await prompt(
'Enter relay URL (or leave empty to finish):',
''
);
if (relay) {
try {
const trimmedUrl = relay.trim();
new URL(trimmedUrl);
if (
!trimmedUrl.startsWith('ws://') &&
!trimmedUrl.startsWith('wss://')
) {
console.log('❌ Relay URL must start with ws:// or wss://');
continue;
}
relayUrls.push(trimmedUrl);
} catch (error) {
console.log(`❌ Invalid relay URL: ${relay}`);
}
} else {
addingRelays = false;
}
}
}
const removeRelays =
relayUrls.length > 0 &&
(await promptYesNo('Would you like to remove any relays?'));
if (removeRelays) {
let removingRelays = true;
while (removingRelays && relayUrls.length > 0) {
console.log('Current relays:');
relayUrls.forEach((relay, i) => console.log(` ${i + 1}. ${relay}`));
const indexStr = await prompt(
'Enter number of relay to remove (or leave empty to finish):',
''
);
if (!indexStr) {
removingRelays = false;
continue;
}
const index = parseInt(indexStr, 10) - 1;
if (isNaN(index) || index < 0 || index >= relayUrls.length) {
console.log('Invalid relay number. Please try again.');
continue;
}
relayUrls.splice(index, 1);
console.log('Relay removed.');
if (relayUrls.length === 0) {
console.log('No relays remaining.');
break;
}
}
}
config.nostr.relayUrls = relayUrls;
console.log('\n🌐 MCP Service Configuration:');
config.mcp.name = await prompt(
'Service name:',
config.mcp.name || 'DVMCP Discovery'
);
config.mcp.version = await prompt(
'Service version:',
config.mcp.version || '1.0.0'
);
config.mcp.about = await prompt(
'Service description:',
config.mcp.about ||
'DVMCP Discovery Server for aggregating MCP tools from DVMs'
);
console.log('\n📝 DVM Whitelist Configuration:');
const useWhitelist = await promptYesNo(
'Would you like to configure a DVM whitelist?'
);
if (useWhitelist) {
config.whitelist = config.whitelist || {};
config.whitelist.allowedDVMs =
config.whitelist.allowedDVMs || new Set<string>();
console.log('Current whitelisted DVMs:');
if (config.whitelist.allowedDVMs && config.whitelist.allowedDVMs.size > 0) {
Array.from(config.whitelist.allowedDVMs).forEach((pubkey, i) =>
console.log(` ${i + 1}. ${pubkey}`)
);
} else {
console.log(' No DVMs whitelisted yet.');
}
let addingDVMs = true;
while (addingDVMs) {
const pubkey = await prompt(
'Enter DVM public key to whitelist (or leave empty to finish):',
''
);
if (pubkey) {
if (HEX_KEYS_REGEX.test(pubkey.trim())) {
config.whitelist.allowedDVMs.add(pubkey.trim());
} else {
console.log(
'❌ Invalid public key format. Please enter a 32-byte hex string.'
);
}
} else {
addingDVMs = false;
}
}
} else if (config.whitelist?.allowedDVMs) {
// If user doesn't want a whitelist but it exists in config, ask if they want to clear it
const clearWhitelist = await promptYesNo(
'Do you want to clear the existing whitelist?'
);
if (clearWhitelist) {
config.whitelist.allowedDVMs = undefined;
console.log('Whitelist cleared.');
}
}
// Save the config
writeFileSync(configPath, stringify(config));
console.log(`\n✅ Configuration saved to ${configPath}`);
}
async function prompt(question: string, defaultValue = ''): Promise<string> {
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(
`${question}${defaultValue ? ` (${defaultValue})` : ''} `,
(answer) => {
rl.close();
resolve(answer || defaultValue);
}
);
});
}
async function promptYesNo(
question: string,
defaultValue = false
): Promise<boolean> {
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
const defaultIndicator = defaultValue ? 'Y/n' : 'y/N';
return new Promise((resolve) => {
rl.question(`${question} (${defaultIndicator}) `, (answer) => {
rl.close();
if (answer.trim() === '') {
resolve(defaultValue);
} else {
resolve(answer.toLowerCase().startsWith('y'));
}
});
});
}
await setupConfig();
import('./index.js');

View File

@@ -3,8 +3,8 @@ nostr:
privateKey: "your_private_key_here" privateKey: "your_private_key_here"
# List of relays to connect to # List of relays to connect to
relayUrls: relayUrls:
- "wss://relay.damus.io" - "wss://relay1.com"
- "wss://relay.nostr.band" - "wss://relay2.net"
mcp: mcp:
# Server name # Server name

View File

@@ -18,6 +18,7 @@
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
"dev": "bun --watch index.ts", "dev": "bun --watch index.ts",
"start": "bun run index.ts", "start": "bun run index.ts",
"setup": "bun run cli.ts",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"lint": "bun run typecheck && bun run format", "lint": "bun run typecheck && bun run format",
"test": "bun test", "test": "bun test",