mirror of
https://github.com/fiatjaf/nak.git
synced 2026-01-28 21:18:54 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba9a5badc6 | ||
|
|
c0bbf73961 | ||
|
|
5320feee4f | ||
|
|
58c1fab0f0 | ||
|
|
5f30009e72 | ||
|
|
548918578b | ||
|
|
7757400ab3 | ||
|
|
3ee6320312 | ||
|
|
91474d65eb |
40
README.md
40
README.md
@@ -1,9 +1,19 @@
|
|||||||
# nak, the nostr army knife
|
# nak, the nostr army knife
|
||||||
|
|
||||||
install with `go install github.com/fiatjaf/nak@latest` or
|
install with this one-liner:
|
||||||
[download a binary](https://github.com/fiatjaf/nak/releases).
|
|
||||||
|
|
||||||
or get the source with `git clone https://github.com/fiatjaf/nak` then install with `go install` or run with docker using `docker build -t nak . && docker run nak event`.
|
```sh
|
||||||
|
curl -sSL https://raw.githubusercontent.com/fiatjaf/nak/master/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
- or install with `go install github.com/fiatjaf/nak@latest` if you have **Go** set up.
|
||||||
|
- or [download a binary](https://github.com/fiatjaf/nak/releases) manually.
|
||||||
|
- or get the source with `git clone https://github.com/fiatjaf/nak` then
|
||||||
|
- install with `go install`;
|
||||||
|
- or run with docker using `docker build -t nak . && docker run nak event`.
|
||||||
|
- or install with `brew install nak` if you use **macOS Homebrew**.
|
||||||
|
- or install with `paru -S nak-bin` or `yay -S nak-bin` if you are on **Arch Linux**.
|
||||||
|
- or install with `nix-env --install nak` if you use **Nix**.
|
||||||
|
|
||||||
## what can you do with it?
|
## what can you do with it?
|
||||||
|
|
||||||
@@ -34,7 +44,7 @@ publishing to wss://relay.damus.io... success.
|
|||||||
"Activando modo zen…\n\n#GM #Nostr #Hispano"
|
"Activando modo zen…\n\n#GM #Nostr #Hispano"
|
||||||
```
|
```
|
||||||
|
|
||||||
### decode a nip19 note1 code, add a relay hint, encode it back to nevent1
|
### decode a NIP-19 note1 code, add a relay hint, encode it back to nevent1
|
||||||
```shell
|
```shell
|
||||||
~> nak decode note1ttnnrw78wy0hs5fa59yj03yvcu2r4y0xetg9vh7uf4em39n604vsyp37f2 | jq -r .id | nak encode nevent -r nostr.zbd.gg
|
~> nak decode note1ttnnrw78wy0hs5fa59yj03yvcu2r4y0xetg9vh7uf4em39n604vsyp37f2 | jq -r .id | nak encode nevent -r nostr.zbd.gg
|
||||||
nevent1qqs94ee3h0rhz8mc2y76zjf8cjxvw9p6j8nv45zktlwy6uacjea86kgpzfmhxue69uhkummnw3ezu7nzvshxwec8zw8h7
|
nevent1qqs94ee3h0rhz8mc2y76zjf8cjxvw9p6j8nv45zktlwy6uacjea86kgpzfmhxue69uhkummnw3ezu7nzvshxwec8zw8h7
|
||||||
@@ -200,6 +210,18 @@ or give it a named profile:
|
|||||||
~> nak bunker --profile myself ...
|
~> nak bunker --profile myself ...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### send a `nostrconnect://` client URI to a running bunker
|
||||||
|
|
||||||
|
```shell
|
||||||
|
~> nak bunker connect 'nostrconnect://...'
|
||||||
|
```
|
||||||
|
|
||||||
|
or, if you're using a persisted profile
|
||||||
|
|
||||||
|
```shell
|
||||||
|
~> nak bunker connect --profile default 'nostrconnect://...'
|
||||||
|
```
|
||||||
|
|
||||||
### generate a NIP-70 protected event with a date set to two weeks ago and some multi-value tags
|
### generate a NIP-70 protected event with a date set to two weeks ago and some multi-value tags
|
||||||
```shell
|
```shell
|
||||||
~> nak event --ts 'two weeks ago' -t '-' -t 'e=f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a;wss://relay.whatever.com;root;a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208' -t 'p=a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208;wss://p-relay.com' -c 'I know the future'
|
~> nak event --ts 'two weeks ago' -t '-' -t 'e=f59911b561c37c90b01e9e5c2557307380835c83399756f4d62d8167227e420a;wss://relay.whatever.com;root;a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208' -t 'p=a9e0f110f636f3191644110c19a33448daf09d7cda9708a769e91b7e91340208;wss://p-relay.com' -c 'I know the future'
|
||||||
@@ -227,7 +249,7 @@ or give it a named profile:
|
|||||||
• events stored: 4, subscriptions opened: 1
|
• events stored: 4, subscriptions opened: 1
|
||||||
```
|
```
|
||||||
|
|
||||||
### enable negentropy (nip77) support in your development relay
|
### enable negentropy (NIP-77) support in your development relay
|
||||||
```shell
|
```shell
|
||||||
~> nak serve --negentropy
|
~> nak serve --negentropy
|
||||||
```
|
```
|
||||||
@@ -442,3 +464,11 @@ gitnostr.com... ok.
|
|||||||
1a851afaa70a26faa82c5b4422ce967c07e278efc56a1413b9719b662f86551a
|
1a851afaa70a26faa82c5b4422ce967c07e278efc56a1413b9719b662f86551a
|
||||||
8031621a54b2502f5bd4dbb87c971c0a69675d252a64d69e22224f3aee6dd2b2
|
8031621a54b2502f5bd4dbb87c971c0a69675d252a64d69e22224f3aee6dd2b2
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### interact with a NIP-29 group
|
||||||
|
```shell
|
||||||
|
~> nak group info "<relay>'<id>"
|
||||||
|
~> nak group admin "<relay>'<id>"
|
||||||
|
~> nak group chat "<relay>'<id>"
|
||||||
|
~> nak group chat send "<relay>'<id>" "<message>"
|
||||||
|
```
|
||||||
|
|||||||
10
bunker.go
10
bunker.go
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -347,6 +348,7 @@ var bunker = &cli.Command{
|
|||||||
}, nostr.SubscriptionOptions{Label: "nak-bunker"})
|
}, nostr.SubscriptionOptions{Label: "nak-bunker"})
|
||||||
|
|
||||||
signer := nip46.NewStaticKeySigner(sec)
|
signer := nip46.NewStaticKeySigner(sec)
|
||||||
|
signer.DefaultRelays = config.Relays
|
||||||
|
|
||||||
// unix socket nostrconnect:// handling
|
// unix socket nostrconnect:// handling
|
||||||
go func() {
|
go func() {
|
||||||
@@ -355,7 +357,7 @@ var bunker = &cli.Command{
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log("- got nostrconnect:// request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(clientPublicKey), uri.String())
|
log("- got nostrconnect:// request from '%s': %s\n", color.New(color.Bold, color.FgBlue).Sprint(clientPublicKey.Hex()), uri.String())
|
||||||
|
|
||||||
relays := uri.Query()["relay"]
|
relays := uri.Query()["relay"]
|
||||||
|
|
||||||
@@ -447,7 +449,11 @@ var bunker = &cli.Command{
|
|||||||
from := ie.Event.PubKey
|
from := ie.Event.PubKey
|
||||||
req, resp, eventResponse, err := signer.HandleRequest(ctx, ie.Event)
|
req, resp, eventResponse, err := signer.HandleRequest(ctx, ie.Event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log("< failed to handle request from %s: %s\n", from, err.Error())
|
if errors.Is(err, nip46.AlreadyHandled) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log("< failed to handle request from %s: %s\n", from.Hex(), err.Error())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ var decode = &cli.Command{
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = lineProcessingError(ctx, "couldn't decode input '%s'", input)
|
ctx = lineProcessingError(ctx, "couldn't decode input '%s': %s", input, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
exitIfLineProcessingError(ctx)
|
exitIfLineProcessingError(ctx)
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -3,7 +3,7 @@ module github.com/fiatjaf/nak
|
|||||||
go 1.25
|
go 1.25
|
||||||
|
|
||||||
require (
|
require (
|
||||||
fiatjaf.com/nostr v0.0.0-20260121154330-061cf7f68fd4
|
fiatjaf.com/nostr v0.0.0-20260126202222-ca3730e50817
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||||
github.com/bep/debounce v1.2.1
|
github.com/bep/debounce v1.2.1
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.3.6
|
github.com/btcsuite/btcd/btcec/v2 v2.3.6
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -1,7 +1,7 @@
|
|||||||
fiatjaf.com/lib v0.3.2 h1:RBS41z70d8Rp8e2nemQsbPY1NLLnEGShiY2c+Bom3+Q=
|
fiatjaf.com/lib v0.3.2 h1:RBS41z70d8Rp8e2nemQsbPY1NLLnEGShiY2c+Bom3+Q=
|
||||||
fiatjaf.com/lib v0.3.2/go.mod h1:UlHaZvPHj25PtKLh9GjZkUHRmQ2xZ8Jkoa4VRaLeeQ8=
|
fiatjaf.com/lib v0.3.2/go.mod h1:UlHaZvPHj25PtKLh9GjZkUHRmQ2xZ8Jkoa4VRaLeeQ8=
|
||||||
fiatjaf.com/nostr v0.0.0-20260121154330-061cf7f68fd4 h1:DF/4NSbCvXqIIRrwYp7L3S0SqC7/IhQl8mHkmYA5uXM=
|
fiatjaf.com/nostr v0.0.0-20260126202222-ca3730e50817 h1:Zp6rPetvwYFOLD+36RtmWmns2C0CLbtphD3DLu3cxCo=
|
||||||
fiatjaf.com/nostr v0.0.0-20260121154330-061cf7f68fd4/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
|
fiatjaf.com/nostr v0.0.0-20260126202222-ca3730e50817/go.mod h1:ue7yw0zHfZj23Ml2kVSdBx0ENEaZiuvGxs/8VEN93FU=
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||||
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
|
github.com/FastFilter/xorfilter v0.2.1 h1:lbdeLG9BdpquK64ZsleBS8B4xO/QW1IM0gMzF7KaBKc=
|
||||||
|
|||||||
73
install.sh
Executable file
73
install.sh
Executable file
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# detect OS
|
||||||
|
detect_os() {
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Linux*) echo "linux";;
|
||||||
|
Darwin*) echo "darwin";;
|
||||||
|
FreeBSD*) echo "freebsd";;
|
||||||
|
MINGW*|MSYS*|CYGWIN*) echo "windows";;
|
||||||
|
*)
|
||||||
|
echo "error: unsupported OS $(uname -s)" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# detect architecture
|
||||||
|
detect_arch() {
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64|amd64) echo "amd64";;
|
||||||
|
aarch64|arm64) echo "arm64";;
|
||||||
|
riscv64) echo "riscv64";;
|
||||||
|
*)
|
||||||
|
echo "error: unsupported architecture $(uname -m)" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# set install directory
|
||||||
|
INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}"
|
||||||
|
|
||||||
|
# detect platform
|
||||||
|
OS=$(detect_os)
|
||||||
|
ARCH=$(detect_arch)
|
||||||
|
|
||||||
|
echo "installing nak ($OS-$ARCH) to $INSTALL_DIR..."
|
||||||
|
|
||||||
|
# check if curl is available
|
||||||
|
command -v curl >/dev/null 2>&1 || { echo "error: curl is required" >&2; exit 1; }
|
||||||
|
|
||||||
|
# get latest release tag
|
||||||
|
RELEASE_INFO=$(curl -s https://api.github.com/repos/fiatjaf/nak/releases/latest)
|
||||||
|
TAG="${RELEASE_INFO#*\"tag_name\"}"
|
||||||
|
TAG="${TAG#*\"}"
|
||||||
|
TAG="${TAG%%\"*}"
|
||||||
|
|
||||||
|
[ -z "$TAG" ] && { echo "error: failed to fetch release info" >&2; exit 1; }
|
||||||
|
|
||||||
|
# construct download URL
|
||||||
|
BINARY_NAME="nak-${TAG}-${OS}-${ARCH}"
|
||||||
|
[ "$OS" = "windows" ] && BINARY_NAME="${BINARY_NAME}.exe"
|
||||||
|
DOWNLOAD_URL="https://github.com/fiatjaf/nak/releases/download/${TAG}/${BINARY_NAME}"
|
||||||
|
|
||||||
|
# create install directory and download
|
||||||
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
TARGET_PATH="$INSTALL_DIR/nak"
|
||||||
|
[ "$OS" = "windows" ] && TARGET_PATH="${TARGET_PATH}.exe"
|
||||||
|
|
||||||
|
if curl -sS -L -f -o "$TARGET_PATH" "$DOWNLOAD_URL"; then
|
||||||
|
chmod +x "$TARGET_PATH"
|
||||||
|
echo "installed nak $TAG to $TARGET_PATH"
|
||||||
|
|
||||||
|
# check if install dir is in PATH
|
||||||
|
case ":$PATH:" in
|
||||||
|
*":$INSTALL_DIR:"*) ;;
|
||||||
|
*) echo "note: add $INSTALL_DIR to your PATH" ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
echo "error: download failed from $DOWNLOAD_URL" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
104
mcp.go
104
mcp.go
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"fiatjaf.com/nostr"
|
"fiatjaf.com/nostr"
|
||||||
|
"fiatjaf.com/nostr/nip11"
|
||||||
"fiatjaf.com/nostr/nip19"
|
"fiatjaf.com/nostr/nip19"
|
||||||
"fiatjaf.com/nostr/sdk"
|
"fiatjaf.com/nostr/sdk"
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
@@ -33,10 +34,10 @@ var mcpServer = &cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("publish_note",
|
s.AddTool(mcp.NewTool("publish_note",
|
||||||
mcp.WithDescription("Publish a short note event to Nostr with the given text content"),
|
mcp.WithDescription("publish a short note event to Nostr with the given text content"),
|
||||||
mcp.WithString("content", mcp.Description("Arbitrary string to be published"), mcp.Required()),
|
mcp.WithString("content", mcp.Description("arbitrary string to be published"), mcp.Required()),
|
||||||
mcp.WithString("relay", mcp.Description("Relay to publish the note to")),
|
mcp.WithString("relay", mcp.Description("relay to publish the note to")),
|
||||||
mcp.WithString("mention", mcp.Description("Nostr user's public key to be mentioned")),
|
mcp.WithString("mention", mcp.Description("nostr user's public key to be mentioned")),
|
||||||
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
content := required[string](r, "content")
|
content := required[string](r, "content")
|
||||||
mention, _ := optional[string](r, "mention")
|
mention, _ := optional[string](r, "mention")
|
||||||
@@ -105,7 +106,7 @@ var mcpServer = &cli.Command{
|
|||||||
})
|
})
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("resolve_nostr_uri",
|
s.AddTool(mcp.NewTool("resolve_nostr_uri",
|
||||||
mcp.WithDescription("Resolve URIs prefixed with nostr:, including nostr:nevent1..., nostr:npub1..., nostr:nprofile1... and nostr:naddr1..."),
|
mcp.WithDescription("resolve URIs prefixed with nostr:, including nostr:nevent1..., nostr:npub1..., nostr:nprofile1... and nostr:naddr1..."),
|
||||||
mcp.WithString("uri", mcp.Description("URI to be resolved"), mcp.Required()),
|
mcp.WithString("uri", mcp.Description("URI to be resolved"), mcp.Required()),
|
||||||
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
uri := required[string](r, "uri")
|
uri := required[string](r, "uri")
|
||||||
@@ -136,23 +137,23 @@ var mcpServer = &cli.Command{
|
|||||||
WithRelays: false,
|
WithRelays: false,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError("Couldn't find this event anywhere"), nil
|
return mcp.NewToolResultError("couldn't find this event anywhere"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return mcp.NewToolResultText(
|
return mcp.NewToolResultText(
|
||||||
fmt.Sprintf("this is a Nostr event: %s", event),
|
fmt.Sprintf("this is a Nostr event: %s", event),
|
||||||
), nil
|
), nil
|
||||||
case "naddr":
|
case "naddr":
|
||||||
return mcp.NewToolResultError("For now we can't handle this kind of Nostr uri"), nil
|
return mcp.NewToolResultError("for now we can't handle this kind of Nostr uri"), nil
|
||||||
default:
|
default:
|
||||||
return mcp.NewToolResultError("We don't know how to handle this Nostr uri"), nil
|
return mcp.NewToolResultError("we don't know how to handle this Nostr uri"), nil
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("search_profile",
|
s.AddTool(mcp.NewTool("search_profile",
|
||||||
mcp.WithDescription("Search for the public key of a Nostr user given their name"),
|
mcp.WithDescription("search for the public key of a Nostr user given their name"),
|
||||||
mcp.WithString("name", mcp.Description("Name to be searched"), mcp.Required()),
|
mcp.WithString("name", mcp.Description("name to be searched"), mcp.Required()),
|
||||||
mcp.WithNumber("limit", mcp.Description("How many results to return")),
|
mcp.WithNumber("limit", mcp.Description("how many results to return")),
|
||||||
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
name := required[string](r, "name")
|
name := required[string](r, "name")
|
||||||
limit, _ := optional[float64](r, "limit")
|
limit, _ := optional[float64](r, "limit")
|
||||||
@@ -163,7 +164,7 @@ var mcpServer = &cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
res := strings.Builder{}
|
res := strings.Builder{}
|
||||||
res.WriteString("Search results: ")
|
res.WriteString("search results: ")
|
||||||
l := 0
|
l := 0
|
||||||
for result := range sys.Pool.FetchMany(ctx, []string{"relay.nostr.band", "nostr.wine"}, filter, nostr.SubscriptionOptions{
|
for result := range sys.Pool.FetchMany(ctx, []string{"relay.nostr.band", "nostr.wine"}, filter, nostr.SubscriptionOptions{
|
||||||
Label: "nak-mcp-search",
|
Label: "nak-mcp-search",
|
||||||
@@ -178,14 +179,14 @@ var mcpServer = &cli.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if l == 0 {
|
if l == 0 {
|
||||||
return mcp.NewToolResultError("Couldn't find anyone with that name."), nil
|
return mcp.NewToolResultError("couldn't find anyone with that name."), nil
|
||||||
}
|
}
|
||||||
return mcp.NewToolResultText(res.String()), nil
|
return mcp.NewToolResultText(res.String()), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("get_outbox_relay_for_pubkey",
|
s.AddTool(mcp.NewTool("get_outbox_relay_for_pubkey",
|
||||||
mcp.WithDescription("Get the best relay from where to read notes from a specific Nostr user"),
|
mcp.WithDescription("get the best relay from where to read notes from a specific Nostr user"),
|
||||||
mcp.WithString("pubkey", mcp.Description("Public key of Nostr user we want to know the relay from where to read"), mcp.Required()),
|
mcp.WithString("pubkey", mcp.Description("public key of Nostr user we want to know the relay from where to read"), mcp.Required()),
|
||||||
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
pubkey, err := nostr.PubKeyFromHex(required[string](r, "pubkey"))
|
pubkey, err := nostr.PubKeyFromHex(required[string](r, "pubkey"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -197,7 +198,7 @@ var mcpServer = &cli.Command{
|
|||||||
})
|
})
|
||||||
|
|
||||||
s.AddTool(mcp.NewTool("read_events_from_relay",
|
s.AddTool(mcp.NewTool("read_events_from_relay",
|
||||||
mcp.WithDescription("Makes a REQ query to one relay using the specified parameters, this can be used to fetch notes from a profile"),
|
mcp.WithDescription("makes a REQ query to one relay using the specified parameters, this can be used to fetch notes from a profile"),
|
||||||
mcp.WithString("relay", mcp.Description("relay URL to send the query to"), mcp.Required()),
|
mcp.WithString("relay", mcp.Description("relay URL to send the query to"), mcp.Required()),
|
||||||
mcp.WithNumber("kind", mcp.Description("event kind number to include in the 'kinds' field"), mcp.Required()),
|
mcp.WithNumber("kind", mcp.Description("event kind number to include in the 'kinds' field"), mcp.Required()),
|
||||||
mcp.WithNumber("limit", mcp.Description("maximum number of events to query"), mcp.Required()),
|
mcp.WithNumber("limit", mcp.Description("maximum number of events to query"), mcp.Required()),
|
||||||
@@ -238,6 +239,77 @@ var mcpServer = &cli.Command{
|
|||||||
return mcp.NewToolResultText(result.String()), nil
|
return mcp.NewToolResultText(result.String()), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
s.AddTool(mcp.NewTool("search_events",
|
||||||
|
mcp.WithDescription("search for Nostr events. specifying the author makes it so we'll try to use their relays instead of generic ones."),
|
||||||
|
mcp.WithString("search", mcp.Description("search query string"), mcp.Required()),
|
||||||
|
mcp.WithString("author", mcp.Description("author public key to filter by")),
|
||||||
|
mcp.WithNumber("kind", mcp.Description("event kind to filter by")),
|
||||||
|
mcp.WithNumber("limit", mcp.Description("maximum number of results to return")),
|
||||||
|
), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||||
|
search := required[string](r, "search")
|
||||||
|
author, hasAuthor := optional[string](r, "author")
|
||||||
|
kind, hasKind := optional[float64](r, "kind")
|
||||||
|
limit, _ := optional[float64](r, "limit")
|
||||||
|
if limit == 0 {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := nostr.Filter{Search: search, Limit: int(limit)}
|
||||||
|
if hasKind {
|
||||||
|
filter.Kinds = []nostr.Kind{nostr.Kind(int(kind))}
|
||||||
|
}
|
||||||
|
|
||||||
|
var relays []string
|
||||||
|
|
||||||
|
if hasAuthor {
|
||||||
|
if pk, err := nostr.PubKeyFromHex(author); err != nil {
|
||||||
|
return mcp.NewToolResultError("the author given isn't a valid public key, it must be 32 bytes hex. Got error: " + err.Error()), nil
|
||||||
|
} else {
|
||||||
|
filter.Authors = append(filter.Authors, pk)
|
||||||
|
}
|
||||||
|
|
||||||
|
pk, _ := nostr.PubKeyFromHex(author)
|
||||||
|
writeRelays := sys.FetchOutboxRelays(ctx, pk, 5)
|
||||||
|
|
||||||
|
for _, relayURL := range writeRelays {
|
||||||
|
if info, err := nip11.Fetch(ctx, relayURL); err == nil {
|
||||||
|
for _, nip := range info.SupportedNIPs {
|
||||||
|
if nipInt, ok := nip.(float64); ok && nipInt == 50 {
|
||||||
|
relays = append(relays, relayURL)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(relays) == 0 {
|
||||||
|
relays = []string{"relay.nostr.band", "nostr.polyserv.xyz/", "search.nos.today/"}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := strings.Builder{}
|
||||||
|
result.WriteString(fmt.Sprintf("search results for '%s':\n\n", search))
|
||||||
|
|
||||||
|
l := 0
|
||||||
|
for event := range sys.Pool.FetchMany(ctx, relays, filter, nostr.SubscriptionOptions{
|
||||||
|
Label: "nak-mcp-search",
|
||||||
|
}) {
|
||||||
|
l++
|
||||||
|
result.WriteString(fmt.Sprintf("result %d\nID: %s\nKind: %d\nAuthor: %s\nContent: %s\n---\n",
|
||||||
|
l, event.ID, event.Kind, event.PubKey.Hex(), event.Content))
|
||||||
|
|
||||||
|
if l >= int(limit) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if l == 0 {
|
||||||
|
return mcp.NewToolResultError("no events found matching the search criteria."), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcp.NewToolResultText(result.String()), nil
|
||||||
|
})
|
||||||
|
|
||||||
return server.ServeStdio(s)
|
return server.ServeStdio(s)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user