mirror of
https://github.com/aljazceru/dvmcp.git
synced 2025-12-17 05:14:24 +01:00
refactor: clis, config-generator, readmes (#4)
This commit is contained in:
45
README.md
45
README.md
@@ -1,104 +1,71 @@
|
||||
# DVMCP: Data Vending Machine Context Protocol
|
||||
|
||||
A monorepo containing packages that bridge Model Context Protocol (MCP) servers with Nostr's Data Vending Machine (DVM) ecosystem, enabling AI and computational tools to be discovered and utilized via Nostr's decentralized network.
|
||||
|
||||
## Packages
|
||||
|
||||
This monorepo contains the following packages:
|
||||
|
||||
### [@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 makes their tools available.
|
||||
|
||||
### [@dvmcp/commons](./packages/dvmcp-commons)
|
||||
Shared utilities and components used across DVMCP packages.
|
||||
|
||||
## Installation & Usage
|
||||
**Prerequisite:** Ensure you have [Bun](https://bun.sh/) installed.
|
||||
|
||||
### Quick Start with NPX (No Installation)
|
||||
|
||||
You can run the packages directly using `npx` without installing them:
|
||||
|
||||
```bash
|
||||
# Run the bridge
|
||||
npx @dvmcp/bridge
|
||||
|
||||
# Run the discovery service
|
||||
npx @dvmcp/discovery
|
||||
```
|
||||
|
||||
The interactive CLI will guide you through configuration setup on first run.
|
||||
|
||||
### Global Installation
|
||||
|
||||
```bash
|
||||
# Install the packages globally
|
||||
npm install -g @dvmcp/bridge @dvmcp/discovery
|
||||
|
||||
# Run the commands
|
||||
dvmcp-bridge
|
||||
dvmcp-discovery
|
||||
```
|
||||
|
||||
## 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
|
||||
- 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
|
||||
|
||||
- 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
|
||||
|
||||
# Start the discovery service in development mode
|
||||
bun run dev --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/dvmcp-commons/README.md)
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit pull requests or create issues.
|
||||
|
||||
## License
|
||||
|
||||
[MIT License](LICENSE)
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [Model Context Protocol](https://modelcontextprotocol.io)
|
||||
- [Nostr Protocol](https://github.com/nostr-protocol/nips)
|
||||
@@ -12,41 +12,30 @@ A bridge implementation that connects Model Context Protocol (MCP) servers to No
|
||||
|
||||
## Configuration
|
||||
|
||||
Create your configuration file by copying the example:
|
||||
When the package is run for the first time, it will detect if the `config.yml` file exists, and if not, it will launch a configuration wizard to help you create the configuration file. You can also create your configuration file by copying `config.example.yml` and changing the values of the fields
|
||||
|
||||
```bash
|
||||
cp config.example.yml config.yml
|
||||
```
|
||||
|
||||
Example configuration:
|
||||
|
||||
```yaml
|
||||
nostr:
|
||||
privateKey: 'your_private_key_here'
|
||||
relayUrls:
|
||||
- 'wss://relay1.com'
|
||||
- 'wss://relay2.net'
|
||||
|
||||
mcp:
|
||||
name: 'DVM MCP Bridge'
|
||||
about: 'MCP-enabled DVM providing AI and computational tools'
|
||||
clientName: 'DVM MCP Bridge Client'
|
||||
clientVersion: '1.0.0'
|
||||
servers:
|
||||
- name: 'server1'
|
||||
command: 'node'
|
||||
args: ['run', 'src/external-mcp-server1.ts']
|
||||
nano config.yml
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Development mode:
|
||||
**Prerequisite:** Ensure you have [Bun](https://bun.sh/) installed.
|
||||
|
||||
You can run this package directly using `npx`:
|
||||
|
||||
```bash
|
||||
npx @dvmcp/bridge
|
||||
```
|
||||
|
||||
Alternatively, for development:
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Production mode:
|
||||
For production:
|
||||
|
||||
```bash
|
||||
bun run start
|
||||
|
||||
@@ -1,230 +1,127 @@
|
||||
#!/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 { fileURLToPath } from 'node:url';
|
||||
import { parse, stringify } from 'yaml';
|
||||
import type { Config } from './src/types.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const isNpxRun = !__dirname.includes(process.cwd());
|
||||
|
||||
if (!isNpxRun) {
|
||||
process.chdir(__dirname);
|
||||
}
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
ConfigGenerator,
|
||||
type FieldConfig,
|
||||
} from '@dvmcp/commons/config-generator';
|
||||
import {
|
||||
generateHexKey,
|
||||
validateHexKey,
|
||||
validateRelayUrl,
|
||||
CONFIG_EMOJIS,
|
||||
} from '@dvmcp/commons/config-generator';
|
||||
import { argv } from 'process';
|
||||
import type { Config } from './src/types';
|
||||
|
||||
const configPath = join(process.cwd(), 'config.yml');
|
||||
const configExamplePath = join(__dirname, 'config.example.yml');
|
||||
|
||||
async function setupConfig() {
|
||||
console.log('🔧 DVMCP Bridge Configuration Setup 🔧');
|
||||
const configFields: Record<string, FieldConfig> = {
|
||||
nostr: {
|
||||
type: 'nested',
|
||||
description: 'Nostr Configuration',
|
||||
emoji: CONFIG_EMOJIS.NOSTR,
|
||||
fields: {
|
||||
privateKey: {
|
||||
type: 'hex',
|
||||
description: 'Private key',
|
||||
generator: generateHexKey,
|
||||
validation: validateHexKey,
|
||||
required: true,
|
||||
},
|
||||
relayUrls: {
|
||||
type: 'array',
|
||||
description: 'Relay URLs',
|
||||
validation: validateRelayUrl,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
mcp: {
|
||||
type: 'nested',
|
||||
description: 'Service Configuration',
|
||||
emoji: CONFIG_EMOJIS.SERVICE,
|
||||
fields: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Service name',
|
||||
default: 'DVM MCP Bridge',
|
||||
},
|
||||
about: {
|
||||
type: 'string',
|
||||
description: 'Service description',
|
||||
default: 'MCP-enabled DVM providing AI and computational tools',
|
||||
},
|
||||
clientName: {
|
||||
type: 'string',
|
||||
description: 'Client name',
|
||||
default: 'DVM MCP Bridge Client',
|
||||
required: true,
|
||||
},
|
||||
clientVersion: {
|
||||
type: 'string',
|
||||
description: 'Client version',
|
||||
default: '1.0.0',
|
||||
required: true,
|
||||
},
|
||||
servers: {
|
||||
type: 'object-array',
|
||||
description: 'Server Configuration',
|
||||
emoji: CONFIG_EMOJIS.SERVER,
|
||||
required: true,
|
||||
fields: {
|
||||
command: {
|
||||
type: 'string',
|
||||
description: 'Command',
|
||||
},
|
||||
args: {
|
||||
type: 'array',
|
||||
description: 'Arguments',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
whitelist: {
|
||||
type: 'nested',
|
||||
description: 'Whitelist Configuration',
|
||||
emoji: CONFIG_EMOJIS.WHITELIST,
|
||||
fields: {
|
||||
allowedPubkeys: {
|
||||
type: 'set',
|
||||
description: 'Allowed public keys',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 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) => {
|
||||
const configure = async () => {
|
||||
console.log(
|
||||
` ${i + 1}. ${server.name} (${server.command} ${server.args.join(' ')})`
|
||||
`${CONFIG_EMOJIS.SETUP} DVMCP Bridge Configuration Setup ${CONFIG_EMOJIS.SETUP}`
|
||||
);
|
||||
});
|
||||
} else {
|
||||
console.log(' No servers configured yet.');
|
||||
const generator = new ConfigGenerator<Config>(configPath, configFields);
|
||||
await generator.generate();
|
||||
};
|
||||
|
||||
const runApp = async () => {
|
||||
const main = await import('./index.js');
|
||||
console.log(`${CONFIG_EMOJIS.INFO} Running main application...`);
|
||||
await main.default();
|
||||
};
|
||||
|
||||
const cliMain = async () => {
|
||||
if (argv.includes('--configure')) {
|
||||
await configure();
|
||||
}
|
||||
|
||||
const configureServers = await promptYesNo(
|
||||
'Would you like to configure MCP servers?'
|
||||
if (!existsSync(configPath)) {
|
||||
console.log(
|
||||
`${CONFIG_EMOJIS.INFO} No configuration file found. Starting setup...`
|
||||
);
|
||||
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 });
|
||||
}
|
||||
await configure();
|
||||
}
|
||||
|
||||
console.log('\n📝 Whitelist Configuration:');
|
||||
const useWhitelist = await promptYesNo(
|
||||
'Would you like to configure a public key whitelist?'
|
||||
);
|
||||
await runApp();
|
||||
};
|
||||
|
||||
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');
|
||||
cliMain().catch(console.error);
|
||||
|
||||
@@ -2,7 +2,6 @@ import { DVMBridge } from './src/dvm-bridge';
|
||||
|
||||
async function main() {
|
||||
const bridge = new DVMBridge();
|
||||
|
||||
const shutdown = async () => {
|
||||
console.log('Shutting down...');
|
||||
try {
|
||||
@@ -13,10 +12,8 @@ async function main() {
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
|
||||
try {
|
||||
await bridge.start();
|
||||
} catch (error) {
|
||||
@@ -25,4 +22,4 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
export default main;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@dvmcp/bridge",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.6",
|
||||
"description": "Bridge connecting MCP servers to Nostr's DVM ecosystem",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
@@ -18,8 +18,7 @@
|
||||
"scripts": {
|
||||
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
|
||||
"dev": "bun --watch index.ts",
|
||||
"start": "bun run index.ts",
|
||||
"setup": "bun run cli.ts",
|
||||
"start": "bun run cli.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "bun run typecheck && bun run format",
|
||||
"test": "bun test",
|
||||
@@ -36,7 +35,7 @@
|
||||
"dotenv": "^16.4.7",
|
||||
"nostr-tools": "^2.10.4",
|
||||
"yaml": "^2.7.0",
|
||||
"@dvmcp/commons": "^0.1.0"
|
||||
"@dvmcp/commons": "^0.1.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
# commons
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.2.2. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
||||
Common package for common functions and constants
|
||||
|
||||
307
packages/dvmcp-commons/config-generator.ts
Normal file
307
packages/dvmcp-commons/config-generator.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
import { createInterface } from 'node:readline';
|
||||
import { stringify } from 'yaml';
|
||||
import { writeFileSync, existsSync } from 'node:fs';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { HEX_KEYS_REGEX } from './constants';
|
||||
|
||||
export const CONFIG_EMOJIS = {
|
||||
NOSTR: '🔑',
|
||||
RELAY: '🔄',
|
||||
SERVICE: '🤖',
|
||||
SERVER: '🖥️',
|
||||
WHITELIST: '🔒',
|
||||
SETUP: '⚙️',
|
||||
SUCCESS: '✅',
|
||||
INFO: 'ℹ️',
|
||||
PROMPT: '❯',
|
||||
} as const;
|
||||
|
||||
export type FieldType =
|
||||
| 'string'
|
||||
| 'array'
|
||||
| 'boolean'
|
||||
| 'hex'
|
||||
| 'url'
|
||||
| 'nested'
|
||||
| 'object-array'
|
||||
| 'set';
|
||||
|
||||
interface ArrayItem {
|
||||
name: string;
|
||||
command: string;
|
||||
args: string[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface FieldConfig {
|
||||
type: FieldType;
|
||||
description?: string;
|
||||
default?: any;
|
||||
validation?: (value: any) => boolean;
|
||||
generator?: () => any;
|
||||
required?: boolean;
|
||||
itemType?: FieldConfig;
|
||||
fields?: Record<string, FieldConfig>;
|
||||
comment?: string;
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
export class ConfigGenerator<T extends Record<string, any>> {
|
||||
constructor(
|
||||
private configPath: string,
|
||||
private fields: Record<string, FieldConfig>
|
||||
) {}
|
||||
|
||||
private async prompt(question: string, defaultValue = ''): Promise<string> {
|
||||
const rl = createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const answer = await new Promise<string>((resolve) => {
|
||||
rl.question(
|
||||
`${question}${defaultValue ? ` (${defaultValue})` : ''} `,
|
||||
(answer) => resolve(answer || defaultValue)
|
||||
);
|
||||
});
|
||||
|
||||
rl.close();
|
||||
return answer;
|
||||
}
|
||||
|
||||
private async promptYesNo(
|
||||
question: string,
|
||||
defaultValue = false
|
||||
): Promise<boolean> {
|
||||
const answer = await this.prompt(
|
||||
`${question} (${defaultValue ? 'Y/n' : 'y/N'})`
|
||||
);
|
||||
return answer.trim() === ''
|
||||
? defaultValue
|
||||
: answer.toLowerCase().startsWith('y');
|
||||
}
|
||||
|
||||
private async handleObjectArrayItem(
|
||||
fields: Record<string, FieldConfig>
|
||||
): Promise<Record<string, any> | null> {
|
||||
const item: Record<string, any> = { name: '' };
|
||||
|
||||
const name = await this.prompt(
|
||||
`${CONFIG_EMOJIS.SERVER} Name (empty to finish):`
|
||||
);
|
||||
if (!name) return null;
|
||||
|
||||
item.name = name;
|
||||
|
||||
for (const [fieldName, fieldConfig] of Object.entries(fields)) {
|
||||
if (fieldName === 'name') continue;
|
||||
|
||||
const value = await this.handleField(fieldName, fieldConfig, false);
|
||||
if (value === null) return null;
|
||||
item[fieldName] = value;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private async handleField(
|
||||
fieldName: string,
|
||||
config: FieldConfig,
|
||||
showFieldName = true,
|
||||
currentValue?: any
|
||||
): Promise<any> {
|
||||
const emoji = config.emoji || CONFIG_EMOJIS.PROMPT;
|
||||
|
||||
if (config.type === 'nested') {
|
||||
console.log(`\n${emoji} ${config.description || fieldName}`);
|
||||
}
|
||||
|
||||
const handlers: Record<FieldType, () => Promise<any>> = {
|
||||
string: async () =>
|
||||
this.promptWithValidation(
|
||||
`${emoji} ${config.description || (showFieldName ? fieldName : '')}:`,
|
||||
config,
|
||||
currentValue || config.default
|
||||
),
|
||||
|
||||
hex: async () => {
|
||||
if (
|
||||
config.generator &&
|
||||
(await this.promptYesNo(`${emoji} Generate new ${fieldName}?`))
|
||||
) {
|
||||
const value = config.generator();
|
||||
console.log(`${CONFIG_EMOJIS.PROMPT} ${value}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
const existingValue = currentValue || config.default;
|
||||
existingValue &&
|
||||
console.log(`${CONFIG_EMOJIS.PROMPT} ${existingValue}`);
|
||||
return this.promptWithValidation(
|
||||
`${emoji} Enter ${fieldName}:`,
|
||||
config,
|
||||
existingValue
|
||||
);
|
||||
},
|
||||
|
||||
array: async () => {
|
||||
const array: string[] = currentValue || [];
|
||||
if (array.length > 0) {
|
||||
console.log('\nCurrent items:');
|
||||
array.forEach((item: string, index: number) => {
|
||||
console.log(`${CONFIG_EMOJIS.INFO} ${index + 1}. ${item}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (await this.promptYesNo(`${emoji} Add ${fieldName}?`, true)) {
|
||||
while (true) {
|
||||
const item = await this.promptWithValidation(
|
||||
`${emoji} Value (empty to finish):`,
|
||||
config,
|
||||
'',
|
||||
true
|
||||
);
|
||||
if (!item) break;
|
||||
array.push(item);
|
||||
}
|
||||
}
|
||||
return array;
|
||||
},
|
||||
|
||||
set: async () => {
|
||||
const items: string[] = Array.from(currentValue || []);
|
||||
if (await this.promptYesNo('Would you like to add items?', false)) {
|
||||
while (true) {
|
||||
const item = await this.promptWithValidation(
|
||||
'Enter item (empty to finish):',
|
||||
config,
|
||||
'',
|
||||
true
|
||||
);
|
||||
if (!item) break;
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
return new Set(items);
|
||||
},
|
||||
|
||||
'object-array': async () => {
|
||||
const array: ArrayItem[] = currentValue || [];
|
||||
if (array.length > 0) {
|
||||
console.log('\nCurrent servers:');
|
||||
array.forEach((item: ArrayItem, index: number) => {
|
||||
console.log(
|
||||
`${CONFIG_EMOJIS.INFO} ${index + 1}. ${item.name} (${item.command} ${item.args.join(' ')})`
|
||||
);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (
|
||||
(await this.promptYesNo(`${emoji} Add new ${fieldName}?`, true)) &&
|
||||
config.fields
|
||||
) {
|
||||
while (true) {
|
||||
const item = await this.handleObjectArrayItem(config.fields);
|
||||
if (!item) break;
|
||||
array.push(item as ArrayItem);
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
return array;
|
||||
},
|
||||
|
||||
nested: async () => {
|
||||
const nestedObj: Record<string, any> = {};
|
||||
if (config.fields) {
|
||||
for (const [key, fieldConfig] of Object.entries(config.fields)) {
|
||||
nestedObj[key] = await this.handleField(
|
||||
key,
|
||||
fieldConfig,
|
||||
true,
|
||||
currentValue?.[key]
|
||||
);
|
||||
}
|
||||
}
|
||||
return nestedObj;
|
||||
},
|
||||
|
||||
boolean: async () => false,
|
||||
url: async () =>
|
||||
this.promptWithValidation(
|
||||
`${emoji} Enter URL:`,
|
||||
config,
|
||||
currentValue || config.default
|
||||
),
|
||||
};
|
||||
|
||||
return handlers[config.type]();
|
||||
}
|
||||
|
||||
private async validateInput(
|
||||
value: any,
|
||||
config: FieldConfig,
|
||||
allowEmpty = true
|
||||
): Promise<boolean> {
|
||||
if (allowEmpty && value === '') return true;
|
||||
|
||||
if (!config.validation) return true;
|
||||
|
||||
const isValid = config.validation(value);
|
||||
if (!isValid) {
|
||||
console.log('❌ Invalid input. Please try again.');
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
|
||||
private async promptWithValidation(
|
||||
question: string,
|
||||
config: FieldConfig,
|
||||
defaultValue = '',
|
||||
allowEmpty = true
|
||||
): Promise<string> {
|
||||
while (true) {
|
||||
const value = await this.prompt(question, defaultValue);
|
||||
if (!value && !config.required && allowEmpty) return value;
|
||||
if (await this.validateInput(value, config, allowEmpty)) return value;
|
||||
}
|
||||
}
|
||||
|
||||
async generate(): Promise<T> {
|
||||
console.log(`\n${CONFIG_EMOJIS.SETUP} Configuration Setup\n`);
|
||||
|
||||
if (existsSync(this.configPath)) {
|
||||
const reconfigure = await this.promptYesNo(
|
||||
`${CONFIG_EMOJIS.PROMPT} Configuration exists. Reconfigure?`,
|
||||
true
|
||||
);
|
||||
if (!reconfigure) process.exit(0);
|
||||
}
|
||||
|
||||
const config: Record<string, any> = {};
|
||||
for (const [fieldName, fieldConfig] of Object.entries(this.fields)) {
|
||||
config[fieldName] = await this.handleField(fieldName, fieldConfig);
|
||||
}
|
||||
|
||||
writeFileSync(this.configPath, stringify(config));
|
||||
console.log(`\n${CONFIG_EMOJIS.SUCCESS} Configuration saved successfully!`);
|
||||
|
||||
return config as T;
|
||||
}
|
||||
}
|
||||
|
||||
export const generateHexKey = () =>
|
||||
Buffer.from(randomBytes(32)).toString('hex');
|
||||
|
||||
export const validateHexKey = (value: string) => HEX_KEYS_REGEX.test(value);
|
||||
|
||||
export const validateRelayUrl = (url: string) => {
|
||||
try {
|
||||
const trimmedUrl = url.trim();
|
||||
new URL(trimmedUrl);
|
||||
return trimmedUrl.startsWith('ws://') || trimmedUrl.startsWith('wss://');
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@dvmcp/commons",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Shared utilities for DVMCP packages",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -11,41 +11,30 @@ A MCP server implementation that aggregates tools from DVMs across the Nostr net
|
||||
|
||||
## Configuration
|
||||
|
||||
Create your configuration file by copying the example:
|
||||
When the package is run for the first time, it will detect if the `config.yml` file exists, and if not, it will launch a configuration wizard to help you create the configuration file. You can also create your configuration file by copying `config.example.yml` and changing the values of the fields
|
||||
|
||||
```bash
|
||||
cp config.example.yml config.yml
|
||||
```
|
||||
|
||||
Example configuration:
|
||||
|
||||
```yaml
|
||||
nostr:
|
||||
privateKey: 'your_private_key_here'
|
||||
relayUrls:
|
||||
- 'wss://relay.damus.io'
|
||||
- 'wss://relay.nostr.band'
|
||||
|
||||
mcp:
|
||||
name: 'DVMCP Discovery'
|
||||
version: '1.0.0'
|
||||
about: 'DVMCP Discovery Server for aggregating MCP tools from DVMs'
|
||||
# Optional: whitelist specific DVMs
|
||||
# whitelist:
|
||||
# allowedDVMs:
|
||||
# - 'pubkey1'
|
||||
# - 'pubkey2'
|
||||
nano config.yml
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Development mode:
|
||||
**Prerequisite:** Ensure you have [Bun](https://bun.sh/) installed.
|
||||
|
||||
You can run this package directly using `npx`:
|
||||
|
||||
```bash
|
||||
npx @dvmcp/discovery
|
||||
```
|
||||
|
||||
Alternatively, for development:
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Production mode:
|
||||
For production:
|
||||
|
||||
```bash
|
||||
bun run start
|
||||
|
||||
@@ -1,280 +1,104 @@
|
||||
#!/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 { fileURLToPath } from 'node:url';
|
||||
import { parse, stringify } from 'yaml';
|
||||
import { HEX_KEYS_REGEX } from '@dvmcp/commons/constants';
|
||||
import type { Config } from './src/config';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const isNpxRun = !__dirname.includes(process.cwd());
|
||||
if (!isNpxRun) {
|
||||
process.chdir(__dirname);
|
||||
}
|
||||
import {
|
||||
ConfigGenerator,
|
||||
generateHexKey,
|
||||
type FieldConfig,
|
||||
CONFIG_EMOJIS,
|
||||
validateHexKey,
|
||||
validateRelayUrl,
|
||||
} from '@dvmcp/commons/config-generator';
|
||||
import { join } from 'path';
|
||||
import type { Config } from './src/config.js';
|
||||
import { argv } from 'process';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
const configPath = join(process.cwd(), 'config.yml');
|
||||
const configExamplePath = join(__dirname, 'config.example.yml');
|
||||
|
||||
async function setupConfig() {
|
||||
console.log('🔧 DVMCP Discovery Configuration Setup 🔧');
|
||||
const configFields: Record<string, FieldConfig> = {
|
||||
nostr: {
|
||||
type: 'nested',
|
||||
description: 'Nostr Configuration',
|
||||
emoji: CONFIG_EMOJIS.NOSTR,
|
||||
fields: {
|
||||
privateKey: {
|
||||
type: 'hex',
|
||||
description: 'Private key',
|
||||
generator: generateHexKey,
|
||||
validation: validateHexKey,
|
||||
required: true,
|
||||
},
|
||||
relayUrls: {
|
||||
type: 'array',
|
||||
description: 'Relay URLs',
|
||||
validation: validateRelayUrl,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
mcp: {
|
||||
type: 'nested',
|
||||
description: 'Service Configuration',
|
||||
emoji: CONFIG_EMOJIS.SERVICE,
|
||||
fields: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Service name',
|
||||
default: 'DVMCP Discovery',
|
||||
},
|
||||
version: {
|
||||
type: 'string',
|
||||
description: 'Service version',
|
||||
default: '1.0.0',
|
||||
required: true,
|
||||
},
|
||||
about: {
|
||||
type: 'string',
|
||||
description: 'Service description',
|
||||
default: 'DVMCP Discovery Server for aggregating MCP tools from DVMs',
|
||||
},
|
||||
},
|
||||
},
|
||||
whitelist: {
|
||||
type: 'nested',
|
||||
description: 'DVM Whitelist Configuration',
|
||||
emoji: CONFIG_EMOJIS.WHITELIST,
|
||||
fields: {
|
||||
allowedDVMs: {
|
||||
type: 'set',
|
||||
description: 'Allowed DVM public keys',
|
||||
validation: validateHexKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 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 {
|
||||
const configure = async () => {
|
||||
console.log(
|
||||
'❌ Invalid key format. Please enter a 32-byte hex string.'
|
||||
`${CONFIG_EMOJIS.SETUP} DVMCP Discovery Configuration Setup ${CONFIG_EMOJIS.SETUP}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Generate a random key
|
||||
config.nostr.privateKey = Buffer.from(randomBytes(32)).toString('hex');
|
||||
console.log(`Generated new private key: ${config.nostr.privateKey}`);
|
||||
}
|
||||
const generator = new ConfigGenerator<Config>(configPath, configFields);
|
||||
await generator.generate();
|
||||
};
|
||||
|
||||
const runApp = async () => {
|
||||
const main = await import('./index.js');
|
||||
console.log(`${CONFIG_EMOJIS.INFO} Running main application...`);
|
||||
await main.default();
|
||||
};
|
||||
|
||||
const cliMain = async () => {
|
||||
if (argv.includes('--configure')) {
|
||||
await configure();
|
||||
}
|
||||
|
||||
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 {
|
||||
if (!existsSync(configPath)) {
|
||||
console.log(
|
||||
'❌ Invalid public key format. Please enter a 32-byte hex string.'
|
||||
`${CONFIG_EMOJIS.INFO} No configuration file found. Starting setup...`
|
||||
);
|
||||
}
|
||||
} 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.');
|
||||
}
|
||||
await configure();
|
||||
}
|
||||
|
||||
// Save the config
|
||||
writeFileSync(configPath, stringify(config));
|
||||
console.log(`\n✅ Configuration saved to ${configPath}`);
|
||||
}
|
||||
await runApp();
|
||||
};
|
||||
|
||||
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');
|
||||
cliMain().catch(console.error);
|
||||
|
||||
@@ -24,4 +24,4 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
export default main;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@dvmcp/discovery",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.7",
|
||||
"description": "Discovery service for MCP tools in the Nostr DVM ecosystem",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
@@ -17,8 +17,7 @@
|
||||
"scripts": {
|
||||
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
|
||||
"dev": "bun --watch index.ts",
|
||||
"start": "bun run index.ts",
|
||||
"setup": "bun run cli.ts",
|
||||
"start": "bun run cli.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "bun run typecheck && bun run format",
|
||||
"test": "bun test",
|
||||
@@ -34,7 +33,7 @@
|
||||
"@modelcontextprotocol/sdk": "^1.5.0",
|
||||
"nostr-tools": "^2.10.4",
|
||||
"yaml": "^2.7.0",
|
||||
"@dvmcp/commons": "^0.1.0"
|
||||
"@dvmcp/commons": "^0.1.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
Reference in New Issue
Block a user