add dvmcp MCP Server js

This commit is contained in:
dbth
2025-02-13 19:31:40 +01:00
parent 4520cef670
commit 792e7ae5d1
9 changed files with 226 additions and 82 deletions

2
.gitignore vendored
View File

@@ -201,3 +201,5 @@ tests/gifs/lock.mdb
tests/gif_library.py
/.idea
/.idea
/tests/mcp/package-lock.json
/tests/mcp/node_modules

View File

@@ -70,13 +70,15 @@ class MCPBridge(DVMTaskInterface):
if self.options.get("server_names"):
self.server_names = (self.options.get("server_names"))
c = "list-tools"
c = "execute-tool"
for tag in event.tags().to_vec():
if tag.as_vec()[0] == 'c':
if tag.as_vec()[0] == "c":
c = tag.as_vec()[1]
print(c)
content = event.content()
print(c)
print(content)
options = {
"command" : c,
@@ -110,7 +112,7 @@ class MCPBridge(DVMTaskInterface):
return json.dumps(final_tools)
elif options["command"] == "execute-tool":
else:
print(options["payload"])
ob = json.loads(options["payload"])
@@ -118,7 +120,7 @@ class MCPBridge(DVMTaskInterface):
tool_name = ob["name"]
tool_args = ob["parameters"]
tool_response = await self.call_tool(config_path, server_names, tool_name, tool_args)
print(tool_response)
return json.dumps(tool_response)
@@ -153,11 +155,11 @@ class MCPBridge(DVMTaskInterface):
tools = await send_tools_list(read_stream, write_stream)
if tools is not None:
alltools.append((server_name, tools))
raise Exception("I'm gonna leave you.")
raise BaseException()
else:
print("nada")
except:
except BaseException as e:
pass
print("Ignore the error. We're good.")
@@ -194,17 +196,18 @@ class MCPBridge(DVMTaskInterface):
server_has_tool = True
if server_has_tool is False:
print("no tool in server")
raise Exception()
raise BaseException()
else:
print(tool_args)
tool_response = await send_call_tool(
tool_name, tool_args, read_stream, write_stream)
raise Exception() # Until we find a better way to leave the async with
raise BaseException() # Until we find a better way to leave the async with
except:
pass
raise Exception()
except:
raise BaseException()
except BaseException as e:
pass
return tool_response

View File

@@ -48,7 +48,7 @@ async def nostr_client_test_mcp_execute_tool(tool_name, tool_parameters, dvm_pub
ptag = Tag.parse(["p", dvm_pubkey])
payload = {"name": tool_name,
"parameters": tool_parameters
"parameters": json.loads(tool_parameters)
}
event = EventBuilder(EventDefinitions.KIND_NIP90_MCP, json.dumps(payload)).tags(
@@ -99,15 +99,15 @@ async def nostr_client():
dvm_pubkey = "12e76a4504c09f0b4b02d8c7497525136c18520b23b9a035c998d23c817f381d"
dvm_pubkey = "85a319f3084cd23d3534d0ab2ddb80454df5dbd296d688257b4a336ed571d36a"
#await nostr_client_test_mcp_get_tools(dvm_pubkey=dvm_pubkey)
#await nostr_client_test_mcp_execute_tool(tool_name="get-crypto-price", tool_parameters={"symbol": "BTC"}, dvm_pubkey=dvm_pubkey)
await nostr_client_test_mcp_execute_tool(tool_name="get-crypto-price", tool_parameters=json.dumps({"symbol": "BTC"}), dvm_pubkey=dvm_pubkey)
#await nostr_client_test_mcp_execute_tool(tool_name="echo_tool", tool_parameters={"message": "Hello"}, dvm_pubkey=dvm_pubkey)
#await nostr_client_test_mcp_get_tools(dvm_pubkey=dvm_pubkey)
await nostr_client_test_mcp_execute_tool(tool_name="extract", tool_parameters={"url": "https://en.wikipedia.org/wiki/Nostr"}, dvm_pubkey=dvm_pubkey)
#await nostr_client_test_mcp_execute_tool(tool_name="extract", tool_parameters={"url": "https://en.wikipedia.org/wiki/Nostr"}, dvm_pubkey=dvm_pubkey)
class NotificationHandler(HandleNotification):
@@ -127,7 +127,7 @@ async def nostr_client():
if __name__ == '__main__':
env_path = Path('.env')
env_path = Path('../.env')
if env_path.is_file():
print(f'loading environment from {env_path.resolve()}')
dotenv.load_dotenv(env_path, verbose=True, override=True)

View File

@@ -18,7 +18,7 @@
"mcp[cli]",
"mcp",
"run",
"tests/mcp_server.py"
"tests/mcp/mcp_server.py"
]
}
}

View File

@@ -94,9 +94,9 @@ def playground(announce=False):
if __name__ == '__main__':
env_path = Path('.env')
env_path = Path('../.env')
if not env_path.is_file():
with open('.env', 'w') as f:
with open('../.env', 'w') as f:
print("Writing new .env file")
f.write('')
if env_path.is_file():

View File

@@ -0,0 +1,181 @@
import {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js";
import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js";
import {z} from "zod";
import {
Alphabet,
Client,
ClientBuilder,
Duration,
EventBuilder,
Filter,
Keys,
Kind,
loadWasmAsync,
loadWasmSync,
NostrSigner,
SingleLetterTag,
Tag
} from "@rust-nostr/nostr-sdk";
// To use in a config, such as in Claude Desktop
//
// "mcpServers": {
// "nostrdvmcp": {
// "command": "node",
// "args": [
// "<Path to this file>/nostr_dvmcp_server.js"
// ]
// }
// }
// }
//Basic config
const name = "nostr-mcp-server"
const version = "0.0.1"
const config = {
name: name,
version: version,
capabilities: {
logging: {},
},
timeout: 1000
}
const relays =
[ "wss://relay.nostrdvm.com",
"wss://nostr.mom",
"wss://nostr.oxtr.dev",
"wss://relay.damus.io",
];
// replace publickey with a unique one (it doesn't matter as much, we just do the requests)
let pkey = "e318cb3e6ac163814dd297c2c7d745faacfbc2a826eb4f6d6c81430426a83c2b"
// Create an MCP server for Stdio
const server = new McpServer(config);
//fetch nip89s of mcp servers TODO: allow to filter
await getnip89s()
//define getNip89s. Fetch from Nostr and add as tools to server.
async function getnip89s() {
await loadWasmAsync();
let keys = Keys.parse(pkey)
let signer = NostrSigner.keys(keys)
let client = new ClientBuilder().signer(signer).build()
for (const relay of relays) {
await client.addRelay(relay);
}
await client.connect();
const filter = new Filter().kind(new Kind(31990)).customTags(SingleLetterTag.lowercase(Alphabet.K), ["5910"])
let evts = await client.fetchEvents(filter, Duration.fromSecs(5))
for (let evt of evts.toVec()) {
let pubkey = evt.author.toHex()
let content_json = JSON.parse(evt.content)
let tools = content_json.tools
for (let tool of tools) {
if (tool.inputSchema === undefined || tool.inputSchema.properties === undefined) {
continue
}
//TODO convert inputSchema.properties? to zod schema or to any other way so it works.
let inputSchema = {symbol: z.string()}
server.tool(tool.name, tool.description, inputSchema,
async (args) => {
return await handle_dvm_request(args, tool.name, pubkey)
});
}
}
}
async function handle_dvm_request(args, name, pubkey) {
await loadWasmSync();
// Generate new random keys
let keys = Keys.parse(pkey)
let request_kind = new Kind(5910);
let response_kind = new Kind(6910);
let payload = {
"name": name,
"parameters": args
}
let signer = NostrSigner.keys(keys);
let client = new Client(signer);
for (const relay of relays) {
await client.addRelay(relay);
}
await client.connect();
var relays_list = merge(["relays"], relays)
let tags = [
Tag.parse(["c", "execute-tool"]),
Tag.parse(["p", pubkey]),
Tag.parse(["output", "application/json"]),
Tag.parse(relays_list)
]
let event = new EventBuilder(request_kind, JSON.stringify(payload)).tags(tags)
//send our request to the DVM
await client.sendEventBuilder(event);
let abortable
const filter = new Filter().pubkey(keys.publicKey).kind(response_kind).limit(0); // Limit set to 0 to get only new events! Timestamp.now() CAN'T be used for gift wrap since the timestamps are tweaked!
//listen for a response
await client.subscribe(filter);
var result = ""
const handle = {
handleEvent: async (relayUrl, subscriptionId, event) => {
//TODO More logic / safety checks
result = JSON.parse(event.content).content[0].text
abortable.abort()
return true
},
handleMsg: async (relayUrl, message) => {
//console.log("Received message from", relayUrl, message.asJson());
}
};
abortable = client.handleNotifications(handle)
//Wait till we have our response
await new Promise((resolve) => {
const checkAbort = setInterval(() => {
if (abortable.is_aborted()) {
clearInterval(checkAbort);
resolve();
}
}, 100);
});
// Then return the final result.
return {
content: [{ type: "text", text: `${result}` }]
};
}
//Connection
// Connect stdio
const transport = new StdioServerTransport();
await server.connect(transport);

21
tests/mcp/package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "vue",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.5.0",
"@rust-nostr/nostr-sdk": "0.39.0",
"zod": "^3.24.2"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.6.1"
}
}

View File

@@ -1,18 +0,0 @@
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Echo")
@mcp.resource("echo://{message}")
def echo_resource(message: str) -> str:
"""Echo a message as a resource"""
return f"Resource echo: {message}"
@mcp.tool()
def echo_tool(message: str) -> str:
"""Echo a message as a tool"""
return f"Tool echo: {message}"
@mcp.prompt()
def echo_prompt(message: str) -> str:
"""Create an echo prompt"""
return f"Please process this message: {message}"

View File

@@ -1,45 +0,0 @@
import json
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Nostr", dependencies=["nostr_dvm==1.1.0"])
@mcp.resource("echo://{message}")
def echo_resource(message: str) -> str:
"""Echo a message as a resource"""
return f"Resource echo: {message}"
@mcp.tool()
async def get_mcp_dvm_tool() -> str:
"""Fetch Tools from Data Vending Machines"""
from nostr_sdk import Keys, NostrSigner, Client
from nostr_dvm.utils.nip89_utils import nip89_fetch_all_dvms_by_kind
from nostr_dvm.utils.nostr_utils import check_and_set_private_key
keys = Keys.parse(check_and_set_private_key("test_client"))
signer = NostrSigner.keys(keys)
client = Client(signer)
await client.add_relay("wss://relay.damus.io")
await client.add_relay("wss://nostr.mom")
await client.add_relay("wss://nostr.oxtr.dev")
await client.add_relay("wss://relay.nostrdvm.com")
await client.connect()
nip89s = await nip89_fetch_all_dvms_by_kind(client, 5910)
tools = []
for announcement in nip89s:
print(announcement.as_json()["tools"])
tools.append(announcement.as_json()["tools"])
return str(tools)
@mcp.prompt()
def echo_prompt(message: str) -> str:
"""Create an echo prompt"""
return f"Please process this message: {message}"