mirror of
https://github.com/aljazceru/dvmcp.git
synced 2025-12-17 05:14:24 +01:00
Init
This commit is contained in:
13
.env.example
Normal file
13
.env.example
Normal file
@@ -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
|
||||
179
.gitignore
vendored
Normal file
179
.gitignore
vendored
Normal file
@@ -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*
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 80,
|
||||
"bracketSpacing": true
|
||||
}
|
||||
|
||||
15
README.md
Normal file
15
README.md
Normal file
@@ -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.
|
||||
98
bun.lock
Normal file
98
bun.lock
Normal file
@@ -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=="],
|
||||
}
|
||||
}
|
||||
25
package.json
Normal file
25
package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
21
src/config.ts
Normal file
21
src/config.ts
Normal file
@@ -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(','),
|
||||
},
|
||||
};
|
||||
108
src/dvm-bridge.ts
Normal file
108
src/dvm-bridge.ts
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
49
src/mcp-client.ts
Normal file
49
src/mcp-client.ts
Normal file
@@ -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<string, any>) {
|
||||
return await this.client.callTool({
|
||||
name,
|
||||
arguments: args,
|
||||
});
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
await this.transport.close();
|
||||
}
|
||||
}
|
||||
30
src/nostr/announcer.ts
Normal file
30
src/nostr/announcer.ts
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
33
src/nostr/keys.ts
Normal file
33
src/nostr/keys.ts
Normal file
@@ -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<Event, 'sig' | 'id'>;
|
||||
|
||||
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);
|
||||
59
src/nostr/relay.ts
Normal file
59
src/nostr/relay.ts
Normal file
@@ -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<void> {
|
||||
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<Event[]> {
|
||||
return await this.pool.querySync(this.relayUrls, filter);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.subscriptions.forEach((sub) => sub.close());
|
||||
this.subscriptions = [];
|
||||
this.pool.close(this.relayUrls);
|
||||
}
|
||||
}
|
||||
27
tsconfig.json
Normal file
27
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