mirror of
https://github.com/aljazceru/dvmcp.git
synced 2025-12-17 05:14:24 +01:00
Refactor/clis (#6)
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -178,4 +178,4 @@ dist
|
|||||||
codebase*
|
codebase*
|
||||||
.aider*
|
.aider*
|
||||||
.prettierrc
|
.prettierrc
|
||||||
config.yml
|
config.dvmcp.yml
|
||||||
@@ -42,8 +42,8 @@ This specification defines these event kinds:
|
|||||||
| Kind | Description |
|
| Kind | Description |
|
||||||
| ----- | ------------------------------------- |
|
| ----- | ------------------------------------- |
|
||||||
| 31990 | DVM Service Announcement (via NIP-89) |
|
| 31990 | DVM Service Announcement (via NIP-89) |
|
||||||
| 5910 | DVM-MCP Bridge Requests |
|
| 5910 | DVMCP Bridge Requests |
|
||||||
| 6910 | DVM-MCP Bridge Responses |
|
| 6910 | DVMCP Bridge Responses |
|
||||||
| 7000 | Job Feedback |
|
| 7000 | Job Feedback |
|
||||||
|
|
||||||
Operations are differentiated using the `c` tag, which specifies the command being executed:
|
Operations are differentiated using the `c` tag, which specifies the command being executed:
|
||||||
@@ -339,7 +339,7 @@ For any error, DVMs MUST:
|
|||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant Client as Nostr Client
|
participant Client as Nostr Client
|
||||||
participant Relay as Nostr Relay
|
participant Relay as Nostr Relay
|
||||||
participant DVM as MCP-DVM Bridge
|
participant DVM as DVMCP-Bridge
|
||||||
participant Server as MCP Server
|
participant Server as MCP Server
|
||||||
|
|
||||||
rect rgb(240, 240, 240)
|
rect rgb(240, 240, 240)
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ node_modules
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.test.ts
|
*.test.ts
|
||||||
*.test.js
|
*.test.js
|
||||||
config.yml
|
config.dvmcp.yml
|
||||||
@@ -12,11 +12,11 @@ A bridge implementation that connects Model Context Protocol (MCP) servers to No
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
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
|
When the package is run for the first time, it will detect if the `'config.dvmcp.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
|
```bash
|
||||||
cp config.example.yml config.yml
|
cp config.example.yml config.dvmcp.yml
|
||||||
nano config.yml
|
nano config.dvmcp.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
import { argv } from 'process';
|
import { argv } from 'process';
|
||||||
import type { Config } from './src/types';
|
import type { Config } from './src/types';
|
||||||
|
|
||||||
const configPath = join(process.cwd(), 'config.yml');
|
const configPath = join(process.cwd(), 'config.dvmcp.yml');
|
||||||
|
|
||||||
const configFields: Record<string, FieldConfig> = {
|
const configFields: Record<string, FieldConfig> = {
|
||||||
nostr: {
|
nostr: {
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ mcp:
|
|||||||
# Required client information
|
# Required client information
|
||||||
clientName: "DVM MCP Bridge Client"
|
clientName: "DVM MCP Bridge Client"
|
||||||
clientVersion: "1.0.0"
|
clientVersion: "1.0.0"
|
||||||
|
# optional metadata
|
||||||
|
picture: "https://image.nostr.build/5bf2e2eb3b858bf72c23e53ed1f41ed0f65b2c8a805eaa48dd506b7cfec4ab88.png"
|
||||||
|
website: "https://github.com/gzuuus/dvmcp"
|
||||||
# MCP Servers Configuration accepts multiple servers
|
# MCP Servers Configuration accepts multiple servers
|
||||||
servers:
|
servers:
|
||||||
- name: "server1"
|
- name: "server1"
|
||||||
|
|||||||
18
packages/dvmcp-bridge/config.yml
Normal file
18
packages/dvmcp-bridge/config.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
nostr:
|
||||||
|
privateKey: 0e79e5e90754cb23275477a32bd1c42bee28dc78a0b10693dfd8cac6852363fd
|
||||||
|
relayUrls:
|
||||||
|
- ws://localhost:10547
|
||||||
|
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: hello
|
||||||
|
command: bun
|
||||||
|
args:
|
||||||
|
- --cwd
|
||||||
|
- /home/user/Documents/dev/gz/js-ts/bun/mcp-echo
|
||||||
|
- start:mcp
|
||||||
|
whitelist:
|
||||||
|
allowedPubkeys: []
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@dvmcp/bridge",
|
"name": "@dvmcp/bridge",
|
||||||
"version": "0.1.6",
|
"version": "0.1.11",
|
||||||
"description": "Bridge connecting MCP servers to Nostr's DVM ecosystem",
|
"description": "Bridge connecting MCP servers to Nostr's DVM ecosystem",
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"nostr-tools": "^2.10.4",
|
"nostr-tools": "^2.10.4",
|
||||||
"yaml": "^2.7.0",
|
"yaml": "^2.7.0",
|
||||||
"@dvmcp/commons": "^0.1.1"
|
"@dvmcp/commons": "^0.1.2"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ export class NostrAnnouncer {
|
|||||||
content: JSON.stringify({
|
content: JSON.stringify({
|
||||||
name: CONFIG.mcp.name,
|
name: CONFIG.mcp.name,
|
||||||
about: CONFIG.mcp.about,
|
about: CONFIG.mcp.about,
|
||||||
|
picture: CONFIG.mcp.picture,
|
||||||
|
website: CONFIG.mcp.website,
|
||||||
|
banner: CONFIG.mcp.banner,
|
||||||
tools: tools,
|
tools: tools,
|
||||||
}),
|
}),
|
||||||
tags: [
|
tags: [
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { existsSync, readFileSync } from 'fs';
|
|||||||
import { HEX_KEYS_REGEX } from '@dvmcp/commons/constants';
|
import { HEX_KEYS_REGEX } from '@dvmcp/commons/constants';
|
||||||
import type { Config, MCPServerConfig } from './types';
|
import type { Config, MCPServerConfig } from './types';
|
||||||
|
|
||||||
const CONFIG_PATH = join(process.cwd(), 'config.yml');
|
const CONFIG_PATH = join(process.cwd(), 'config.dvmcp.yml');
|
||||||
|
|
||||||
const TEST_CONFIG: Config = {
|
const TEST_CONFIG: Config = {
|
||||||
nostr: {
|
nostr: {
|
||||||
@@ -87,7 +87,7 @@ function loadConfig(): Config {
|
|||||||
|
|
||||||
if (!existsSync(CONFIG_PATH)) {
|
if (!existsSync(CONFIG_PATH)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'No config.yml file found. Please create one based on config.example.yml'
|
'No config.dvmcp.yml file found. Please create one based on config.example.yml'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +117,9 @@ function loadConfig(): Config {
|
|||||||
rawConfig.mcp?.clientVersion,
|
rawConfig.mcp?.clientVersion,
|
||||||
'mcp.clientVersion'
|
'mcp.clientVersion'
|
||||||
),
|
),
|
||||||
|
picture: rawConfig.mcp?.picture,
|
||||||
|
website: rawConfig.mcp?.website,
|
||||||
|
banner: rawConfig.mcp?.banner,
|
||||||
servers: validateMCPServers(rawConfig.mcp?.servers),
|
servers: validateMCPServers(rawConfig.mcp?.servers),
|
||||||
},
|
},
|
||||||
whitelist: {
|
whitelist: {
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ export class DVMBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private isWhitelisted(pubkey: string): boolean {
|
private isWhitelisted(pubkey: string): boolean {
|
||||||
if (!CONFIG.whitelist.allowedPubkeys) {
|
if (
|
||||||
|
!CONFIG.whitelist.allowedPubkeys ||
|
||||||
|
CONFIG.whitelist.allowedPubkeys.size == 0
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return CONFIG.whitelist.allowedPubkeys.has(pubkey);
|
return CONFIG.whitelist.allowedPubkeys.has(pubkey);
|
||||||
@@ -47,7 +50,12 @@ export class DVMBridge {
|
|||||||
await this.nostrAnnouncer.updateAnnouncement();
|
await this.nostrAnnouncer.updateAnnouncement();
|
||||||
|
|
||||||
console.log('Setting up request handlers...');
|
console.log('Setting up request handlers...');
|
||||||
this.relayHandler.subscribeToRequests(this.handleRequest.bind(this));
|
const publicKey = keyManager.getPublicKey();
|
||||||
|
this.relayHandler.subscribeToRequests(this.handleRequest.bind(this), {
|
||||||
|
kinds: [TOOL_REQUEST_KIND],
|
||||||
|
'#p': [publicKey],
|
||||||
|
since: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
|
||||||
this.isRunning = true;
|
this.isRunning = true;
|
||||||
console.log('DVM Bridge is now running and ready to handle requests');
|
console.log('DVM Bridge is now running and ready to handle requests');
|
||||||
@@ -88,14 +96,14 @@ export class DVMBridge {
|
|||||||
tools,
|
tools,
|
||||||
}),
|
}),
|
||||||
tags: [
|
tags: [
|
||||||
['request', JSON.stringify(event)],
|
['c', 'list-tools-response'],
|
||||||
['e', event.id],
|
['e', event.id],
|
||||||
['p', event.pubkey],
|
['p', event.pubkey],
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.relayHandler.publishEvent(response);
|
await this.relayHandler.publishEvent(response);
|
||||||
} else {
|
} else if (command === 'execute-tool') {
|
||||||
const jobRequest = JSON.parse(event.content);
|
const jobRequest = JSON.parse(event.content);
|
||||||
const processingStatus = keyManager.signEvent({
|
const processingStatus = keyManager.signEvent({
|
||||||
...keyManager.createEventTemplate(DVM_NOTICE_KIND),
|
...keyManager.createEventTemplate(DVM_NOTICE_KIND),
|
||||||
@@ -125,7 +133,7 @@ export class DVMBridge {
|
|||||||
...keyManager.createEventTemplate(TOOL_RESPONSE_KIND),
|
...keyManager.createEventTemplate(TOOL_RESPONSE_KIND),
|
||||||
content: JSON.stringify(result),
|
content: JSON.stringify(result),
|
||||||
tags: [
|
tags: [
|
||||||
['request', JSON.stringify(event)],
|
['c', 'execute-tool-response'],
|
||||||
['e', event.id],
|
['e', event.id],
|
||||||
['p', event.pubkey],
|
['p', event.pubkey],
|
||||||
],
|
],
|
||||||
@@ -159,6 +167,7 @@ export class DVMBridge {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
await this.relayHandler.publishEvent(errorStatus);
|
await this.relayHandler.publishEvent(errorStatus);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error handling request:', error);
|
console.error('Error handling request:', error);
|
||||||
|
|||||||
@@ -22,4 +22,9 @@ describe('KeyManager', () => {
|
|||||||
expect(signedEvent.sig).toBeDefined();
|
expect(signedEvent.sig).toBeDefined();
|
||||||
expect(signedEvent.pubkey).toBe(keyManager.pubkey);
|
expect(signedEvent.pubkey).toBe(keyManager.pubkey);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should get public key', () => {
|
||||||
|
const publicKey = keyManager.getPublicKey();
|
||||||
|
expect(publicKey).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ describe('MCPPool', () => {
|
|||||||
let transports: any[] = [];
|
let transports: any[] = [];
|
||||||
const serverNames = ['server1', 'server2', 'server3', 'server4'];
|
const serverNames = ['server1', 'server2', 'server3', 'server4'];
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const mockServerPath = join(import.meta.dir, 'mock-server.ts');
|
const mockServerPath = join(
|
||||||
|
import.meta.dir,
|
||||||
|
'../../dvmcp-commons/mock-server.ts'
|
||||||
|
);
|
||||||
|
|
||||||
const serverConfigs = serverNames.map((name) => ({
|
const serverConfigs = serverNames.map((name) => ({
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
||||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export const createMockServer = async (name: string) => {
|
|
||||||
const server = new McpServer({
|
|
||||||
name: `Mock ${name}`,
|
|
||||||
version: '1.0.0',
|
|
||||||
});
|
|
||||||
|
|
||||||
server.tool(
|
|
||||||
`${name}-echo`,
|
|
||||||
`Echo tool for ${name}`,
|
|
||||||
{
|
|
||||||
text: z.string(),
|
|
||||||
},
|
|
||||||
async ({ text }) => ({
|
|
||||||
content: [{ type: 'text' as const, text: `[${name}] ${text}` }],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const transport = new StdioServerTransport();
|
|
||||||
await server.connect(transport);
|
|
||||||
|
|
||||||
return { server, transport };
|
|
||||||
};
|
|
||||||
|
|
||||||
if (import.meta.path === Bun.main) {
|
|
||||||
await createMockServer(process.argv[2] || 'default');
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,9 @@ export interface MCPConfig {
|
|||||||
about: string;
|
about: string;
|
||||||
clientName: string;
|
clientName: string;
|
||||||
clientVersion: string;
|
clientVersion: string;
|
||||||
|
picture?: string;
|
||||||
|
website?: string;
|
||||||
|
banner?: string;
|
||||||
servers: MCPServerConfig[];
|
servers: MCPServerConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createInterface } from 'node:readline';
|
import { createInterface } from 'node:readline';
|
||||||
import { stringify } from 'yaml';
|
import { parse, stringify } from 'yaml';
|
||||||
import { writeFileSync, existsSync } from 'node:fs';
|
import { writeFileSync, existsSync, readFileSync } from 'node:fs';
|
||||||
import { randomBytes } from 'node:crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
import { HEX_KEYS_REGEX } from './constants';
|
import { HEX_KEYS_REGEX } from './constants';
|
||||||
|
|
||||||
@@ -47,10 +47,21 @@ export interface FieldConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ConfigGenerator<T extends Record<string, any>> {
|
export class ConfigGenerator<T extends Record<string, any>> {
|
||||||
|
private currentConfig: T | null = null;
|
||||||
constructor(
|
constructor(
|
||||||
private configPath: string,
|
private configPath: string,
|
||||||
private fields: Record<string, FieldConfig>
|
private fields: Record<string, FieldConfig>
|
||||||
) {}
|
) {
|
||||||
|
if (existsSync(configPath)) {
|
||||||
|
try {
|
||||||
|
this.currentConfig = parse(readFileSync(configPath, 'utf8'));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
`${CONFIG_EMOJIS.INFO} Could not parse existing configuration`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async prompt(question: string, defaultValue = ''): Promise<string> {
|
private async prompt(question: string, defaultValue = ''): Promise<string> {
|
||||||
const rl = createInterface({
|
const rl = createInterface({
|
||||||
@@ -112,6 +123,27 @@ export class ConfigGenerator<T extends Record<string, any>> {
|
|||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const emoji = config.emoji || CONFIG_EMOJIS.PROMPT;
|
const emoji = config.emoji || CONFIG_EMOJIS.PROMPT;
|
||||||
|
|
||||||
|
if (currentValue !== undefined) {
|
||||||
|
if (config.type !== 'nested' && config.type !== 'object-array') {
|
||||||
|
console.log(
|
||||||
|
`${CONFIG_EMOJIS.INFO} Current value: ${
|
||||||
|
typeof currentValue === 'object'
|
||||||
|
? JSON.stringify(currentValue)
|
||||||
|
: currentValue
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const keepCurrent = await this.promptYesNo(
|
||||||
|
`${emoji} Keep current ${fieldName}?`,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
if (keepCurrent) {
|
||||||
|
return currentValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (config.type === 'nested') {
|
if (config.type === 'nested') {
|
||||||
console.log(`\n${emoji} ${config.description || fieldName}`);
|
console.log(`\n${emoji} ${config.description || fieldName}`);
|
||||||
}
|
}
|
||||||
@@ -151,7 +183,20 @@ export class ConfigGenerator<T extends Record<string, any>> {
|
|||||||
array.forEach((item: string, index: number) => {
|
array.forEach((item: string, index: number) => {
|
||||||
console.log(`${CONFIG_EMOJIS.INFO} ${index + 1}. ${item}`);
|
console.log(`${CONFIG_EMOJIS.INFO} ${index + 1}. ${item}`);
|
||||||
});
|
});
|
||||||
console.log('');
|
|
||||||
|
if (await this.promptYesNo(`${emoji} Remove any items?`, false)) {
|
||||||
|
while (true) {
|
||||||
|
const index =
|
||||||
|
parseInt(
|
||||||
|
await this.prompt('Enter index to remove (0 to finish):')
|
||||||
|
) - 1;
|
||||||
|
if (isNaN(index) || index < 0) break;
|
||||||
|
if (index < array.length) {
|
||||||
|
array.splice(index, 1);
|
||||||
|
console.log('Item removed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.promptYesNo(`${emoji} Add ${fieldName}?`, true)) {
|
if (await this.promptYesNo(`${emoji} Add ${fieldName}?`, true)) {
|
||||||
@@ -187,7 +232,7 @@ export class ConfigGenerator<T extends Record<string, any>> {
|
|||||||
},
|
},
|
||||||
|
|
||||||
'object-array': async () => {
|
'object-array': async () => {
|
||||||
const array: ArrayItem[] = currentValue || [];
|
let array: ArrayItem[] = currentValue || [];
|
||||||
if (array.length > 0) {
|
if (array.length > 0) {
|
||||||
console.log('\nCurrent servers:');
|
console.log('\nCurrent servers:');
|
||||||
array.forEach((item: ArrayItem, index: number) => {
|
array.forEach((item: ArrayItem, index: number) => {
|
||||||
@@ -196,19 +241,49 @@ export class ConfigGenerator<T extends Record<string, any>> {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
console.log('');
|
console.log('');
|
||||||
}
|
const keepCurrent = await this.promptYesNo(
|
||||||
|
`${emoji} Keep current ${fieldName}?`,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
if (!keepCurrent) {
|
||||||
|
array = [];
|
||||||
|
console.log(`${CONFIG_EMOJIS.INFO} Cleared existing servers.`);
|
||||||
|
} else {
|
||||||
if (
|
if (
|
||||||
(await this.promptYesNo(`${emoji} Add new ${fieldName}?`, true)) &&
|
await this.promptYesNo(
|
||||||
config.fields
|
`${emoji} Remove any existing servers?`,
|
||||||
|
false
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
while (true) {
|
while (true) {
|
||||||
const item = await this.handleObjectArrayItem(config.fields);
|
const index =
|
||||||
|
parseInt(
|
||||||
|
await this.prompt(
|
||||||
|
'Enter server number to remove (0 to finish):'
|
||||||
|
)
|
||||||
|
) - 1;
|
||||||
|
if (isNaN(index) || index < 0) break;
|
||||||
|
if (index < array.length) {
|
||||||
|
console.log(
|
||||||
|
`${CONFIG_EMOJIS.INFO} Removed server: ${array[index].name}`
|
||||||
|
);
|
||||||
|
array.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await this.promptYesNo(`${emoji} Add new ${fieldName}?`, true)) {
|
||||||
|
while (true) {
|
||||||
|
const item = await this.handleObjectArrayItem(config.fields!);
|
||||||
if (!item) break;
|
if (!item) break;
|
||||||
array.push(item as ArrayItem);
|
array.push(item as ArrayItem);
|
||||||
|
console.log(`${CONFIG_EMOJIS.INFO} Added new server: ${item.name}`);
|
||||||
console.log('');
|
console.log('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return array;
|
return array;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -269,19 +344,15 @@ export class ConfigGenerator<T extends Record<string, any>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async generate(): Promise<T> {
|
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> = {};
|
const config: Record<string, any> = {};
|
||||||
for (const [fieldName, fieldConfig] of Object.entries(this.fields)) {
|
for (const [fieldName, fieldConfig] of Object.entries(this.fields)) {
|
||||||
config[fieldName] = await this.handleField(fieldName, fieldConfig);
|
const currentValue = this.currentConfig?.[fieldName];
|
||||||
|
config[fieldName] = await this.handleField(
|
||||||
|
fieldName,
|
||||||
|
fieldConfig,
|
||||||
|
true,
|
||||||
|
currentValue
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFileSync(this.configPath, stringify(config));
|
writeFileSync(this.configPath, stringify(config));
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ export const createKeyManager = (privateKeyHex: string) => {
|
|||||||
content: '',
|
content: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
getPublicKey(): string {
|
||||||
|
return this.pubkey;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Manager();
|
return new Manager();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@dvmcp/commons",
|
"name": "@dvmcp/commons",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"description": "Shared utilities for DVMCP packages",
|
"description": "Shared utilities for DVMCP packages",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ node_modules
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.test.ts
|
*.test.ts
|
||||||
*.test.js
|
*.test.js
|
||||||
config.yml
|
config.dvmcp.yml
|
||||||
@@ -11,11 +11,11 @@ A MCP server implementation that aggregates tools from DVMs across the Nostr net
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
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
|
When the package is run for the first time, it will detect if the `config.dvmcp.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
|
```bash
|
||||||
cp config.example.yml config.yml
|
cp config.example.yml config.dvmcp.yml
|
||||||
nano config.yml
|
nano config.dvmcp.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import type { Config } from './src/config.js';
|
|||||||
import { argv } from 'process';
|
import { argv } from 'process';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
|
|
||||||
const configPath = join(process.cwd(), 'config.yml');
|
const configPath = join(process.cwd(), 'config.dvmcp.yml');
|
||||||
|
|
||||||
const configFields: Record<string, FieldConfig> = {
|
const configFields: Record<string, FieldConfig> = {
|
||||||
nostr: {
|
nostr: {
|
||||||
|
|||||||
10
packages/dvmcp-discovery/config.yml
Normal file
10
packages/dvmcp-discovery/config.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
nostr:
|
||||||
|
privateKey: c5af6a2588250d3dfe82a9cc1c054697d7483b53850d7e6552620ff8e565630c
|
||||||
|
relayUrls:
|
||||||
|
- wss://relay.dvmcp.fun
|
||||||
|
mcp:
|
||||||
|
name: DVMCP Discovery
|
||||||
|
version: 1.0.0
|
||||||
|
about: DVMCP Discovery Server for aggregating MCP tools from DVMs
|
||||||
|
whitelist:
|
||||||
|
allowedDVMs: []
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@dvmcp/discovery",
|
"name": "@dvmcp/discovery",
|
||||||
"version": "0.1.7",
|
"version": "0.1.11",
|
||||||
"description": "Discovery service for MCP tools in the Nostr DVM ecosystem",
|
"description": "Discovery service for MCP tools in the Nostr DVM ecosystem",
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"@modelcontextprotocol/sdk": "^1.5.0",
|
"@modelcontextprotocol/sdk": "^1.5.0",
|
||||||
"nostr-tools": "^2.10.4",
|
"nostr-tools": "^2.10.4",
|
||||||
"yaml": "^2.7.0",
|
"yaml": "^2.7.0",
|
||||||
"@dvmcp/commons": "^0.1.1"
|
"@dvmcp/commons": "^0.1.2"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export interface Config {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG_PATH = join(process.cwd(), 'config.yml');
|
const CONFIG_PATH = join(process.cwd(), 'config.dvmcp.yml');
|
||||||
|
|
||||||
const TEST_CONFIG: Config = {
|
const TEST_CONFIG: Config = {
|
||||||
nostr: {
|
nostr: {
|
||||||
@@ -79,7 +79,7 @@ function loadConfig(): Config {
|
|||||||
|
|
||||||
if (!existsSync(CONFIG_PATH)) {
|
if (!existsSync(CONFIG_PATH)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'No config.yml file found. Please create one based on config.example.yml'
|
'No config.dvmcp.yml file found. Please create one based on config.example.yml'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export class DiscoveryServer {
|
|||||||
if (!announcement?.tools) return;
|
if (!announcement?.tools) return;
|
||||||
|
|
||||||
for (const tool of announcement.tools) {
|
for (const tool of announcement.tools) {
|
||||||
const toolId = `${event.pubkey.slice(0, 12)}:${tool.name}`;
|
const toolId = `${tool.name}:${event.pubkey.slice(0, 4)}`;
|
||||||
this.toolRegistry.registerTool(toolId, tool);
|
this.toolRegistry.registerTool(toolId, tool);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -72,7 +72,7 @@ export class DiscoveryServer {
|
|||||||
private isAllowedDVM(pubkey: string): boolean {
|
private isAllowedDVM(pubkey: string): boolean {
|
||||||
if (
|
if (
|
||||||
!CONFIG.whitelist?.allowedDVMs ||
|
!CONFIG.whitelist?.allowedDVMs ||
|
||||||
CONFIG.whitelist.allowedDVMs.size === 0
|
CONFIG.whitelist.allowedDVMs.size == 0
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ describe('DiscoveryServer E2E', () => {
|
|||||||
const toolIds = Array.from(toolRegistry['discoveredTools'].keys());
|
const toolIds = Array.from(toolRegistry['discoveredTools'].keys());
|
||||||
console.log('Available tool IDs:', toolIds);
|
console.log('Available tool IDs:', toolIds);
|
||||||
|
|
||||||
const toolId = toolIds.find((id) => id.endsWith(`:${mockTool.name}`));
|
const toolId = toolIds.find((id) => id.startsWith(`${mockTool.name}`));
|
||||||
expect(toolId).toBeDefined();
|
expect(toolId).toBeDefined();
|
||||||
console.log('Selected tool ID:', toolId);
|
console.log('Selected tool ID:', toolId);
|
||||||
|
|
||||||
|
|||||||
@@ -104,10 +104,18 @@ export class ToolExecutor {
|
|||||||
|
|
||||||
private createToolRequest(tool: Tool, params: unknown): Event {
|
private createToolRequest(tool: Tool, params: unknown): Event {
|
||||||
const request = this.keyManager.createEventTemplate(TOOL_REQUEST_KIND);
|
const request = this.keyManager.createEventTemplate(TOOL_REQUEST_KIND);
|
||||||
|
|
||||||
|
const parameters =
|
||||||
|
!tool.inputSchema.properties ||
|
||||||
|
Object.keys(tool.inputSchema.properties).length === 0
|
||||||
|
? {}
|
||||||
|
: params;
|
||||||
|
|
||||||
request.content = JSON.stringify({
|
request.content = JSON.stringify({
|
||||||
name: tool.name,
|
name: tool.name,
|
||||||
parameters: params,
|
parameters,
|
||||||
});
|
});
|
||||||
|
|
||||||
request.tags.push(['c', 'execute-tool']);
|
request.tags.push(['c', 'execute-tool']);
|
||||||
return this.keyManager.signEvent(request);
|
return this.keyManager.signEvent(request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,27 +81,34 @@ export class ToolRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private mapJsonSchemaToZod(schema: Tool['inputSchema']): z.ZodRawShape {
|
private mapJsonSchemaToZod(schema: Tool['inputSchema']): z.ZodRawShape {
|
||||||
|
if (!schema.properties || Object.keys(schema.properties).length === 0) {
|
||||||
|
return { _: z.object({}).optional() };
|
||||||
|
}
|
||||||
|
|
||||||
const properties: z.ZodRawShape = {};
|
const properties: z.ZodRawShape = {};
|
||||||
if (schema.properties) {
|
|
||||||
for (const [key, prop] of Object.entries(schema.properties)) {
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
||||||
if (typeof prop === 'object' && prop && 'type' in prop) {
|
if (typeof prop === 'object' && prop && 'type' in prop) {
|
||||||
|
let zodType: z.ZodType;
|
||||||
switch (prop.type) {
|
switch (prop.type) {
|
||||||
case 'string':
|
case 'string':
|
||||||
properties[key] = z.string();
|
zodType = z.string();
|
||||||
break;
|
break;
|
||||||
case 'number':
|
case 'number':
|
||||||
properties[key] = z.number();
|
zodType = z.number();
|
||||||
break;
|
break;
|
||||||
case 'integer':
|
case 'integer':
|
||||||
properties[key] = z.number().int();
|
zodType = z.number().int();
|
||||||
break;
|
break;
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
properties[key] = z.boolean();
|
zodType = z.boolean();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
properties[key] = z.any();
|
zodType = z.any();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
properties[key] =
|
||||||
|
Array.isArray(schema.required) && schema.required.includes(key)
|
||||||
|
? zodType
|
||||||
|
: zodType.optional();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return properties;
|
return properties;
|
||||||
|
|||||||
Reference in New Issue
Block a user