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:
6
go.mod
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
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=
|
||||
|
1
main.go
1
main.go
@@ -38,6 +38,7 @@ var app = &cli.Command{
|
||||
decrypt,
|
||||
outbox,
|
||||
wallet,
|
||||
mcpServer,
|
||||
},
|
||||
Version: version,
|
||||
Flags: []cli.Flag{
|
||||
|
164
mcp.go
Normal file
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)
|
||||
},
|
||||
}
|
Reference in New Issue
Block a user