fix small bugs with setup tools

This commit is contained in:
hzrd149
2025-03-28 09:50:53 +00:00
parent 77debba066
commit 27b412abcb
12 changed files with 114 additions and 66 deletions

View File

@@ -2,12 +2,14 @@
import "./polyfill.js"; import "./polyfill.js";
import process from "node:process"; import process from "node:process";
import path from "node:path"; import path from "node:path";
import express, { Request } from "express"; import express, { Request } from "express";
import dayjs from "dayjs"; import dayjs from "dayjs";
import duration from "dayjs/plugin/duration.js"; import duration from "dayjs/plugin/duration.js";
import localizedFormat from "dayjs/plugin/localizedFormat.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 App from "./app/index.js";
import { PUBLIC_ADDRESS, IS_MCP } from "./env.js"; import { PUBLIC_ADDRESS, IS_MCP } from "./env.js";
import { addListener, logger } from "./logger.js"; import { addListener, logger } from "./logger.js";
@@ -72,12 +74,9 @@ await app.start();
// Setup MCP interface on stdio // Setup MCP interface on stdio
if (IS_MCP) { 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 // connect MCP server to stdio
const transport = new StdioServerTransport(); const transport = new StdioServerTransport();
await server.connect(transport); await mcpServer.connect(transport);
} }
// shutdown process // shutdown process

View File

@@ -1,5 +1,5 @@
import "./resources.js"; import "./resources.js";
import "./tools/index.js"; import "./tools/index.js";
import server from "./server.js"; import mcpServer from "./server.js";
export default server; export default mcpServer;

View File

@@ -1,12 +1,12 @@
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import server from "./server.js"; import mcpServer from "./server.js";
import bakeryConfig from "../config.js"; import bakeryConfig 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";
import { kinds } from "nostr-tools"; import { kinds } from "nostr-tools";
server.resource("owner_pubkey", "pubkey://owner", async (uri) => ({ mcpServer.resource("owner_pubkey", "pubkey://owner", async (uri) => ({
contents: [ contents: [
{ {
uri: uri.href, 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: [ contents: [
{ {
uri: uri.href, uri: uri.href,
@@ -24,7 +24,7 @@ server.resource("config", "config://app", async (uri) => ({
], ],
})); }));
server.resource( mcpServer.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 }) => {
@@ -44,7 +44,7 @@ server.resource(
}, },
); );
server.resource("event_kinds", "nostr://kinds", async (uri) => { mcpServer.resource("event_kinds", "nostr://kinds", async (uri) => {
return { return {
contents: [{ uri: uri.href, text: JSON.stringify(kinds) }], contents: [{ uri: uri.href, text: JSON.stringify(kinds) }],
}; };

View File

@@ -1,8 +1,8 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({ const mcpServer = new McpServer({
name: "Bakery", name: "Bakery",
version: "1.0.0", version: "1.0.0",
}); });
export default server; export default mcpServer;

View File

@@ -1,12 +1,12 @@
import z from "zod"; import z from "zod";
import { FollowUser, PinNote, UnfollowUser, UnpinNote, UpdateProfile } from "applesauce-actions/actions"; 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 { ownerActions, ownerPublish } from "../../owner.js";
import { normalizeToHexPubkey } from "../../../helpers/nip19.js"; import { normalizeToHexPubkey } from "../../../helpers/nip19.js";
import eventCache from "../../event-cache.js"; import eventCache from "../../event-cache.js";
server.tool( mcpServer.tool(
"follow_user", "follow_user",
"Adds another users pubkey to the owners following list", "Adds another users pubkey to the owners following list",
{ {
@@ -25,7 +25,7 @@ server.tool(
}, },
); );
server.tool( mcpServer.tool(
"unfollow_user", "unfollow_user",
"Removes another users pubkey from the owners following list", "Removes another users pubkey from the owners following list",
{ {
@@ -44,7 +44,7 @@ server.tool(
}, },
); );
server.tool( mcpServer.tool(
"pin_note", "pin_note",
"Pins a kind 1 note to the owners pinned notes list", "Pins a kind 1 note to the owners pinned notes list",
{ {
@@ -59,7 +59,7 @@ server.tool(
}, },
); );
server.tool( mcpServer.tool(
"unpin_note", "unpin_note",
"Unpins a kind 1 note from the owners pinned notes list", "Unpins a kind 1 note from the owners pinned notes list",
{ {
@@ -74,7 +74,7 @@ server.tool(
}, },
); );
server.tool( mcpServer.tool(
"set_profile_field", "set_profile_field",
"Sets a field in the owners profile", "Sets a field in the owners profile",
{ {

View File

@@ -1,11 +1,11 @@
import server from "../server.js"; import mcpServer from "../server.js";
import bakeryConfig, { bakeryConfigSchema } from "../../config.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) }] }; return { content: [{ type: "text", text: JSON.stringify(bakeryConfig.data) }] };
}); });
server.tool( mcpServer.tool(
"update_bakery_config", "update_bakery_config",
"Updates the bakery config with the provided config", "Updates the bakery config with the provided config",
{ config: bakeryConfigSchema.partial().describe("A partial config to update") }, { config: bakeryConfigSchema.partial().describe("A partial config to update") },

View File

@@ -2,16 +2,16 @@ import { firstValueFrom } from "rxjs";
import { isSameURL } from "applesauce-core/helpers"; import { isSameURL } from "applesauce-core/helpers";
import { z } from "zod"; import { z } from "zod";
import server from "../server.js"; import mcpServer from "../server.js";
import { connections$, notices$ } from "../../rx-nostr.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$); const relays = await firstValueFrom(connections$);
return { content: [{ type: "text", text: JSON.stringify(relays) }] }; return { content: [{ type: "text", text: JSON.stringify(relays) }] };
}); });
server.tool( mcpServer.tool(
"get_relay_notices", "get_relay_notices",
"Gets the notices from the all relays or a certain relay", "Gets the notices from the all relays or a certain relay",
{ relay: z.string().url().optional() }, { relay: z.string().url().optional() },

View File

@@ -1,12 +1,12 @@
import database from "../../database.js"; 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(); const result = database.db.prepare<[], { events: number }>(`SELECT COUNT(*) AS events FROM events`).get();
return { content: [{ type: "text", text: `Total events: ${result?.events ?? 0}` }] }; 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 const result = database.db
.prepare<[], { users: number }>(`SELECT COUNT(*) AS users FROM events GROUP BY pubkey`) .prepare<[], { users: number }>(`SELECT COUNT(*) AS users FROM events GROUP BY pubkey`)
.get(); .get();

View File

@@ -3,7 +3,7 @@ import { NoteBlueprint } from "applesauce-factory/blueprints";
import { EventTemplate } from "nostr-tools"; import { EventTemplate } from "nostr-tools";
import { z } from "zod"; import { z } from "zod";
import server from "../server.js"; import mcpServer from "../server.js";
import { ownerFactory } from "../../owner.js"; import { ownerFactory } from "../../owner.js";
async function returnUnsigned(draft: EventTemplate | Promise<EventTemplate>): Promise<CallToolResult> { async function returnUnsigned(draft: EventTemplate | Promise<EventTemplate>): Promise<CallToolResult> {
@@ -12,7 +12,7 @@ async function returnUnsigned(draft: EventTemplate | Promise<EventTemplate>): Pr
}; };
} }
server.tool( mcpServer.tool(
"short_text_note_draft", "short_text_note_draft",
"Create a short text note draft event", "Create a short text note draft event",
{ {

View File

@@ -3,14 +3,14 @@ import { getProfileContent } from "applesauce-core/helpers";
import { kinds } from "nostr-tools"; import { kinds } from "nostr-tools";
import z from "zod"; import z from "zod";
import server from "../server.js"; import mcpServer from "../server.js";
import { ownerFactory, ownerPublish } from "../../owner.js"; import { ownerFactory, ownerPublish } from "../../owner.js";
import { requestLoader } from "../../loaders.js"; import { requestLoader } from "../../loaders.js";
import bakeryConfig from "../../config.js"; import bakeryConfig from "../../config.js";
import eventCache from "../../event-cache.js"; import eventCache from "../../event-cache.js";
import { normalizeToHexPubkey } from "../../../helpers/nip19.js"; import { normalizeToHexPubkey } from "../../../helpers/nip19.js";
server.tool( mcpServer.tool(
"sign_draft_event", "sign_draft_event",
"Signs a draft note event with the owners pubkey", "Signs a draft note event with the owners pubkey",
{ {
@@ -34,7 +34,7 @@ server.tool(
}, },
); );
server.tool( mcpServer.tool(
"publish_event", "publish_event",
"Publishes a signed nostr event to the relays or the users outbox relays", "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_events",
"Search for events of a certain kind that contain the query", "Search for events of a certain kind that contain the query",
{ query: z.string(), kind: z.number().default(1), limit: z.number().default(50) }, { 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 // 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] }]); const event = await eventCache.getEventsForFilters([{ ids: [id] }]);
return { 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_user_pubkey",
"Search for users pubkeys that match the query", "Search for users pubkeys that match the query",
{ query: z.string(), limit: z.number().default(10) }, { query: z.string(), limit: z.number().default(10) },
@@ -119,7 +119,7 @@ server.tool(
}, },
); );
server.tool( mcpServer.tool(
"get_users_recent_events", "get_users_recent_events",
"Gets a list of recent events created by a pubkey", "Gets a list of recent events created by a pubkey",
{ {
@@ -141,7 +141,7 @@ server.tool(
}, },
); );
server.tool( mcpServer.tool(
"get_events_pubkey_mentioned", "get_events_pubkey_mentioned",
"Gets a list of recent events that the pubkey was mentioned in", "Gets a list of recent events that the pubkey was mentioned in",
{ {

View File

@@ -3,11 +3,45 @@ import { NostrConnectAccount } from "applesauce-accounts/accounts/nostr-connect-
import qrcode from "qrcode-terminal"; import qrcode from "qrcode-terminal";
import { z } from "zod"; import { z } from "zod";
import server from "../server.js"; import mcpServer from "../server.js";
import { ownerAccount$, setupSigner$, startSignerSetup, stopSignerSetup } from "../../owner.js"; import { ownerAccount$, setupSigner$, startSignerSetup, stopSignerSetup } from "../../owner.js";
import { DEFAULT_NOSTR_CONNECT_RELAYS } from "../../../const.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", "connect_nostr_signer",
"Connects remote signer using a bunker:// URI", "Connects remote signer using a bunker:// URI",
{ uri: z.string().startsWith("bunker://") }, { 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" }] }; if (!ownerAccount$.value) return { content: [{ type: "text", text: "No signer connected" }] };
ownerAccount$.next(undefined); ownerAccount$.next(undefined);
return { content: [{ type: "text", text: "Disconnected from the signer" }] }; 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 // signer setup tools
server.tool( mcpServer.tool(
"create_signer_setup_link", "create_signer_setup_link",
"Creates a nostrconnect:// URI for the owner to setup their signer", "Creates a nostrconnect:// URI for the owner to setup their signer",
{ relays: z.array(z.string().url()).default(DEFAULT_NOSTR_CONNECT_RELAYS) }, { 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<string>((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(); await stopSignerSetup();
return { content: [{ type: "text", text: "signer setup aborted" }] }; return { content: [{ type: "text", text: "signer setup aborted" }] };

View File

@@ -34,6 +34,7 @@ export function startSignerSetup(relays = DEFAULT_NOSTR_CONNECT_RELAYS) {
const p = signer.waitForSigner().then(async () => { const p = signer.waitForSigner().then(async () => {
const pubkey = await signer.getPublicKey(); const pubkey = await signer.getPublicKey();
ownerAccount$.next(new NostrConnectAccount(pubkey, signer)); ownerAccount$.next(new NostrConnectAccount(pubkey, signer));
bakeryConfig.setField("owner", pubkey);
setupSigner$.next(undefined); setupSigner$.next(undefined);
}); });