From 1625661356d14747be0072eb6592de191adfb8d1 Mon Sep 17 00:00:00 2001 From: gzuuus Date: Sat, 8 Feb 2025 12:21:16 +0100 Subject: [PATCH] Init --- .env.example | 13 +++ .gitignore | 179 +++++++++++++++++++++++++++++++++++++++++ .prettierrc | 8 ++ README.md | 15 ++++ bun.lock | 98 ++++++++++++++++++++++ package.json | 25 ++++++ src/config.ts | 21 +++++ src/dvm-bridge.ts | 108 +++++++++++++++++++++++++ src/mcp-client.ts | 49 +++++++++++ src/nostr/announcer.ts | 30 +++++++ src/nostr/keys.ts | 33 ++++++++ src/nostr/relay.ts | 59 ++++++++++++++ tsconfig.json | 27 +++++++ 13 files changed, 665 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bun.lock create mode 100644 package.json create mode 100644 src/config.ts create mode 100644 src/dvm-bridge.ts create mode 100644 src/mcp-client.ts create mode 100644 src/nostr/announcer.ts create mode 100644 src/nostr/keys.ts create mode 100644 src/nostr/relay.ts create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..aa63a12 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Nostr +PRIVATE_KEY=your_private_key_here +RELAY_URLS=wss://relay.damus.io,wss://relay.nostr.band,wss://nos.lol + +# MCP Service Info +MCP_SERVICE_NAME="DVM MCP Bridge" +MCP_SERVICE_ABOUT="MCP-enabled DVM providing AI and computational tools" + +# MCP Client Connection +MCP_CLIENT_NAME="DVM MCP Bridge Client" +MCP_CLIENT_VERSION="1.0.0" +MCP_SERVER_COMMAND=bun # The command to start the MCP server +MCP_SERVER_ARGS=run,src/external-mcp-server.ts # Comma-separated args to pass to the server command \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a7e46e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,179 @@ +# 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 + +# +codebase* +.aider* diff --git a/.prettierrc b/.prettierrc index e69de29..a9999da 100644 --- a/.prettierrc +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 80, + "bracketSpacing": true +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..851132c --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# dvmcp + +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. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..6b57801 --- /dev/null +++ b/bun.lock @@ -0,0 +1,98 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "dvmcp", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.4.1", + "@noble/hashes": "^1.7.1", + "dotenv": "^16.4.7", + "nostr-tools": "^2.10.4", + "prettier": "^3.4.2", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + }, + }, + "packages": { + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.4.1", "", { "dependencies": { "content-type": "^1.0.5", "eventsource": "^3.0.2", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-wS6YC4lkUZ9QpP+/7NBTlVNiEvsnyl0xF7rRusLF+RsG0xDPc/zWR7fEEyhKnnNutGsDAZh59l/AeoWGwIb1+g=="], + + "@noble/ciphers": ["@noble/ciphers@0.5.3", "", {}, "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w=="], + + "@noble/curves": ["@noble/curves@1.2.0", "", { "dependencies": { "@noble/hashes": "1.3.2" } }, "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw=="], + + "@noble/hashes": ["@noble/hashes@1.7.1", "", {}, "sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ=="], + + "@scure/base": ["@scure/base@1.1.1", "", {}, "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="], + + "@scure/bip32": ["@scure/bip32@1.3.1", "", { "dependencies": { "@noble/curves": "~1.1.0", "@noble/hashes": "~1.3.1", "@scure/base": "~1.1.0" } }, "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A=="], + + "@scure/bip39": ["@scure/bip39@1.2.1", "", { "dependencies": { "@noble/hashes": "~1.3.0", "@scure/base": "~1.1.0" } }, "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg=="], + + "@types/bun": ["@types/bun@1.2.2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="], + + "@types/node": ["@types/node@22.13.1", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew=="], + + "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], + + "bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="], + + "eventsource": ["eventsource@3.0.5", "", { "dependencies": { "eventsource-parser": "^3.0.0" } }, "sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw=="], + + "eventsource-parser": ["eventsource-parser@3.0.0", "", {}, "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA=="], + + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "nostr-tools": ["nostr-tools@2.10.4", "", { "dependencies": { "@noble/ciphers": "^0.5.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.1", "@scure/base": "1.1.1", "@scure/bip32": "1.3.1", "@scure/bip39": "1.2.1" }, "optionalDependencies": { "nostr-wasm": "0.1.0" }, "peerDependencies": { "typescript": ">=5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-biU7sk+jxHgVASfobg2T5ttxOGGSt69wEVBC51sHHOEaKAAdzHBLV/I2l9Rf61UzClhliZwNouYhqIso4a3HYg=="], + + "nostr-wasm": ["nostr-wasm@0.1.0", "", {}, "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="], + + "prettier": ["prettier@3.4.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ=="], + + "raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.24.1", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w=="], + + "@noble/curves/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], + + "@scure/bip32/@noble/curves": ["@noble/curves@1.1.0", "", { "dependencies": { "@noble/hashes": "1.3.1" } }, "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA=="], + + "@scure/bip32/@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="], + + "@scure/bip39/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], + + "nostr-tools/@noble/hashes": ["@noble/hashes@1.3.1", "", {}, "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5bcc777 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "dvmcp", + "module": "src/dvm-bridge.ts", + "type": "module", + "scripts": { + "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json}\"", + "dev": "bun --watch src/dvm-bridge.ts", + "start": "bun run src/dvm-bridge.ts", + "typecheck": "tsc --noEmit", + "lint": "bun run typecheck && bun run format" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.4.1", + "@noble/hashes": "^1.7.1", + "dotenv": "^16.4.7", + "nostr-tools": "^2.10.4", + "prettier": "^3.4.2" + } +} \ No newline at end of file diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..50ee6ba --- /dev/null +++ b/src/config.ts @@ -0,0 +1,21 @@ +import { config } from 'dotenv'; +config(); + +export const CONFIG = { + nostr: { + privateKey: process.env.PRIVATE_KEY!, + relayUrls: process.env.RELAY_URLS!.split(',').map((url) => url.trim()), + }, + mcp: { + // Service info + name: process.env.MCP_SERVICE_NAME || 'DVM MCP Bridge', + about: + process.env.MCP_SERVICE_ABOUT || + 'MCP-enabled DVM providing AI and computational tools', + // Client connection info + clientName: process.env.MCP_CLIENT_NAME!, + clientVersion: process.env.MCP_CLIENT_VERSION!, + serverCommand: process.env.MCP_SERVER_COMMAND!, + serverArgs: process.env.MCP_SERVER_ARGS!.split(','), + }, +}; diff --git a/src/dvm-bridge.ts b/src/dvm-bridge.ts new file mode 100644 index 0000000..37125cb --- /dev/null +++ b/src/dvm-bridge.ts @@ -0,0 +1,108 @@ +import { MCPClientHandler } from './mcp-client'; +import { NostrAnnouncer } from './nostr/announcer'; +import { RelayHandler } from './nostr/relay'; +import { keyManager } from './nostr/keys'; +import { CONFIG } from './config'; +import type { Event } from 'nostr-tools/pure'; + +export class DVMBridge { + private mcpClient: MCPClientHandler; + private nostrAnnouncer: NostrAnnouncer; + private relayHandler: RelayHandler; + + constructor() { + this.mcpClient = new MCPClientHandler(); + this.nostrAnnouncer = new NostrAnnouncer(); + this.relayHandler = new RelayHandler(CONFIG.nostr.relayUrls); + } + + async start() { + await this.mcpClient.connect(); + + const tools = await this.mcpClient.listTools(); + console.log('Available MCP tools:', tools); + + await this.nostrAnnouncer.announceService(); + + this.relayHandler.subscribeToRequests(this.handleRequest.bind(this)); + } + + private async handleRequest(event: Event) { + try { + if (event.kind === 5000) { + const tools = await this.mcpClient.listTools(); + + const response = keyManager.signEvent({ + ...keyManager.createEventTemplate(6000), + content: JSON.stringify({ + schema_version: '1.0', + tools, + }), + tags: [ + ['e', event.id], + ['p', event.pubkey], + ], + }); + + await this.relayHandler.publishEvent(response); + } else if (event.kind === 5001) { + const { name, parameters } = JSON.parse(event.content); + + const processingStatus = keyManager.signEvent({ + ...keyManager.createEventTemplate(7000), + tags: [ + ['status', 'processing'], + ['e', event.id], + ['p', event.pubkey], + ], + }); + await this.relayHandler.publishEvent(processingStatus); + + try { + const result = await this.mcpClient.callTool(name, parameters); + + const successStatus = keyManager.signEvent({ + ...keyManager.createEventTemplate(7000), + tags: [ + ['status', 'success'], + ['e', event.id], + ['p', event.pubkey], + ], + }); + await this.relayHandler.publishEvent(successStatus); + + const response = keyManager.signEvent({ + ...keyManager.createEventTemplate(6001), + content: JSON.stringify(result), + tags: [ + ['e', event.id], + ['p', event.pubkey], + ], + }); + await this.relayHandler.publishEvent(response); + } catch (error) { + const errorStatus = keyManager.signEvent({ + ...keyManager.createEventTemplate(7000), + tags: [ + [ + 'status', + 'error', + error instanceof Error ? error.message : 'Unknown error', + ], + ['e', event.id], + ['p', event.pubkey], + ], + }); + await this.relayHandler.publishEvent(errorStatus); + } + } + } catch (error) { + console.error('Error handling request:', error); + } + } + + async stop() { + await this.mcpClient.disconnect(); + this.relayHandler.cleanup(); + } +} diff --git a/src/mcp-client.ts b/src/mcp-client.ts new file mode 100644 index 0000000..3a99f5e --- /dev/null +++ b/src/mcp-client.ts @@ -0,0 +1,49 @@ +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { CONFIG } from './config'; + +export class MCPClientHandler { + private client: Client; + private transport: StdioClientTransport; + + constructor() { + this.transport = new StdioClientTransport({ + command: CONFIG.mcp.serverCommand, + args: CONFIG.mcp.serverArgs, + }); + + this.client = new Client( + { + name: CONFIG.mcp.clientName, + version: CONFIG.mcp.clientVersion, + }, + { + capabilities: { + tools: {}, + prompts: {}, + resources: {}, + }, + } + ); + } + + async connect() { + await this.client.connect(this.transport); + console.log('Connected to MCP server'); + } + + async listTools() { + return await this.client.listTools(); + } + + async callTool(name: string, args: Record) { + return await this.client.callTool({ + name, + arguments: args, + }); + } + + async disconnect() { + await this.transport.close(); + } +} diff --git a/src/nostr/announcer.ts b/src/nostr/announcer.ts new file mode 100644 index 0000000..f20475e --- /dev/null +++ b/src/nostr/announcer.ts @@ -0,0 +1,30 @@ +import { CONFIG } from '../config'; +import { RelayHandler } from './relay'; +import { keyManager } from './keys'; + +export class NostrAnnouncer { + private relayHandler: RelayHandler; + + constructor() { + this.relayHandler = new RelayHandler(CONFIG.nostr.relayUrls); + } + + async announceService() { + const event = keyManager.signEvent({ + ...keyManager.createEventTemplate(31990), + content: JSON.stringify({ + name: CONFIG.mcp.name, + about: CONFIG.mcp.about, + }), + tags: [ + ['d', Math.random().toString(36).substring(7)], + ['k', '5000'], + ['k', '5001'], + ['capabilities', 'mcp-1.0'], + ['t', 'mcp'], + ], + }); + + await this.relayHandler.publishEvent(event); + } +} diff --git a/src/nostr/keys.ts b/src/nostr/keys.ts new file mode 100644 index 0000000..24cdc4c --- /dev/null +++ b/src/nostr/keys.ts @@ -0,0 +1,33 @@ +import { hexToBytes } from '@noble/hashes/utils'; +import { getPublicKey, finalizeEvent } from 'nostr-tools/pure'; +import type { Event } from 'nostr-tools/pure'; +import { CONFIG } from '../config'; + +export type UnsignedEvent = Omit; + +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(); +}; + +export const keyManager = createKeyManager(CONFIG.nostr.privateKey); diff --git a/src/nostr/relay.ts b/src/nostr/relay.ts new file mode 100644 index 0000000..73585a4 --- /dev/null +++ b/src/nostr/relay.ts @@ -0,0 +1,59 @@ +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'; + +useWebSocketImplementation(WebSocket); + +export class RelayHandler { + private pool: SimplePool; + private relayUrls: string[]; + private subscriptions: SubCloser[] = []; + + constructor(relayUrls: string[]) { + this.pool = new SimplePool(); + this.relayUrls = relayUrls; + } + + async publishEvent(event: Event): Promise { + try { + await Promise.any(this.pool.publish(this.relayUrls, event)); + console.log('Event published:', event.id); + } catch (error) { + console.error('Failed to publish event:', error); + throw error; + } + } + + subscribeToRequests(onRequest: (event: Event) => void): SubCloser { + const filters: Filter[] = [ + { + kinds: [5000, 5001], + }, + ]; + + const sub = this.pool.subscribeMany(this.relayUrls, filters, { + onevent(event) { + onRequest(event); + }, + oneose() { + console.log('Reached end of stored events'); + }, + }); + + this.subscriptions.push(sub); + return sub; + } + + async queryEvents(filter: Filter): Promise { + return await this.pool.querySync(this.relayUrls, filter); + } + + cleanup() { + this.subscriptions.forEach((sub) => sub.close()); + this.subscriptions = []; + this.pool.close(this.relayUrls); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/tsconfig.json @@ -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 + } +}