Compare commits

..

3 Commits

Author SHA1 Message Date
franzap
6daf0f4e5c Update zapstore.yaml 2025-05-20 23:15:31 -03:00
Alex Gleason
bfeaf0710f Allow --prompt-sec to be used with pipes 2025-05-03 07:27:37 -03:00
fiatjaf
e45b54ea62 fix nak mcp. 2025-04-10 16:59:56 -03:00
3 changed files with 109 additions and 97 deletions

View File

@@ -15,6 +15,7 @@ import (
"github.com/nbd-wtf/go-nostr/nip19" "github.com/nbd-wtf/go-nostr/nip19"
"github.com/nbd-wtf/go-nostr/nip46" "github.com/nbd-wtf/go-nostr/nip46"
"github.com/nbd-wtf/go-nostr/nip49" "github.com/nbd-wtf/go-nostr/nip49"
"golang.org/x/term"
) )
var defaultKeyFlags = []cli.Flag{ var defaultKeyFlags = []cli.Flag{
@@ -84,9 +85,6 @@ func gatherSecretKeyOrBunkerFromArguments(ctx context.Context, c *cli.Command) (
} }
if c.Bool("prompt-sec") { if c.Bool("prompt-sec") {
if isPiped() {
return "", nil, fmt.Errorf("can't prompt for a secret key when processing data from a pipe, try again without --prompt-sec")
}
sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil) sec, err = askPassword("type your secret key as ncryptsec, nsec or hex: ", nil)
if err != nil { if err != nil {
return "", nil, fmt.Errorf("failed to get secret key: %w", err) return "", nil, fmt.Errorf("failed to get secret key: %w", err)
@@ -133,29 +131,59 @@ func promptDecrypt(ncryptsec string) (string, error) {
} }
func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) { func askPassword(msg string, shouldAskAgain func(answer string) bool) (string, error) {
config := &readline.Config{ if isPiped() {
Stdout: color.Error, // Use TTY method when stdin is piped
Prompt: color.YellowString(msg), tty, err := os.Open("/dev/tty")
InterruptPrompt: "^C", if err != nil {
DisableAutoSaveHistory: true, return "", fmt.Errorf("can't prompt for a secret key when processing data from a pipe on this system (failed to open /dev/tty: %w), try again without --prompt-sec or provide the key via --sec or NOSTR_SECRET_KEY environment variable", err)
EnableMask: true, }
MaskRune: '*', defer tty.Close()
}
rl, err := readline.NewEx(config) for {
if err != nil { // Print the prompt to stderr so it's visible to the user
return "", err fmt.Fprintf(color.Error, color.YellowString(msg))
}
for { // Read password from TTY with masking
answer, err := rl.Readline() password, err := term.ReadPassword(int(tty.Fd()))
if err != nil {
return "", err
}
// Print newline after password input
fmt.Fprintln(color.Error)
answer := strings.TrimSpace(string(password))
if shouldAskAgain != nil && shouldAskAgain(answer) {
continue
}
return answer, nil
}
} else {
// Use normal readline method when stdin is not piped
config := &readline.Config{
Stdout: color.Error,
Prompt: color.YellowString(msg),
InterruptPrompt: "^C",
DisableAutoSaveHistory: true,
EnableMask: true,
MaskRune: '*',
}
rl, err := readline.NewEx(config)
if err != nil { if err != nil {
return "", err return "", err
} }
answer = strings.TrimSpace(answer)
if shouldAskAgain != nil && shouldAskAgain(answer) { for {
continue answer, err := rl.Readline()
if err != nil {
return "", err
}
answer = strings.TrimSpace(answer)
if shouldAskAgain != nil && shouldAskAgain(answer) {
continue
}
return answer, err
} }
return answer, err
} }
} }

115
mcp.go
View File

@@ -28,25 +28,13 @@ 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("relay", mcp.WithString("content", mcp.Description("Arbitrary string to be published"), mcp.Required()),
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("content", ), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
mcp.Required(), content := required[string](r, "content")
mcp.Description("Arbitrary string to be published"), mention, _ := optional[string](r, "mention")
), relay, _ := optional[string](r, "relay")
mcp.WithString("mention",
mcp.Required(),
mcp.Description("Nostr user's public key to be mentioned"),
),
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
content, _ := request.Params.Arguments["content"].(string)
mention, _ := request.Params.Arguments["mention"].(string)
relayI, ok := request.Params.Arguments["relay"]
var relay string
if ok {
relay, _ = relayI.(string)
}
if mention != "" && !nostr.IsValidPublicKey(mention) { if mention != "" && !nostr.IsValidPublicKey(mention) {
return mcp.NewToolResultError("the given mention isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile"), nil return mcp.NewToolResultError("the given mention isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile"), nil
@@ -81,7 +69,9 @@ var mcpServer = &cli.Command{
} }
// extra relay specified // extra relay specified
relays = append(relays, relay) if relay != "" {
relays = append(relays, relay)
}
result := strings.Builder{} result := strings.Builder{}
result.WriteString( result.WriteString(
@@ -111,12 +101,9 @@ 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.WithString("uri", mcp.Description("URI to be resolved"), mcp.Required()),
mcp.Required(), ), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
mcp.Description("URI to be resolved"), uri := required[string](r, "uri")
),
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
uri, _ := request.Params.Arguments["uri"].(string)
if strings.HasPrefix(uri, "nostr:") { if strings.HasPrefix(uri, "nostr:") {
uri = uri[6:] uri = uri[6:]
} }
@@ -159,12 +146,9 @@ var mcpServer = &cli.Command{
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.WithString("name", mcp.Description("Name to be searched"), mcp.Required()),
mcp.Required(), ), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
mcp.Description("Name to be searched"), name := required[string](r, "name")
),
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
name, _ := request.Params.Arguments["name"].(string)
re := sys.Pool.QuerySingle(ctx, []string{"relay.nostr.band", "nostr.wine"}, nostr.Filter{Search: name, Kinds: []int{0}}) re := sys.Pool.QuerySingle(ctx, []string{"relay.nostr.band", "nostr.wine"}, nostr.Filter{Search: name, Kinds: []int{0}})
if re == nil { if re == nil {
return mcp.NewToolResultError("couldn't find anyone with that name"), nil return mcp.NewToolResultError("couldn't find anyone with that name"), nil
@@ -175,42 +159,24 @@ var mcpServer = &cli.Command{
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.WithString("pubkey", mcp.Description("Public key of Nostr user we want to know the relay from where to read"), mcp.Required()),
mcp.Required(), ), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
mcp.Description("Public key of Nostr user we want to know the relay from where to read"), pubkey := required[string](r, "pubkey")
),
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
pubkey, _ := request.Params.Arguments["pubkey"].(string)
res := sys.FetchOutboxRelays(ctx, pubkey, 1) res := sys.FetchOutboxRelays(ctx, pubkey, 1)
return mcp.NewToolResultText(res[0]), nil return mcp.NewToolResultText(res[0]), nil
}) })
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.WithNumber("kind", mcp.WithString("relay", mcp.Description("relay URL to send the query to"), mcp.Required()),
mcp.Required(), mcp.WithNumber("kind", mcp.Description("event kind number to include in the 'kinds' field"), mcp.Required()),
mcp.Description("event kind number to include in the 'kinds' field"), mcp.WithNumber("limit", mcp.Description("maximum number of events to query"), mcp.Required()),
), mcp.WithString("pubkey", mcp.Description("pubkey to include in the 'authors' field")),
mcp.WithString("pubkey", ), func(ctx context.Context, r mcp.CallToolRequest) (*mcp.CallToolResult, error) {
mcp.Description("pubkey to include in the 'authors' field"), relay := required[string](r, "relay")
), kind := int(required[float64](r, "kind"))
mcp.WithNumber("limit", limit := int(required[float64](r, "limit"))
mcp.Required(), pubkey, _ := optional[string](r, "pubkey")
mcp.Description("maximum number of events to query"),
),
mcp.WithString("relay",
mcp.Required(),
mcp.Description("relay URL to send the query to"),
),
), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
relay, _ := request.Params.Arguments["relay"].(string)
limit, _ := request.Params.Arguments["limit"].(int)
kind, _ := request.Params.Arguments["kind"].(int)
pubkeyI, ok := request.Params.Arguments["pubkey"]
var pubkey string
if ok {
pubkey, _ = pubkeyI.(string)
}
if pubkey != "" && !nostr.IsValidPublicKey(pubkey) { if pubkey != "" && !nostr.IsValidPublicKey(pubkey) {
return mcp.NewToolResultError("the given pubkey isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile"), nil return mcp.NewToolResultError("the given pubkey isn't a valid public key, it must be 32 bytes hex, like the ones returned by search_profile"), nil
@@ -242,3 +208,28 @@ var mcpServer = &cli.Command{
return server.ServeStdio(s) return server.ServeStdio(s)
}, },
} }
func required[T comparable](r mcp.CallToolRequest, p string) T {
var zero T
if _, ok := r.Params.Arguments[p]; !ok {
return zero
}
if _, ok := r.Params.Arguments[p].(T); !ok {
return zero
}
if r.Params.Arguments[p].(T) == zero {
return zero
}
return r.Params.Arguments[p].(T)
}
func optional[T any](r mcp.CallToolRequest, p string) (T, bool) {
var zero T
if _, ok := r.Params.Arguments[p]; !ok {
return zero, false
}
if _, ok := r.Params.Arguments[p].(T); !ok {
return zero, false
}
return r.Params.Arguments[p].(T), true
}

View File

@@ -1,14 +1,7 @@
nak: repository: https://github.com/fiatjaf/nak
cli: assets:
name: nak - nak-v\d+\.\d+\.\d+-darwin-arm64
summary: a command line tool for doing all things nostr - nak-v\d+\.\d+\.\d+-linux-amd64
repository: https://github.com/fiatjaf/nak - nak-v\d+\.\d+\.\d+-linux-arm64
artifacts: remote_metadata:
nak-v%v-darwin-arm64: - github
platforms: [darwin-arm64]
nak-v%v-darwin-amd64:
platforms: [darwin-x86_64]
nak-v%v-linux-arm64:
platforms: [linux-aarch64]
nak-v%v-linux-amd64:
platforms: [linux-x86_64]