mirror of
https://github.com/believethehype/nostrdvm.git
synced 2025-07-28 18:22:14 +02:00
add dvmcp MCP Server js
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -201,3 +201,5 @@ tests/gifs/lock.mdb
|
||||
tests/gif_library.py
|
||||
/.idea
|
||||
/.idea
|
||||
/tests/mcp/package-lock.json
|
||||
/tests/mcp/node_modules
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
@@ -18,7 +18,7 @@
|
||||
"mcp[cli]",
|
||||
"mcp",
|
||||
"run",
|
||||
"tests/mcp_server.py"
|
||||
"tests/mcp/mcp_server.py"
|
||||
]
|
||||
}
|
||||
}
|
@@ -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():
|
181
tests/mcp/nostr_dvmcp_server.js
Normal file
181
tests/mcp/nostr_dvmcp_server.js
Normal 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
21
tests/mcp/package.json
Normal 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"
|
||||
}
|
||||
}
|
@@ -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}"
|
@@ -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}"
|
Reference in New Issue
Block a user