mirror of
https://github.com/aljazceru/dvmcp.git
synced 2025-12-18 22:04:23 +01:00
feat: working on distribution
This commit is contained in:
175
packages/dvmcp-commons/.gitignore
vendored
Normal file
175
packages/dvmcp-commons/.gitignore
vendored
Normal file
@@ -0,0 +1,175 @@
|
||||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Caches
|
||||
|
||||
.cache
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
6
packages/dvmcp-commons/.npmignore
Normal file
6
packages/dvmcp-commons/.npmignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
*.log
|
||||
*.lock
|
||||
.DS_Store
|
||||
*.test.ts
|
||||
*.test.js
|
||||
15
packages/dvmcp-commons/README.md
Normal file
15
packages/dvmcp-commons/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# 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.
|
||||
5
packages/dvmcp-commons/constants.ts
Normal file
5
packages/dvmcp-commons/constants.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const HEX_KEYS_REGEX = /^(?:[0-9a-fA-F]{64})$/;
|
||||
export const DVM_ANNOUNCEMENT_KIND = 31990;
|
||||
export const DVM_NOTICE_KIND = 7000;
|
||||
export const TOOL_REQUEST_KIND = 5910;
|
||||
export const TOOL_RESPONSE_KIND = 6910;
|
||||
30
packages/dvmcp-commons/mock-server.ts
Normal file
30
packages/dvmcp-commons/mock-server.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
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');
|
||||
}
|
||||
28
packages/dvmcp-commons/nostr/key-manager.ts
Normal file
28
packages/dvmcp-commons/nostr/key-manager.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { hexToBytes } from '@noble/hashes/utils';
|
||||
import { getPublicKey, finalizeEvent } from 'nostr-tools/pure';
|
||||
import type { Event, UnsignedEvent } from 'nostr-tools/pure';
|
||||
|
||||
export const createKeyManager = (privateKeyHex: string) => {
|
||||
const privateKeyBytes = hexToBytes(privateKeyHex);
|
||||
const pubkey = getPublicKey(privateKeyBytes);
|
||||
|
||||
class Manager {
|
||||
public readonly pubkey = pubkey;
|
||||
|
||||
signEvent(eventInitial: UnsignedEvent): Event {
|
||||
return finalizeEvent(eventInitial, privateKeyBytes);
|
||||
}
|
||||
|
||||
createEventTemplate(kind: number): UnsignedEvent {
|
||||
return {
|
||||
kind,
|
||||
pubkey: this.pubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
content: '',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return new Manager();
|
||||
};
|
||||
197
packages/dvmcp-commons/nostr/mock-relay.ts
Normal file
197
packages/dvmcp-commons/nostr/mock-relay.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import { serve, type ServerWebSocket } from 'bun';
|
||||
import {
|
||||
finalizeEvent,
|
||||
generateSecretKey,
|
||||
type Filter,
|
||||
type NostrEvent,
|
||||
type UnsignedEvent,
|
||||
} from 'nostr-tools';
|
||||
import {
|
||||
DVM_ANNOUNCEMENT_KIND,
|
||||
TOOL_REQUEST_KIND,
|
||||
TOOL_RESPONSE_KIND,
|
||||
} from '../constants';
|
||||
|
||||
const relayPort = 3334;
|
||||
let mockEvents: NostrEvent[] = [];
|
||||
|
||||
const mockDVMAnnouncement = {
|
||||
kind: DVM_ANNOUNCEMENT_KIND,
|
||||
content: JSON.stringify({
|
||||
name: 'Test DVM',
|
||||
about: 'A test DVM instance',
|
||||
tools: [
|
||||
{
|
||||
name: 'test-echo',
|
||||
description: 'Echo test tool',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
text: { type: 'string' },
|
||||
},
|
||||
required: ['text'],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['d', 'dvm-announcement'],
|
||||
['k', `${TOOL_REQUEST_KIND}`],
|
||||
['capabilities', 'mcp-1.0'],
|
||||
['t', 'mcp'],
|
||||
['t', 'test-echo'],
|
||||
],
|
||||
} as UnsignedEvent;
|
||||
|
||||
const finalizedEvent = finalizeEvent(mockDVMAnnouncement, generateSecretKey());
|
||||
mockEvents.push(finalizedEvent);
|
||||
|
||||
const handleToolExecution = (event: NostrEvent) => {
|
||||
if (event.kind === TOOL_REQUEST_KIND) {
|
||||
const commandTag = event.tags.find((tag) => tag[0] === 'c');
|
||||
if (commandTag && commandTag[1] === 'execute-tool') {
|
||||
const request = JSON.parse(event.content);
|
||||
console.log('Processing execution request:', request);
|
||||
|
||||
const responseEvent = {
|
||||
kind: TOOL_RESPONSE_KIND,
|
||||
content: JSON.stringify({
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `[test] ${request.parameters.text}`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['e', event.id],
|
||||
['p', event.pubkey],
|
||||
['c', 'execute-tool-response'],
|
||||
],
|
||||
} as UnsignedEvent;
|
||||
|
||||
console.log('Created response event:', responseEvent);
|
||||
const finalizedResponse = finalizeEvent(
|
||||
responseEvent,
|
||||
generateSecretKey()
|
||||
);
|
||||
mockEvents.push(finalizedResponse);
|
||||
return finalizedResponse;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const server = serve({
|
||||
port: relayPort,
|
||||
fetch(req, server) {
|
||||
if (server.upgrade(req)) {
|
||||
return;
|
||||
}
|
||||
return new Response('Upgrade failed', { status: 500 });
|
||||
},
|
||||
websocket: {
|
||||
message(ws, message: string | Buffer) {
|
||||
try {
|
||||
const data = JSON.parse(message as string);
|
||||
console.log('Received message:', data);
|
||||
|
||||
if (data[0] === 'REQ') {
|
||||
const subscriptionId = data[1];
|
||||
const filter = data[2] as Filter;
|
||||
|
||||
activeSubscriptions.set(subscriptionId, { ws, filter });
|
||||
|
||||
const filteredEvents = mockEvents.filter((event) => {
|
||||
let matches = true;
|
||||
|
||||
if (filter.kinds && !filter.kinds.includes(event.kind)) {
|
||||
matches = false;
|
||||
}
|
||||
|
||||
if (filter.since && event.created_at < filter.since) {
|
||||
matches = false;
|
||||
}
|
||||
|
||||
return matches;
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Sending ${filteredEvents.length} filtered events for subscription ${subscriptionId}`
|
||||
);
|
||||
|
||||
filteredEvents.forEach((event) => {
|
||||
ws.send(JSON.stringify(['EVENT', subscriptionId, event]));
|
||||
});
|
||||
|
||||
ws.send(JSON.stringify(['EOSE', subscriptionId]));
|
||||
} else if (data[0] === 'EVENT') {
|
||||
const event: NostrEvent = data[1];
|
||||
mockEvents.push(event);
|
||||
|
||||
const response = handleToolExecution(event);
|
||||
if (response) {
|
||||
console.log('Created response event:', response);
|
||||
mockEvents.push(response);
|
||||
|
||||
for (const [subId, sub] of activeSubscriptions) {
|
||||
if (
|
||||
!sub.filter.kinds ||
|
||||
sub.filter.kinds.includes(response.kind)
|
||||
) {
|
||||
if (
|
||||
!sub.filter.since ||
|
||||
response.created_at >= sub.filter.since
|
||||
) {
|
||||
console.log(`Sending response to subscription ${subId}`);
|
||||
sub.ws.send(JSON.stringify(['EVENT', subId, response]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ws.send(JSON.stringify(['OK', event.id, true, '']));
|
||||
} else if (data[0] === 'CLOSE') {
|
||||
const subscriptionId = data[1];
|
||||
activeSubscriptions.delete(subscriptionId);
|
||||
console.log(`Subscription closed: ${subscriptionId}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing message:', error);
|
||||
}
|
||||
},
|
||||
open() {
|
||||
console.log('Client connected');
|
||||
},
|
||||
close() {
|
||||
console.log('Client disconnected');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Mock Nostr Relay started on port ${relayPort}`);
|
||||
|
||||
const activeSubscriptions = new Map<
|
||||
string,
|
||||
{
|
||||
ws: ServerWebSocket<unknown>;
|
||||
filter: Filter;
|
||||
}
|
||||
>();
|
||||
|
||||
const stop = async () => {
|
||||
for (const [_, sub] of activeSubscriptions) {
|
||||
try {
|
||||
sub.ws.close();
|
||||
} catch (e) {
|
||||
console.debug('Warning during subscription cleanup:', e);
|
||||
}
|
||||
}
|
||||
activeSubscriptions.clear();
|
||||
mockEvents = [];
|
||||
server.stop();
|
||||
};
|
||||
|
||||
export { server, mockEvents, stop };
|
||||
105
packages/dvmcp-commons/nostr/relay-handler.ts
Normal file
105
packages/dvmcp-commons/nostr/relay-handler.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { SimplePool } from 'nostr-tools/pool';
|
||||
import type { Event } from 'nostr-tools/pure';
|
||||
import type { SubCloser } from 'nostr-tools/pool';
|
||||
import WebSocket from 'ws';
|
||||
import { useWebSocketImplementation } from 'nostr-tools/pool';
|
||||
import type { Filter } from 'nostr-tools';
|
||||
import {
|
||||
DVM_NOTICE_KIND,
|
||||
TOOL_REQUEST_KIND,
|
||||
TOOL_RESPONSE_KIND,
|
||||
} from '../constants';
|
||||
|
||||
useWebSocketImplementation(WebSocket);
|
||||
|
||||
export class RelayHandler {
|
||||
private pool: SimplePool;
|
||||
private relayUrls: string[];
|
||||
private subscriptions: SubCloser[] = [];
|
||||
private reconnectInterval?: ReturnType<typeof setTimeout>;
|
||||
|
||||
constructor(relayUrls: string[]) {
|
||||
this.pool = new SimplePool();
|
||||
this.relayUrls = relayUrls;
|
||||
this.startReconnectLoop();
|
||||
}
|
||||
|
||||
private startReconnectLoop() {
|
||||
this.reconnectInterval = setInterval(() => {
|
||||
this.relayUrls.forEach((url) => {
|
||||
const normalizedUrl = new URL(url).href;
|
||||
if (!this.getConnectionStatus().get(normalizedUrl)) {
|
||||
this.ensureRelay(url);
|
||||
}
|
||||
});
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
private async ensureRelay(url: string) {
|
||||
try {
|
||||
await this.pool.ensureRelay(url, { connectionTimeout: 5000 });
|
||||
console.log(`Connected to relay: ${url}`);
|
||||
} catch (error) {
|
||||
console.log(`Failed to connect to relay ${url}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
async publishEvent(event: Event): Promise<void> {
|
||||
try {
|
||||
await Promise.any(this.pool.publish(this.relayUrls, event));
|
||||
console.log(
|
||||
`Event published(${event.kind}), id: ${event.id.slice(0, 12)}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to publish event:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
subscribeToRequests(
|
||||
onRequest: (event: Event) => void,
|
||||
filter?: Filter
|
||||
): SubCloser {
|
||||
const defaultFilter: Filter = {
|
||||
kinds: [TOOL_REQUEST_KIND, TOOL_RESPONSE_KIND, DVM_NOTICE_KIND],
|
||||
since: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
|
||||
const filters: Filter[] = [filter || defaultFilter];
|
||||
|
||||
const sub = this.pool.subscribeMany(this.relayUrls, filters, {
|
||||
onevent(event) {
|
||||
console.log(
|
||||
`Event received(${event.kind}), id: ${event.id.slice(0, 12)}`
|
||||
);
|
||||
onRequest(event);
|
||||
},
|
||||
oneose() {
|
||||
console.log('Reached end of stored events');
|
||||
},
|
||||
onclose(reasons) {
|
||||
console.log('Subscription closed:', reasons);
|
||||
},
|
||||
});
|
||||
|
||||
this.subscriptions.push(sub);
|
||||
return sub;
|
||||
}
|
||||
|
||||
async queryEvents(filter: Filter): Promise<Event[]> {
|
||||
return await this.pool.querySync(this.relayUrls, filter);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if (this.reconnectInterval) {
|
||||
clearInterval(this.reconnectInterval);
|
||||
}
|
||||
this.subscriptions.forEach((sub) => sub.close());
|
||||
this.subscriptions = [];
|
||||
this.pool.close(this.relayUrls);
|
||||
}
|
||||
|
||||
getConnectionStatus(): Map<string, boolean> {
|
||||
return this.pool.listConnectionStatus();
|
||||
}
|
||||
}
|
||||
26
packages/dvmcp-commons/package.json
Normal file
26
packages/dvmcp-commons/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@dvmcp/commons",
|
||||
"version": "0.1.0",
|
||||
"description": "Shared utilities for DVMCP packages",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./*": "./*.ts",
|
||||
"./*/*": "./*/*.ts"
|
||||
},
|
||||
"files": [
|
||||
"**/*.ts",
|
||||
"**/*.js",
|
||||
"!**/*.test.ts",
|
||||
"!**/*.test.js"
|
||||
],
|
||||
"scripts": {
|
||||
"format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
27
packages/dvmcp-commons/tsconfig.json
Normal file
27
packages/dvmcp-commons/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Enable latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user