mirror of
https://github.com/aljazceru/dvmcp.git
synced 2025-12-17 13:24:24 +01:00
refactor: improved clis, setup command, readme
This commit is contained in:
82
README.md
82
README.md
@@ -6,36 +6,76 @@ A monorepo containing packages that bridge Model Context Protocol (MCP) servers
|
||||
|
||||
This monorepo contains the following packages:
|
||||
|
||||
### [@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.
|
||||
### [@dvmcp/bridge](./packages/dvmcp-bridge)
|
||||
The bridge implementation that connects MCP servers to Nostr's DVM ecosystem. Handles tool announcement, execution, and status updates.
|
||||
|
||||
### [@dvmcp-discovery](./packages/dvmcp-discovery)
|
||||
A MCP server, discovery service that aggregates MCP tools from DVMs, and make their tools available
|
||||
### [@dvmcp/discovery](./packages/dvmcp-discovery)
|
||||
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.
|
||||
|
||||
## 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
|
||||
bun install
|
||||
# Run the bridge
|
||||
npx @dvmcp/bridge
|
||||
|
||||
# Run the discovery service
|
||||
npx @dvmcp/discovery
|
||||
```
|
||||
|
||||
2. Set up configurations:
|
||||
```bash
|
||||
# For the bridge
|
||||
cp packages/dvmcp-bridge/config.example.yml packages/dvmcp-bridge/config.yml
|
||||
The interactive CLI will guide you through configuration setup on first run.
|
||||
|
||||
# For the discovery service
|
||||
cp packages/dvmcp-discovery/config.example.yml packages/dvmcp-discovery/config.yml
|
||||
### Global Installation
|
||||
|
||||
```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
|
||||
|
||||
For contributors to this repository:
|
||||
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://github.com/gzuuus/dvmcp.git
|
||||
cd dvmcp
|
||||
|
||||
# Install dependencies
|
||||
bun install
|
||||
|
||||
# Start the bridge in development mode
|
||||
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
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
```bash
|
||||
# Start the bridge
|
||||
bun run start --cwd packages/dvmcp-bridge
|
||||
|
||||
# Start the discovery service
|
||||
bun run start --cwd packages/dvmcp-discovery
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [DVMCP Specification](./docs/dvmcp-spec.md)
|
||||
- [Bridge Package](./packages/dvmcp-bridge/README.md)
|
||||
- [Discovery Package](./packages/dvmcp-discovery/README.md)
|
||||
- [Commons Package](./packages/commons/README.md)
|
||||
- [Commons Package](./packages/dvmcp-commons/README.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"scripts": {
|
||||
"start:mcp-dvm": "cd packages/mcp-dvm-bridge && bun start",
|
||||
"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:discovery": "cd packages/dvmcp-discovery && npm publish --access public",
|
||||
"publish:all": "npm run publish:commons && npm run publish:bridge && npm run publish:discovery"
|
||||
|
||||
@@ -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 { existsSync, copyFileSync } from 'node:fs';
|
||||
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 __dirname = dirname(__filename);
|
||||
process.chdir(__dirname);
|
||||
|
||||
// Check for config file
|
||||
const configPath = join(process.cwd(), 'config.yml');
|
||||
const configExamplePath = join(process.cwd(), 'config.example.yml');
|
||||
const isNpxRun = !__dirname.includes(process.cwd());
|
||||
|
||||
if (!existsSync(configPath)) {
|
||||
console.log('Configuration file not found at config.yml');
|
||||
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 (existsSync(configExamplePath)) {
|
||||
console.log('Creating config.yml from example...');
|
||||
copyFileSync(configExamplePath, configPath);
|
||||
console.log(
|
||||
'✅ Created config.yml - please edit this file with your settings!'
|
||||
);
|
||||
}
|
||||
if (!isNpxRun) {
|
||||
process.chdir(__dirname);
|
||||
}
|
||||
|
||||
// Run the application
|
||||
import './index.js';
|
||||
const configPath = join(process.cwd(), 'config.yml');
|
||||
const configExamplePath = join(__dirname, 'config.example.yml');
|
||||
|
||||
async function setupConfig() {
|
||||
console.log('🔧 DVMCP Bridge Configuration Setup 🔧');
|
||||
|
||||
// If config exists, ask user if they want to reconfigure
|
||||
if (existsSync(configPath)) {
|
||||
const shouldReconfigure = await promptYesNo(
|
||||
'Configuration file already exists. Do you want to reconfigure it?'
|
||||
);
|
||||
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();
|
||||
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');
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
|
||||
"dev": "bun --watch index.ts",
|
||||
"start": "bun run index.ts",
|
||||
"setup": "bun run cli.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "bun run typecheck && bun run format",
|
||||
"test": "bun test",
|
||||
|
||||
@@ -2,11 +2,11 @@ import { parse } from 'yaml';
|
||||
import { join } from 'path';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
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 TEST_CONFIG: AppConfig = {
|
||||
const TEST_CONFIG: Config = {
|
||||
nostr: {
|
||||
privateKey:
|
||||
'd4d4d7aae7857054596c4c0976b22a73acac3a10d30bf56db35ee038bbf0dd44',
|
||||
@@ -80,7 +80,7 @@ function validateMCPServers(servers: any): MCPServerConfig[] {
|
||||
});
|
||||
}
|
||||
|
||||
function loadConfig(): AppConfig {
|
||||
function loadConfig(): Config {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
return TEST_CONFIG;
|
||||
}
|
||||
@@ -95,7 +95,7 @@ function loadConfig(): AppConfig {
|
||||
const configFile = readFileSync(CONFIG_PATH, 'utf8');
|
||||
const rawConfig = parse(configFile);
|
||||
|
||||
const config: AppConfig = {
|
||||
const config: Config = {
|
||||
nostr: {
|
||||
privateKey: validateRequiredField(
|
||||
rawConfig.nostr?.privateKey,
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface WhitelistConfig {
|
||||
allowedPubkeys: Set<string> | undefined;
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
export interface Config {
|
||||
nostr: NostrConfig;
|
||||
mcp: MCPConfig;
|
||||
whitelist: WhitelistConfig;
|
||||
|
||||
@@ -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 { existsSync, copyFileSync } from 'node:fs';
|
||||
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 __dirname = dirname(__filename);
|
||||
process.chdir(__dirname);
|
||||
|
||||
// Check for config file
|
||||
const configPath = join(process.cwd(), 'config.yml');
|
||||
const configExamplePath = join(process.cwd(), 'config.example.yml');
|
||||
|
||||
if (!existsSync(configPath)) {
|
||||
console.log('Configuration file not found at config.yml');
|
||||
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 (existsSync(configExamplePath)) {
|
||||
console.log('Creating config.yml from example...');
|
||||
copyFileSync(configExamplePath, configPath);
|
||||
console.log(
|
||||
'✅ Created config.yml - please edit this file with your settings!'
|
||||
);
|
||||
}
|
||||
const isNpxRun = !__dirname.includes(process.cwd());
|
||||
if (!isNpxRun) {
|
||||
process.chdir(__dirname);
|
||||
}
|
||||
|
||||
// Run the application
|
||||
import './index.js';
|
||||
const configPath = join(process.cwd(), 'config.yml');
|
||||
const configExamplePath = join(__dirname, 'config.example.yml');
|
||||
|
||||
async function setupConfig() {
|
||||
console.log('🔧 DVMCP Discovery Configuration Setup 🔧');
|
||||
|
||||
// If config exists, ask user if they want to reconfigure
|
||||
if (existsSync(configPath)) {
|
||||
const shouldReconfigure = await promptYesNo(
|
||||
'Configuration file already exists. Do you want to reconfigure it?'
|
||||
);
|
||||
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.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
config.nostr = config.nostr || {};
|
||||
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');
|
||||
|
||||
@@ -3,8 +3,8 @@ nostr:
|
||||
privateKey: "your_private_key_here"
|
||||
# List of relays to connect to
|
||||
relayUrls:
|
||||
- "wss://relay.damus.io"
|
||||
- "wss://relay.nostr.band"
|
||||
- "wss://relay1.com"
|
||||
- "wss://relay2.net"
|
||||
|
||||
mcp:
|
||||
# Server name
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
|
||||
"dev": "bun --watch index.ts",
|
||||
"start": "bun run index.ts",
|
||||
"setup": "bun run cli.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "bun run typecheck && bun run format",
|
||||
"test": "bun test",
|
||||
|
||||
Reference in New Issue
Block a user