add mcp profile action

This commit is contained in:
hzrd149
2025-03-27 21:22:15 +00:00
parent a1507d2309
commit 5ccb7058a4
4 changed files with 147 additions and 28 deletions

View File

@@ -5,7 +5,7 @@ import config from "../config.js";
import { normalizeToHexPubkey } from "../../helpers/nip19.js";
import { requestLoader } from "../loaders.js";
server.resource("owner pubkey", "pubkey://owner", async (uri) => ({
server.resource("owner_pubkey", "pubkey://owner", async (uri) => ({
contents: [
{
uri: uri.href,
@@ -24,7 +24,7 @@ server.resource("config", "config://app", async (uri) => ({
}));
server.resource(
"user profile",
"user_profile",
new ResourceTemplate("users://{pubkey}/profile", { list: undefined }),
async (uri, { pubkey }) => {
if (typeof pubkey !== "string") throw new Error("Pubkey must be a string");

View File

@@ -1,13 +1,20 @@
import z from "zod";
import { FollowUser, PinNote, UnfollowUser, UnpinNote, UpdateProfile } from "applesauce-actions/actions";
import server from "../server.js";
import { ownerActions, ownerPublish } from "../../owner.js";
import { FollowUser, UnfollowUser } from "applesauce-actions/actions";
import { normalizeToHexPubkey } from "../../../helpers/nip19.js";
import eventCache from "../../event-cache.js";
server.tool(
"follow_user",
"Adds another users pubkey to the owners following list",
{ pubkey: z.string().transform((hex) => normalizeToHexPubkey(hex, true)) },
{
pubkey: z
.string()
.transform((hex) => normalizeToHexPubkey(hex, true))
.describe("The pubkey of the user to follow"),
},
async ({ pubkey }) => {
try {
await ownerActions.exec(FollowUser, pubkey).forEach(ownerPublish);
@@ -21,7 +28,12 @@ server.tool(
server.tool(
"unfollow_user",
"Removes another users pubkey from the owners following list",
{ pubkey: z.string().transform((hex) => normalizeToHexPubkey(hex, true)) },
{
pubkey: z
.string()
.transform((hex) => normalizeToHexPubkey(hex, true))
.describe("The pubkey of the user to unfollow"),
},
async ({ pubkey }) => {
try {
await ownerActions.exec(UnfollowUser, pubkey).forEach(ownerPublish);
@@ -31,3 +43,46 @@ server.tool(
}
},
);
server.tool(
"pin_note",
"Pins a kind 1 note to the owners pinned notes list",
{
id: z.string().describe("The event id of the note to pin"),
},
async ({ id }) => {
const event = (await eventCache.getEventsForFilters([{ ids: [id] }]))[0];
if (!event) throw new Error("Cant find note with id: " + id);
await ownerActions.exec(PinNote, event).forEach(ownerPublish);
return { content: [{ type: "text", text: "Pinned note" }] };
},
);
server.tool(
"unpin_note",
"Unpins a kind 1 note from the owners pinned notes list",
{
id: z.string().describe("The event id of the note to unpin"),
},
async ({ id }) => {
const event = (await eventCache.getEventsForFilters([{ ids: [id] }]))[0];
if (!event) throw new Error("Cant find note with id: " + id);
await ownerActions.exec(UnpinNote, event).forEach(ownerPublish);
return { content: [{ type: "text", text: "Unpinned note" }] };
},
);
server.tool(
"set_profile_field",
"Sets a field in the owners profile",
{
field: z.enum(["name", "about", "picture", "nip05", "website"]).describe("The field to set"),
value: z.string().describe("The value to set the field to"),
},
async ({ field, value }) => {
await ownerActions.exec(UpdateProfile, { [field]: value }).forEach(ownerPublish);
return { content: [{ type: "text", text: "Set profile field" }] };
},
);

View File

@@ -0,0 +1,22 @@
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { NoteBlueprint } from "applesauce-factory/blueprints";
import { EventTemplate } from "nostr-tools";
import { z } from "zod";
import server from "../server.js";
import { ownerFactory } from "../../owner.js";
async function returnUnsigned(draft: EventTemplate | Promise<EventTemplate>): Promise<CallToolResult> {
return {
content: [{ type: "text", text: JSON.stringify(await ownerFactory.stamp(await draft)) }],
};
}
server.tool(
"short_text_note_draft",
"Create a short text note draft event",
{
content: z.string(),
},
async ({ content }) => returnUnsigned(ownerFactory.create(NoteBlueprint, content)),
);

View File

@@ -1,6 +1,7 @@
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { NoteBlueprint } from "applesauce-factory/blueprints";
import { EventTemplate } from "nostr-tools";
import { getProfileContent } from "applesauce-core/helpers";
import { kinds } from "nostr-tools";
import { lastValueFrom, toArray } from "rxjs";
import z from "zod";
import server from "../server.js";
@@ -8,7 +9,7 @@ import { ownerFactory } from "../../owner.js";
import { rxNostr } from "../../rx-nostr.js";
import { requestLoader } from "../../loaders.js";
import config from "../../config.js";
import { lastValueFrom, toArray } from "rxjs";
import eventCache from "../../event-cache.js";
server.tool(
"sign_draft_event",
@@ -36,21 +37,25 @@ server.tool(
server.tool(
"publish_event",
"Publishes an event to the owners outbox relays",
"Publishes a signed nostr event to the relays or the users outbox relays",
{
event: z.object({
created_at: z.number(),
content: z.string(),
tags: z.array(z.array(z.string())),
kind: z.number(),
sig: z.string(),
pubkey: z.string().length(64),
}),
event: z
.object({
created_at: z.number(),
content: z.string(),
tags: z.array(z.array(z.string())),
kind: z.number(),
sig: z.string(),
pubkey: z.string().length(64),
})
.describe("The nostr event to publish"),
relays: z.array(z.string().url()).optional().describe("An array of relays to publish to"),
},
async ({ event }) => {
async ({ event, relays }) => {
if (!config.data.owner) throw new Error("Owner not set");
const mailboxes = await requestLoader.mailboxes({ pubkey: config.data.owner });
const results = await lastValueFrom(rxNostr.send(event, { on: { relays: mailboxes.outboxes } }).pipe(toArray()));
relays = relays || (await requestLoader.mailboxes({ pubkey: config.data.owner })).outboxes;
const results = await lastValueFrom(rxNostr.send(event, { on: { relays } }).pipe(toArray()));
return {
content: results.map((result) => ({
@@ -61,17 +66,54 @@ server.tool(
},
);
async function returnUnsigned(draft: EventTemplate | Promise<EventTemplate>): Promise<CallToolResult> {
server.tool(
"search_events",
"Search for events of a certain kind that contain the query",
{ query: z.string(), kind: z.number().default(1), limit: z.number().default(50) },
async ({ query, kind, limit }) => {
const events = await eventCache.getEventsForFilters([{ kinds: [kind], limit, search: query }]);
return {
content: events.map((event) => ({ type: "text", text: JSON.stringify(event) })),
};
},
);
// TODO: this needs to accept naddr, and nevent
server.tool("get_event", "Get an event by id", { id: z.string().length(64) }, async ({ id }) => {
const event = await eventCache.getEventsForFilters([{ ids: [id] }]);
return {
content: [{ type: "text", text: JSON.stringify(await ownerFactory.stamp(await draft)) }],
content: [{ type: "text", text: JSON.stringify(event) }],
};
}
});
server.tool(
"short_text_note_draft",
"Create a short text note draft event",
{
content: z.string(),
"search_user_pubkey",
"Search for users pubkeys that match the query",
{ query: z.string(), limit: z.number().default(10) },
async ({ query, limit }) => {
const profiles = await eventCache.getEventsForFilters([{ search: query, kinds: [kinds.Metadata], limit }]);
return {
content: profiles.map((profile) => {
const content = getProfileContent(profile);
const text = [
`Pubkey: ${profile.pubkey}`,
content.name && `Name: ${content.name}`,
content.about && `About: ${content.about}`,
content.picture && `Picture: ${content.picture}`,
content.nip05 && `NIP-05: ${content.nip05}`,
content.website && `Website: ${content.website}`,
]
.filter(Boolean)
.join("\n");
return {
type: "text",
text,
} satisfies CallToolResult["content"][number];
}),
};
},
async ({ content }) => returnUnsigned(ownerFactory.create(NoteBlueprint, content)),
);