1
0
mirror of https://github.com/fiatjaf/nak.git synced 2025-08-06 03:48:27 -04:00

experimental mcp server.

This commit is contained in:
fiatjaf
2025-01-31 23:44:03 -03:00
parent 12a1f1563e
commit dba3f648ad
4 changed files with 174 additions and 1 deletions

6
go.mod

@@ -1,6 +1,8 @@
module github.com/fiatjaf/nak
go 1.23.1
go 1.23.3
toolchain go1.23.4
require (
github.com/bep/debounce v1.2.1
@@ -13,6 +15,7 @@ require (
github.com/fiatjaf/khatru v0.15.0
github.com/json-iterator/go v1.1.12
github.com/mailru/easyjson v0.9.0
github.com/mark3labs/mcp-go v0.8.3
github.com/markusmobius/go-dateparser v1.2.3
github.com/nbd-wtf/go-nostr v0.49.2
)
@@ -34,6 +37,7 @@ require (
github.com/elnosh/gonuts v0.3.1-0.20250123162555-7c0381a585e3 // indirect
github.com/fasthttp/websocket v1.5.7 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/graph-gophers/dataloader/v7 v7.1.0 // indirect
github.com/hablullah/go-hijri v1.0.2 // indirect
github.com/hablullah/go-juliandays v1.0.0 // indirect

4
go.sum

@@ -88,6 +88,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc=
github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q=
@@ -112,6 +114,8 @@ github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mark3labs/mcp-go v0.8.3 h1:IzlyN8BaP4YwUMUDqxOGJhGdZXEDQiAPX43dNPgnzrg=
github.com/mark3labs/mcp-go v0.8.3/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE=
github.com/markusmobius/go-dateparser v1.2.3 h1:TvrsIvr5uk+3v6poDjaicnAFJ5IgtFHgLiuMY2Eb7Nw=
github.com/markusmobius/go-dateparser v1.2.3/go.mod h1:cMwQRrBUQlK1UI5TIFHEcvpsMbkWrQLXuaPNMFzuYLk=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=

@@ -38,6 +38,7 @@ var app = &cli.Command{
decrypt,
outbox,
wallet,
mcpServer,
},
Version: version,
Flags: []cli.Flag{

164
mcp.go Normal file

@@ -0,0 +1,164 @@
package main
import (
"context"
"fmt"
"os"
"strings"
"github.com/fiatjaf/cli/v3"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/nbd-wtf/go-nostr"
)
var mcpServer = &cli.Command{
Name: "mcp",
Usage: "pander to the AI gods",
Description: ``,
DisableSliceFlagSeparator: true,
Flags: []cli.Flag{},
Action: func(ctx context.Context, c *cli.Command) error {
s := server.NewMCPServer(
"nak",
version,
)
s.AddTool(mcp.NewTool("publish_note",
mcp.WithDescription("Publish a short note event to Nostr with the given text content"),
mcp.WithString("content",
mcp.Required(),
mcp.Description("Arbitrary string to be published"),
),
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)
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
}
sk := os.Getenv("NOSTR_SECRET_KEY")
if sk == "" {
sk = "0000000000000000000000000000000000000000000000000000000000000001"
}
var relays []string
evt := nostr.Event{
Kind: 1,
Tags: nostr.Tags{{"client", "goose/nak"}},
Content: content,
CreatedAt: nostr.Now(),
}
if mention != "" {
evt.Tags = append(evt.Tags, nostr.Tag{"p", mention})
// their inbox relays
relays = sys.FetchInboxRelays(ctx, mention, 3)
}
evt.Sign(sk)
// our write relays
relays = append(relays, sys.FetchOutboxRelays(ctx, evt.PubKey, 3)...)
if len(relays) == 0 {
relays = []string{"nos.lol", "relay.damus.io"}
}
for res := range sys.Pool.PublishMany(ctx, []string{"nos.lol"}, evt) {
if res.Error != nil {
return mcp.NewToolResultError(
fmt.Sprintf("there was an error publishing the event to the relay %s",
res.RelayURL),
), nil
}
}
return mcp.NewToolResultText("event was successfully published with id " + evt.ID), nil
})
s.AddTool(mcp.NewTool("search_profile",
mcp.WithDescription("Search for the public key of a Nostr user given their name"),
mcp.WithString("name",
mcp.Required(),
mcp.Description("Name to be searched"),
),
), 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}})
if re == nil {
return mcp.NewToolResultError("couldn't find anyone with that name"), nil
}
return mcp.NewToolResultText(re.PubKey), nil
})
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.WithString("pubkey",
mcp.Required(),
mcp.Description("Public key of Nostr user we want to know the relay from where to read"),
),
), 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}})
if re == nil {
return mcp.NewToolResultError("couldn't find anyone with that name"), nil
}
return mcp.NewToolResultText(re.PubKey), nil
})
s.AddTool(mcp.NewTool("read_events_from_relay",
mcp.WithDescription("Makes a REQ query to one relay using the specified parameters"),
mcp.WithNumber("kind",
mcp.Required(),
mcp.Description("event kind number to include in the 'kinds' field"),
),
mcp.WithString("pubkey",
mcp.Description("pubkey to include in the 'authors' field"),
),
mcp.WithNumber("limit",
mcp.Required(),
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)
pubkey, _ := request.Params.Arguments["pubkey"].(string)
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
}
filter := nostr.Filter{
Limit: limit,
Kinds: []int{kind},
}
if pubkey != "" {
filter.Authors = []string{pubkey}
}
events := sys.Pool.SubManyEose(ctx, []string{relay}, nostr.Filters{filter})
results := make([]string, 0, limit)
for ie := range events {
results = append(results, ie.String())
}
return mcp.NewToolResultText(strings.Join(results, "\n\n")), nil
})
return server.ServeStdio(s)
},
}