mirror of
https://github.com/aljazceru/bakery.git
synced 2025-12-17 12:45:20 +01:00
add mcp profile action
This commit is contained in:
@@ -5,7 +5,7 @@ import config from "../config.js";
|
|||||||
import { normalizeToHexPubkey } from "../../helpers/nip19.js";
|
import { normalizeToHexPubkey } from "../../helpers/nip19.js";
|
||||||
import { requestLoader } from "../loaders.js";
|
import { requestLoader } from "../loaders.js";
|
||||||
|
|
||||||
server.resource("owner pubkey", "pubkey://owner", async (uri) => ({
|
server.resource("owner_pubkey", "pubkey://owner", async (uri) => ({
|
||||||
contents: [
|
contents: [
|
||||||
{
|
{
|
||||||
uri: uri.href,
|
uri: uri.href,
|
||||||
@@ -24,7 +24,7 @@ server.resource("config", "config://app", async (uri) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
server.resource(
|
server.resource(
|
||||||
"user profile",
|
"user_profile",
|
||||||
new ResourceTemplate("users://{pubkey}/profile", { list: undefined }),
|
new ResourceTemplate("users://{pubkey}/profile", { list: undefined }),
|
||||||
async (uri, { pubkey }) => {
|
async (uri, { pubkey }) => {
|
||||||
if (typeof pubkey !== "string") throw new Error("Pubkey must be a string");
|
if (typeof pubkey !== "string") throw new Error("Pubkey must be a string");
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
import { FollowUser, PinNote, UnfollowUser, UnpinNote, UpdateProfile } from "applesauce-actions/actions";
|
||||||
|
|
||||||
import server from "../server.js";
|
import server from "../server.js";
|
||||||
import { ownerActions, ownerPublish } from "../../owner.js";
|
import { ownerActions, ownerPublish } from "../../owner.js";
|
||||||
import { FollowUser, UnfollowUser } from "applesauce-actions/actions";
|
|
||||||
import { normalizeToHexPubkey } from "../../../helpers/nip19.js";
|
import { normalizeToHexPubkey } from "../../../helpers/nip19.js";
|
||||||
|
import eventCache from "../../event-cache.js";
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
"follow_user",
|
"follow_user",
|
||||||
"Adds another users pubkey to the owners following list",
|
"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 }) => {
|
async ({ pubkey }) => {
|
||||||
try {
|
try {
|
||||||
await ownerActions.exec(FollowUser, pubkey).forEach(ownerPublish);
|
await ownerActions.exec(FollowUser, pubkey).forEach(ownerPublish);
|
||||||
@@ -21,7 +28,12 @@ server.tool(
|
|||||||
server.tool(
|
server.tool(
|
||||||
"unfollow_user",
|
"unfollow_user",
|
||||||
"Removes another users pubkey from the owners following list",
|
"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 }) => {
|
async ({ pubkey }) => {
|
||||||
try {
|
try {
|
||||||
await ownerActions.exec(UnfollowUser, pubkey).forEach(ownerPublish);
|
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" }] };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
22
src/services/mcp/tools/draft.ts
Normal file
22
src/services/mcp/tools/draft.ts
Normal 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)),
|
||||||
|
);
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { NoteBlueprint } from "applesauce-factory/blueprints";
|
import { getProfileContent } from "applesauce-core/helpers";
|
||||||
import { EventTemplate } from "nostr-tools";
|
import { kinds } from "nostr-tools";
|
||||||
|
import { lastValueFrom, toArray } from "rxjs";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
import server from "../server.js";
|
import server from "../server.js";
|
||||||
@@ -8,7 +9,7 @@ import { ownerFactory } from "../../owner.js";
|
|||||||
import { rxNostr } from "../../rx-nostr.js";
|
import { rxNostr } from "../../rx-nostr.js";
|
||||||
import { requestLoader } from "../../loaders.js";
|
import { requestLoader } from "../../loaders.js";
|
||||||
import config from "../../config.js";
|
import config from "../../config.js";
|
||||||
import { lastValueFrom, toArray } from "rxjs";
|
import eventCache from "../../event-cache.js";
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
"sign_draft_event",
|
"sign_draft_event",
|
||||||
@@ -36,21 +37,25 @@ server.tool(
|
|||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
"publish_event",
|
"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({
|
event: z
|
||||||
|
.object({
|
||||||
created_at: z.number(),
|
created_at: z.number(),
|
||||||
content: z.string(),
|
content: z.string(),
|
||||||
tags: z.array(z.array(z.string())),
|
tags: z.array(z.array(z.string())),
|
||||||
kind: z.number(),
|
kind: z.number(),
|
||||||
sig: z.string(),
|
sig: z.string(),
|
||||||
pubkey: z.string().length(64),
|
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");
|
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 {
|
return {
|
||||||
content: results.map((result) => ({
|
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 {
|
return {
|
||||||
content: [{ type: "text", text: JSON.stringify(await ownerFactory.stamp(await draft)) }],
|
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(event) }],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
server.tool(
|
server.tool(
|
||||||
"short_text_note_draft",
|
"search_user_pubkey",
|
||||||
"Create a short text note draft event",
|
"Search for users pubkeys that match the query",
|
||||||
{
|
{ query: z.string(), limit: z.number().default(10) },
|
||||||
content: z.string(),
|
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)),
|
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user