From 27b412abcbdfb01ee4542f7040d38038bb57291e Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Fri, 28 Mar 2025 09:50:53 +0000 Subject: [PATCH] fix small bugs with setup tools --- src/index.ts | 9 ++- src/services/mcp/index.ts | 4 +- src/services/mcp/resources.ts | 10 +-- src/services/mcp/server.ts | 4 +- src/services/mcp/tools/actions.ts | 12 ++-- src/services/mcp/tools/config.ts | 6 +- src/services/mcp/tools/connection.ts | 6 +- src/services/mcp/tools/database.ts | 6 +- src/services/mcp/tools/draft.ts | 4 +- src/services/mcp/tools/events.ts | 16 ++--- src/services/mcp/tools/signer.ts | 102 ++++++++++++++++++++------- src/services/owner.ts | 1 + 12 files changed, 114 insertions(+), 66 deletions(-) diff --git a/src/index.ts b/src/index.ts index bd94e7d..b52a81b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,12 +2,14 @@ import "./polyfill.js"; import process from "node:process"; import path from "node:path"; - import express, { Request } from "express"; import dayjs from "dayjs"; import duration from "dayjs/plugin/duration.js"; import localizedFormat from "dayjs/plugin/localizedFormat.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import mcpServer from "./services/mcp/index.js"; + import App from "./app/index.js"; import { PUBLIC_ADDRESS, IS_MCP } from "./env.js"; import { addListener, logger } from "./logger.js"; @@ -72,12 +74,9 @@ await app.start(); // Setup MCP interface on stdio if (IS_MCP) { - const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js"); - const { default: server } = await import("./services/mcp/index.js"); - // connect MCP server to stdio const transport = new StdioServerTransport(); - await server.connect(transport); + await mcpServer.connect(transport); } // shutdown process diff --git a/src/services/mcp/index.ts b/src/services/mcp/index.ts index 1ad5307..7e9f34d 100644 --- a/src/services/mcp/index.ts +++ b/src/services/mcp/index.ts @@ -1,5 +1,5 @@ import "./resources.js"; import "./tools/index.js"; -import server from "./server.js"; -export default server; +import mcpServer from "./server.js"; +export default mcpServer; diff --git a/src/services/mcp/resources.ts b/src/services/mcp/resources.ts index 5edf2bf..3f2424b 100644 --- a/src/services/mcp/resources.ts +++ b/src/services/mcp/resources.ts @@ -1,12 +1,12 @@ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; -import server from "./server.js"; +import mcpServer from "./server.js"; import bakeryConfig from "../config.js"; import { normalizeToHexPubkey } from "../../helpers/nip19.js"; import { requestLoader } from "../loaders.js"; import { kinds } from "nostr-tools"; -server.resource("owner_pubkey", "pubkey://owner", async (uri) => ({ +mcpServer.resource("owner_pubkey", "pubkey://owner", async (uri) => ({ contents: [ { uri: uri.href, @@ -15,7 +15,7 @@ server.resource("owner_pubkey", "pubkey://owner", async (uri) => ({ ], })); -server.resource("config", "config://app", async (uri) => ({ +mcpServer.resource("config", "config://app", async (uri) => ({ contents: [ { uri: uri.href, @@ -24,7 +24,7 @@ server.resource("config", "config://app", async (uri) => ({ ], })); -server.resource( +mcpServer.resource( "user_profile", new ResourceTemplate("users://{pubkey}/profile", { list: undefined }), async (uri, { pubkey }) => { @@ -44,7 +44,7 @@ server.resource( }, ); -server.resource("event_kinds", "nostr://kinds", async (uri) => { +mcpServer.resource("event_kinds", "nostr://kinds", async (uri) => { return { contents: [{ uri: uri.href, text: JSON.stringify(kinds) }], }; diff --git a/src/services/mcp/server.ts b/src/services/mcp/server.ts index 9fc054f..5f447b9 100644 --- a/src/services/mcp/server.ts +++ b/src/services/mcp/server.ts @@ -1,8 +1,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -const server = new McpServer({ +const mcpServer = new McpServer({ name: "Bakery", version: "1.0.0", }); -export default server; +export default mcpServer; diff --git a/src/services/mcp/tools/actions.ts b/src/services/mcp/tools/actions.ts index 07d564d..99b05fb 100644 --- a/src/services/mcp/tools/actions.ts +++ b/src/services/mcp/tools/actions.ts @@ -1,12 +1,12 @@ import z from "zod"; import { FollowUser, PinNote, UnfollowUser, UnpinNote, UpdateProfile } from "applesauce-actions/actions"; -import server from "../server.js"; +import mcpServer from "../server.js"; import { ownerActions, ownerPublish } from "../../owner.js"; import { normalizeToHexPubkey } from "../../../helpers/nip19.js"; import eventCache from "../../event-cache.js"; -server.tool( +mcpServer.tool( "follow_user", "Adds another users pubkey to the owners following list", { @@ -25,7 +25,7 @@ server.tool( }, ); -server.tool( +mcpServer.tool( "unfollow_user", "Removes another users pubkey from the owners following list", { @@ -44,7 +44,7 @@ server.tool( }, ); -server.tool( +mcpServer.tool( "pin_note", "Pins a kind 1 note to the owners pinned notes list", { @@ -59,7 +59,7 @@ server.tool( }, ); -server.tool( +mcpServer.tool( "unpin_note", "Unpins a kind 1 note from the owners pinned notes list", { @@ -74,7 +74,7 @@ server.tool( }, ); -server.tool( +mcpServer.tool( "set_profile_field", "Sets a field in the owners profile", { diff --git a/src/services/mcp/tools/config.ts b/src/services/mcp/tools/config.ts index eda2bb0..369015e 100644 --- a/src/services/mcp/tools/config.ts +++ b/src/services/mcp/tools/config.ts @@ -1,11 +1,11 @@ -import server from "../server.js"; +import mcpServer from "../server.js"; import bakeryConfig, { bakeryConfigSchema } from "../../config.js"; -server.tool("get_bakery_config", "Gets the current configuration for the bakery", {}, async () => { +mcpServer.tool("get_bakery_config", "Gets the current configuration for the bakery", {}, async () => { return { content: [{ type: "text", text: JSON.stringify(bakeryConfig.data) }] }; }); -server.tool( +mcpServer.tool( "update_bakery_config", "Updates the bakery config with the provided config", { config: bakeryConfigSchema.partial().describe("A partial config to update") }, diff --git a/src/services/mcp/tools/connection.ts b/src/services/mcp/tools/connection.ts index f22af93..4214223 100644 --- a/src/services/mcp/tools/connection.ts +++ b/src/services/mcp/tools/connection.ts @@ -2,16 +2,16 @@ import { firstValueFrom } from "rxjs"; import { isSameURL } from "applesauce-core/helpers"; import { z } from "zod"; -import server from "../server.js"; +import mcpServer from "../server.js"; import { connections$, notices$ } from "../../rx-nostr.js"; -server.tool("get_connected_relays", "Gets the connection status of all the relays", {}, async () => { +mcpServer.tool("get_connected_relays", "Gets the connection status of all the relays", {}, async () => { const relays = await firstValueFrom(connections$); return { content: [{ type: "text", text: JSON.stringify(relays) }] }; }); -server.tool( +mcpServer.tool( "get_relay_notices", "Gets the notices from the all relays or a certain relay", { relay: z.string().url().optional() }, diff --git a/src/services/mcp/tools/database.ts b/src/services/mcp/tools/database.ts index e8a4b9d..9a986d2 100644 --- a/src/services/mcp/tools/database.ts +++ b/src/services/mcp/tools/database.ts @@ -1,12 +1,12 @@ import database from "../../database.js"; -import server from "../server.js"; +import mcpServer from "../server.js"; -server.tool("total_events", "Get the total number of events in the database", {}, async () => { +mcpServer.tool("total_events", "Get the total number of events in the database", {}, async () => { const result = database.db.prepare<[], { events: number }>(`SELECT COUNT(*) AS events FROM events`).get(); return { content: [{ type: "text", text: `Total events: ${result?.events ?? 0}` }] }; }); -server.tool("total_users", "Get the total number of users in the database", {}, async () => { +mcpServer.tool("total_users", "Get the total number of users in the database", {}, async () => { const result = database.db .prepare<[], { users: number }>(`SELECT COUNT(*) AS users FROM events GROUP BY pubkey`) .get(); diff --git a/src/services/mcp/tools/draft.ts b/src/services/mcp/tools/draft.ts index 0d8abf7..ffeae6b 100644 --- a/src/services/mcp/tools/draft.ts +++ b/src/services/mcp/tools/draft.ts @@ -3,7 +3,7 @@ import { NoteBlueprint } from "applesauce-factory/blueprints"; import { EventTemplate } from "nostr-tools"; import { z } from "zod"; -import server from "../server.js"; +import mcpServer from "../server.js"; import { ownerFactory } from "../../owner.js"; async function returnUnsigned(draft: EventTemplate | Promise): Promise { @@ -12,7 +12,7 @@ async function returnUnsigned(draft: EventTemplate | Promise): Pr }; } -server.tool( +mcpServer.tool( "short_text_note_draft", "Create a short text note draft event", { diff --git a/src/services/mcp/tools/events.ts b/src/services/mcp/tools/events.ts index 71aa8aa..f73187f 100644 --- a/src/services/mcp/tools/events.ts +++ b/src/services/mcp/tools/events.ts @@ -3,14 +3,14 @@ import { getProfileContent } from "applesauce-core/helpers"; import { kinds } from "nostr-tools"; import z from "zod"; -import server from "../server.js"; +import mcpServer from "../server.js"; import { ownerFactory, ownerPublish } from "../../owner.js"; import { requestLoader } from "../../loaders.js"; import bakeryConfig from "../../config.js"; import eventCache from "../../event-cache.js"; import { normalizeToHexPubkey } from "../../../helpers/nip19.js"; -server.tool( +mcpServer.tool( "sign_draft_event", "Signs a draft note event with the owners pubkey", { @@ -34,7 +34,7 @@ server.tool( }, ); -server.tool( +mcpServer.tool( "publish_event", "Publishes a signed nostr event to the relays or the users outbox relays", { @@ -67,7 +67,7 @@ server.tool( }, ); -server.tool( +mcpServer.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) }, @@ -81,7 +81,7 @@ server.tool( ); // TODO: this needs to accept naddr, and nevent -server.tool("get_event", "Get an event by id", { id: z.string().length(64) }, async ({ id }) => { +mcpServer.tool("get_event", "Get an event by id", { id: z.string().length(64) }, async ({ id }) => { const event = await eventCache.getEventsForFilters([{ ids: [id] }]); return { @@ -89,7 +89,7 @@ server.tool("get_event", "Get an event by id", { id: z.string().length(64) }, as }; }); -server.tool( +mcpServer.tool( "search_user_pubkey", "Search for users pubkeys that match the query", { query: z.string(), limit: z.number().default(10) }, @@ -119,7 +119,7 @@ server.tool( }, ); -server.tool( +mcpServer.tool( "get_users_recent_events", "Gets a list of recent events created by a pubkey", { @@ -141,7 +141,7 @@ server.tool( }, ); -server.tool( +mcpServer.tool( "get_events_pubkey_mentioned", "Gets a list of recent events that the pubkey was mentioned in", { diff --git a/src/services/mcp/tools/signer.ts b/src/services/mcp/tools/signer.ts index bc521b8..1adc4af 100644 --- a/src/services/mcp/tools/signer.ts +++ b/src/services/mcp/tools/signer.ts @@ -3,11 +3,45 @@ import { NostrConnectAccount } from "applesauce-accounts/accounts/nostr-connect- import qrcode from "qrcode-terminal"; import { z } from "zod"; -import server from "../server.js"; +import mcpServer from "../server.js"; import { ownerAccount$, setupSigner$, startSignerSetup, stopSignerSetup } from "../../owner.js"; import { DEFAULT_NOSTR_CONNECT_RELAYS } from "../../../const.js"; +import { normalizeToHexPubkey } from "../../../helpers/nip19.js"; +import bakeryConfig from "../../config.js"; -server.tool( +mcpServer.tool( + "set_owner_pubkey", + "Sets the owner's pubkey", + { pubkey: z.string().transform((p) => normalizeToHexPubkey(p, true)) }, + async ({ pubkey }) => { + bakeryConfig.setField("owner", pubkey); + return { content: [{ type: "text", text: "Owner pubkey set" }] }; + }, +); + +mcpServer.tool("get_owner_pubkey", "Gets the owner's pubkey", {}, async () => { + const pubkey = bakeryConfig.data.owner; + if (!pubkey) return { content: [{ type: "text", text: "No owner pubkey set" }] }; + + return { content: [{ type: "text", text: pubkey }] }; +}); + +mcpServer.tool("get_setup_status", "Checks if the bakery needs to be setup", {}, async () => { + if (!bakeryConfig.data.owner) + return { content: [{ type: "text", text: "Missing owner pubkey, please set an owner to finish bakery setup" }] }; + + const account = ownerAccount$.getValue(); + if (!account) + return { content: [{ type: "text", text: "No signer connected, setup a signer if you want to sign events" }] }; + + if (setupSigner$.value) + return { content: [{ type: "text", text: "Signer setup in progress, waiting for the owner to connect" }] }; + + return { content: [{ type: "text", text: "Bakery is ready to use" }] }; +}); + +// connect remote signer tools +mcpServer.tool( "connect_nostr_signer", "Connects remote signer using a bunker:// URI", { uri: z.string().startsWith("bunker://") }, @@ -23,37 +57,15 @@ server.tool( }, ); -server.tool("disconnect_nostr_signer", "Disconnects and forgets the current signer", {}, async () => { +mcpServer.tool("disconnect_nostr_signer", "Disconnects and forgets the current signer", {}, async () => { if (!ownerAccount$.value) return { content: [{ type: "text", text: "No signer connected" }] }; ownerAccount$.next(undefined); return { content: [{ type: "text", text: "Disconnected from the signer" }] }; }); -server.tool("nostr_signer_status", "Gets the status of the current signer", {}, async () => { - const account = ownerAccount$.getValue(); - if (!account) return { content: [{ type: "text", text: "No signer connected" }] }; - - if (setupSigner$.value) { - return { content: [{ type: "text", text: "Signer setup in progress, waiting for the owner to connect" }] }; - } - - return { - content: [ - { - type: "text", - text: [ - `Pubkey: ${await account.getPublicKey()}`, - `Connected: ${account.signer.isConnected}`, - `Relays: ${account.signer.relays.join(", ")}`, - ].join("\n"), - }, - ], - }; -}); - // signer setup tools -server.tool( +mcpServer.tool( "create_signer_setup_link", "Creates a nostrconnect:// URI for the owner to setup their signer", { relays: z.array(z.string().url()).default(DEFAULT_NOSTR_CONNECT_RELAYS) }, @@ -80,7 +92,43 @@ server.tool( }, ); -server.tool("abort_nostr_signer_setup", "Aborts the signer setup process", {}, async () => { +// get signer status tools +mcpServer.tool("nostr_signer_status", "Gets the status of the current signer", {}, async () => { + const account = ownerAccount$.getValue(); + if (!account) return { content: [{ type: "text", text: "No signer connected" }] }; + + if (setupSigner$.value) { + const uri = setupSigner$.value!.getNostrConnectURI(); + + // Generate QR code + const qr = await new Promise((resolve) => { + qrcode.generate(uri, { small: true }, (qr: string) => resolve(qr)); + }); + + return { + content: [ + { type: "text", text: "Signer setup in progress, waiting for the signer to connect" }, + { type: "text", text: qr }, + { type: "text", text: `Nostr Connect URI: ${uri}` }, + ], + }; + } + + return { + content: [ + { + type: "text", + text: [ + `Pubkey: ${await account.getPublicKey()}`, + `Connected: ${account.signer.isConnected}`, + `Relays: ${account.signer.relays.join(", ")}`, + ].join("\n"), + }, + ], + }; +}); + +mcpServer.tool("abort_nostr_signer_setup", "Aborts the signer setup process", {}, async () => { await stopSignerSetup(); return { content: [{ type: "text", text: "signer setup aborted" }] }; diff --git a/src/services/owner.ts b/src/services/owner.ts index 74e4c3c..8a2a50d 100644 --- a/src/services/owner.ts +++ b/src/services/owner.ts @@ -34,6 +34,7 @@ export function startSignerSetup(relays = DEFAULT_NOSTR_CONNECT_RELAYS) { const p = signer.waitForSigner().then(async () => { const pubkey = await signer.getPublicKey(); ownerAccount$.next(new NostrConnectAccount(pubkey, signer)); + bakeryConfig.setField("owner", pubkey); setupSigner$.next(undefined); });