mirror of
https://github.com/aljazceru/bakery.git
synced 2025-12-17 20:55:02 +01:00
add mcp actions
This commit is contained in:
12
.cursor/mcp.json
Normal file
12
.cursor/mcp.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"bakery-dev": {
|
||||
"command": "npx",
|
||||
"args": ["nostrudel-bakery", "--mcp"],
|
||||
"env": {
|
||||
"DATA_DIR": "/home/robert/Projects/bakery/data",
|
||||
"OWNER_PUBKEY": "npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@@ -4,6 +4,13 @@
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Attach",
|
||||
"port": 9229,
|
||||
"request": "attach",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"type": "node"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
|
||||
11
package.json
11
package.json
@@ -11,6 +11,7 @@
|
||||
"start": "node .",
|
||||
"dev": "nodemon --loader @swc-node/register/esm src/index.ts",
|
||||
"mcp": "mcp-inspector node . --mcp --port 8080",
|
||||
"mcp-debug": "mcp-inspector node --inspect-brk . --mcp --port 8080",
|
||||
"build": "tsc",
|
||||
"test": "vitest run",
|
||||
"format": "prettier -w .",
|
||||
@@ -32,10 +33,12 @@
|
||||
"@modelcontextprotocol/sdk": "^1.8.0",
|
||||
"@noble/hashes": "^1.7.1",
|
||||
"@satellite-earth/core": "^0.5.0",
|
||||
"applesauce-core": "^0.12.0",
|
||||
"applesauce-factory": "^0.12.0",
|
||||
"applesauce-loaders": "^0.12.0",
|
||||
"applesauce-signers": "^0.12.0",
|
||||
"applesauce-accounts": "next",
|
||||
"applesauce-actions": "next",
|
||||
"applesauce-core": "next",
|
||||
"applesauce-factory": "next",
|
||||
"applesauce-loaders": "next",
|
||||
"applesauce-signers": "next",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"blossom-client-sdk": "^2.1.1",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
89
pnpm-lock.yaml
generated
89
pnpm-lock.yaml
generated
@@ -20,18 +20,24 @@ importers:
|
||||
'@satellite-earth/core':
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0(typescript@5.8.2)
|
||||
applesauce-accounts:
|
||||
specifier: next
|
||||
version: 0.0.0-next-20250327153627(typescript@5.8.2)
|
||||
applesauce-actions:
|
||||
specifier: next
|
||||
version: 0.0.0-next-20250327153627(typescript@5.8.2)
|
||||
applesauce-core:
|
||||
specifier: ^0.12.0
|
||||
version: 0.12.1(typescript@5.8.2)
|
||||
specifier: next
|
||||
version: 0.0.0-next-20250327153627(typescript@5.8.2)
|
||||
applesauce-factory:
|
||||
specifier: ^0.12.0
|
||||
version: 0.12.2(typescript@5.8.2)
|
||||
specifier: next
|
||||
version: 0.0.0-next-20250327153627(typescript@5.8.2)
|
||||
applesauce-loaders:
|
||||
specifier: ^0.12.0
|
||||
version: 0.12.0(typescript@5.8.2)
|
||||
specifier: next
|
||||
version: 0.0.0-next-20250327153627(typescript@5.8.2)
|
||||
applesauce-signers:
|
||||
specifier: ^0.12.0
|
||||
version: 0.12.0(typescript@5.8.2)
|
||||
specifier: next
|
||||
version: 0.0.0-next-20250327153627(typescript@5.8.2)
|
||||
better-sqlite3:
|
||||
specifier: ^11.9.1
|
||||
version: 11.9.1
|
||||
@@ -1288,20 +1294,26 @@ packages:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
applesauce-content@0.12.0:
|
||||
resolution: {integrity: sha512-Utgowd2cY8ZSwp/F3J1p28KTX51UHZlb3t4n7yAnWxapzCoY2EZ1AJwfIMA5AQ8ohvp6M3KEWUYdpXdc8tW/Vg==}
|
||||
applesauce-accounts@0.0.0-next-20250327153627:
|
||||
resolution: {integrity: sha512-VH76fI5HNQGMz2weCJN1qXlvllBtHaTwvYJ3hhx1wOm9do05/BTncH0amh2nu9pcQ+kmCdq91K40MWmgFcElaw==}
|
||||
|
||||
applesauce-core@0.12.1:
|
||||
resolution: {integrity: sha512-H9ahp0L4iriIrompC8mZTq4jmOZ1rBxBvQxy7GYIVYiYoRTUv0vOvjAnaOATpSWjLpqDYZbU3UZvYkPoQD3eKg==}
|
||||
applesauce-actions@0.0.0-next-20250327153627:
|
||||
resolution: {integrity: sha512-JYoIvFnXZgy6lB7rCNs7ExB+/Iw6ibbUdA4KaLzItELg0JeqKMhCk5NOWrp8uZF/odN5g6OqwdQ845NLc94+0g==}
|
||||
|
||||
applesauce-factory@0.12.2:
|
||||
resolution: {integrity: sha512-tlFsw+5bvRmCT8XFCWZK5Aj8Aa/KiVzn162FWCvMWBbxQJ85tt43HBcQRBXB3RmF3AosTwiVtGloqxX1OoQTWA==}
|
||||
applesauce-content@0.0.0-next-20250327153627:
|
||||
resolution: {integrity: sha512-hMnZH4fTI9w74eIljC/+F7L9VS8QTBMr04AtzT5la34hXMlL2qm32MN9qm6myAW0CKg3N9ZfspHkRk7yDb8ivA==}
|
||||
|
||||
applesauce-loaders@0.12.0:
|
||||
resolution: {integrity: sha512-GXX1kZMcK7Bw4y8yeZEL0ylROc4hodEWMNNwIQ2Y2wWBWfnwAiHoTq/yusp++DgpsbYDjPnPJGh8lgTvhHul7w==}
|
||||
applesauce-core@0.0.0-next-20250327153627:
|
||||
resolution: {integrity: sha512-58oyp8gTjYJcE+ddQ6UVdwmef92GjT/rUdq5OfOZWCLAZh3HZoAsw2xsAcXdzl0Did7zB9CUJfC5Mr7Ajc6jhA==}
|
||||
|
||||
applesauce-signers@0.12.0:
|
||||
resolution: {integrity: sha512-fMUfGuOkazKcBpyVgVo3gSriAKEZ1xxZU8hiDi61L1FLh/etQEngqBoTHlHgXXSc7cNMMCxDvXGHpB6DeDg7ow==}
|
||||
applesauce-factory@0.0.0-next-20250327153627:
|
||||
resolution: {integrity: sha512-snQ4s/JLdwK84c6dq2zG5BFd1kds4WHFKmxHuNjslenTipexxwosid9OjpBiC6sHAhr5N6PsaY/3dqIxIkeRNA==}
|
||||
|
||||
applesauce-loaders@0.0.0-next-20250327153627:
|
||||
resolution: {integrity: sha512-bSDMBqTYUPwLTbCnEXm4vMzqoOavK+9SNxjDcczMSTaYPISkDRQ5iBpttLSoMdMDpeeZqzU7oGFyeQw+1BPD6Q==}
|
||||
|
||||
applesauce-signers@0.0.0-next-20250327153627:
|
||||
resolution: {integrity: sha512-yBfsw+xn2n/tBJWJt99wAwXEdHOVvC8PY2Rxs538kPi/uD3yIpQLKeQ5wrACt8cOSm0C8rnEQN3Rw+8dqUOcZg==}
|
||||
|
||||
arg@4.1.3:
|
||||
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
|
||||
@@ -4493,13 +4505,34 @@ snapshots:
|
||||
normalize-path: 3.0.0
|
||||
picomatch: 2.3.1
|
||||
|
||||
applesauce-content@0.12.0(typescript@5.8.2):
|
||||
applesauce-accounts@0.0.0-next-20250327153627(typescript@5.8.2):
|
||||
dependencies:
|
||||
'@noble/hashes': 1.7.1
|
||||
applesauce-signers: 0.0.0-next-20250327153627(typescript@5.8.2)
|
||||
nanoid: 5.1.5
|
||||
nostr-tools: 2.11.0(typescript@5.8.2)
|
||||
rxjs: 7.8.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-actions@0.0.0-next-20250327153627(typescript@5.8.2):
|
||||
dependencies:
|
||||
applesauce-core: 0.0.0-next-20250327153627(typescript@5.8.2)
|
||||
applesauce-factory: 0.0.0-next-20250327153627(typescript@5.8.2)
|
||||
nostr-tools: 2.11.0(typescript@5.8.2)
|
||||
rxjs: 7.8.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-content@0.0.0-next-20250327153627(typescript@5.8.2):
|
||||
dependencies:
|
||||
'@cashu/cashu-ts': 2.0.0-rc1
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
'@types/unist': 3.0.3
|
||||
applesauce-core: 0.12.1(typescript@5.8.2)
|
||||
applesauce-core: 0.0.0-next-20250327153627(typescript@5.8.2)
|
||||
mdast-util-find-and-replace: 3.0.2
|
||||
nostr-tools: 2.11.0(typescript@5.8.2)
|
||||
remark: 15.0.1
|
||||
@@ -4510,7 +4543,7 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-core@0.12.1(typescript@5.8.2):
|
||||
applesauce-core@0.0.0-next-20250327153627(typescript@5.8.2):
|
||||
dependencies:
|
||||
'@noble/hashes': 1.7.1
|
||||
'@scure/base': 1.2.4
|
||||
@@ -4525,19 +4558,19 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-factory@0.12.2(typescript@5.8.2):
|
||||
applesauce-factory@0.0.0-next-20250327153627(typescript@5.8.2):
|
||||
dependencies:
|
||||
applesauce-content: 0.12.0(typescript@5.8.2)
|
||||
applesauce-core: 0.12.1(typescript@5.8.2)
|
||||
applesauce-content: 0.0.0-next-20250327153627(typescript@5.8.2)
|
||||
applesauce-core: 0.0.0-next-20250327153627(typescript@5.8.2)
|
||||
nanoid: 5.1.5
|
||||
nostr-tools: 2.11.0(typescript@5.8.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-loaders@0.12.0(typescript@5.8.2):
|
||||
applesauce-loaders@0.0.0-next-20250327153627(typescript@5.8.2):
|
||||
dependencies:
|
||||
applesauce-core: 0.12.1(typescript@5.8.2)
|
||||
applesauce-core: 0.0.0-next-20250327153627(typescript@5.8.2)
|
||||
nanoid: 5.1.5
|
||||
nostr-tools: 2.11.0(typescript@5.8.2)
|
||||
rx-nostr: 3.5.0
|
||||
@@ -4546,12 +4579,12 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
applesauce-signers@0.12.0(typescript@5.8.2):
|
||||
applesauce-signers@0.0.0-next-20250327153627(typescript@5.8.2):
|
||||
dependencies:
|
||||
'@noble/hashes': 1.7.1
|
||||
'@noble/secp256k1': 1.7.1
|
||||
'@scure/base': 1.2.4
|
||||
applesauce-core: 0.12.1(typescript@5.8.2)
|
||||
applesauce-core: 0.0.0-next-20250327153627(typescript@5.8.2)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
nanoid: 5.1.5
|
||||
nostr-tools: 2.11.0(typescript@5.8.2)
|
||||
|
||||
@@ -55,6 +55,7 @@ import { getDMRecipient } from "../helpers/direct-messages.js";
|
||||
import { onConnection, onJSONMessage } from "../helpers/ws.js";
|
||||
import QueryManager from "../modules/queries/manager.js";
|
||||
import "../modules/queries/queries/index.js";
|
||||
import bakerySigner from "../services/bakery.js";
|
||||
|
||||
type EventMap = {
|
||||
listening: [];
|
||||
@@ -98,16 +99,12 @@ export default class App extends EventEmitter<EventMap> {
|
||||
|
||||
this.secrets = secrets;
|
||||
|
||||
this.signer = new SimpleSigner(this.secrets.get("nostrKey"));
|
||||
this.signer = bakerySigner;
|
||||
|
||||
// copy the vapid public key over to config so the web ui can access it
|
||||
// TODO: this should be moved to another place
|
||||
this.secrets.on("updated", () => {
|
||||
this.config.data.vapidPublicKey = this.secrets.get("vapidPublicKey");
|
||||
|
||||
if (this.signer.key !== this.secrets.get("nostrKey")) {
|
||||
this.signer = new SimpleSigner(this.secrets.get("nostrKey"));
|
||||
}
|
||||
});
|
||||
|
||||
// set owner pubkey from env variable
|
||||
|
||||
35
src/classes/proxy-signer.ts
Normal file
35
src/classes/proxy-signer.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Nip07Interface } from "applesauce-signers";
|
||||
import { EventTemplate, NostrEvent } from "nostr-tools";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
export class ProxySigner<T extends Nip07Interface> implements Nip07Interface {
|
||||
private _signer: T | undefined;
|
||||
protected get signer(): T {
|
||||
if (!this._signer) throw new Error(this.error || "Missing signer");
|
||||
return this._signer;
|
||||
}
|
||||
|
||||
get nip04() {
|
||||
if (!this.signer.nip04) throw new Error("Signer does not support nip04");
|
||||
return this.signer.nip04;
|
||||
}
|
||||
|
||||
get nip44() {
|
||||
if (!this.signer.nip44) throw new Error("Signer does not support nip44");
|
||||
return this.signer.nip44;
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected upstream: Observable<T | undefined>,
|
||||
protected error?: string,
|
||||
) {
|
||||
this.upstream.subscribe((signer) => (this._signer = signer));
|
||||
}
|
||||
|
||||
signEvent(template: EventTemplate): Promise<NostrEvent> | NostrEvent {
|
||||
return this.signer.signEvent(template);
|
||||
}
|
||||
getPublicKey(): Promise<string> | string {
|
||||
return this.signer.getPublicKey();
|
||||
}
|
||||
}
|
||||
35
src/helpers/applesauce.ts
Normal file
35
src/helpers/applesauce.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { NostrPublishMethod, NostrSubscriptionMethod } from "applesauce-signers";
|
||||
import { lastValueFrom, Observable } from "rxjs";
|
||||
|
||||
import { rxNostr } from "../services/rx-nostr.js";
|
||||
import { SimplePool } from "nostr-tools";
|
||||
|
||||
const pool = new SimplePool();
|
||||
|
||||
export const nostrConnectSubscription: NostrSubscriptionMethod = (filters, relays) => {
|
||||
return new Observable((observer) => {
|
||||
const sub = pool.subscribeMany(relays, filters, {
|
||||
onevent: (event) => {
|
||||
observer.next(event);
|
||||
},
|
||||
});
|
||||
|
||||
return () => sub.close();
|
||||
});
|
||||
// return new Observable((observer) => {
|
||||
// const req = createRxForwardReq("nostr-connect");
|
||||
|
||||
// const observable = rxNostr.use(req, { on: { relays } });
|
||||
|
||||
// // hack to ensure subscription is active before sending filters
|
||||
// const sub = observable.subscribe((p) => {
|
||||
// observer.next(p.event);
|
||||
// });
|
||||
|
||||
// req.emit(filters);
|
||||
// return sub;
|
||||
// });
|
||||
};
|
||||
export const nostrConnectPublish: NostrPublishMethod = async (event, relays) => {
|
||||
await lastValueFrom(rxNostr.send(event, { on: { relays } }));
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import App from "./app/index.js";
|
||||
import { PUBLIC_ADDRESS, IS_MCP } from "./env.js";
|
||||
import { addListener, logger } from "./logger.js";
|
||||
import { pathExists } from "./helpers/fs.js";
|
||||
import "./services/owner.js";
|
||||
|
||||
// add durations plugin
|
||||
dayjs.extend(duration);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import WebSocket from "ws";
|
||||
import { Observable, Subject, Subscription } from "rxjs";
|
||||
import { Subject, Subscription } from "rxjs";
|
||||
import { logger } from "../../logger.js";
|
||||
import { Query, QueryClose, QueryData, QueryError } from "./types.js";
|
||||
|
||||
|
||||
8
src/modules/queries/queries/connections.ts
Normal file
8
src/modules/queries/queries/connections.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ConnectionState } from "rx-nostr";
|
||||
|
||||
import { Query } from "../types.js";
|
||||
import { connections$ } from "../../../services/rx-nostr.js";
|
||||
|
||||
export const ConnectionsQuery: Query<Record<string, ConnectionState>> = () => {
|
||||
return connections$;
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import QueryManager from "../manager.js";
|
||||
import { ConfigQuery } from "./config.js";
|
||||
import { ConnectionsQuery } from "./connections.js";
|
||||
import { LogsQuery } from "./logs.js";
|
||||
import NetworkStateQuery from "./network-status.js";
|
||||
import { ServicesQuery } from "./services.js";
|
||||
@@ -8,3 +9,4 @@ QueryManager.types.set("network-status", NetworkStateQuery);
|
||||
QueryManager.types.set("logs", LogsQuery);
|
||||
QueryManager.types.set("services", ServicesQuery);
|
||||
QueryManager.types.set("config", ConfigQuery);
|
||||
QueryManager.types.set("connections", ConnectionsQuery);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Observable } from "rxjs";
|
||||
import _throttle from "lodash.throttle";
|
||||
import { generateSecretKey } from "nostr-tools";
|
||||
import EventEmitter from "events";
|
||||
import { SerializedAccount } from "applesauce-accounts";
|
||||
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
|
||||
import { NostrConnectAccountSignerData } from "applesauce-accounts/accounts";
|
||||
import EventEmitter from "events";
|
||||
import webPush from "web-push";
|
||||
import crypto from "crypto";
|
||||
import fs from "fs";
|
||||
@@ -10,6 +13,7 @@ import { logger } from "../logger.js";
|
||||
|
||||
type Secrets = {
|
||||
nostrKey: Uint8Array;
|
||||
ownerAccount?: SerializedAccount<NostrConnectAccountSignerData, any>;
|
||||
vapidPrivateKey: string;
|
||||
vapidPublicKey: string;
|
||||
hyperKey: Buffer;
|
||||
@@ -18,6 +22,7 @@ type Secrets = {
|
||||
};
|
||||
type RawJson = Partial<{
|
||||
nostrKey: string;
|
||||
ownerAccount?: SerializedAccount<NostrConnectAccountSignerData, any>;
|
||||
vapidPrivateKey: string;
|
||||
vapidPublicKey: string;
|
||||
hyperKey: string;
|
||||
@@ -58,6 +63,17 @@ export default class SecretsManager extends EventEmitter<EventMap> {
|
||||
this.write();
|
||||
}
|
||||
|
||||
/** Subscribe to the value of a secret */
|
||||
watch<T extends keyof Secrets>(key: T): Observable<Secrets[T]> {
|
||||
return new Observable((observer) => {
|
||||
observer.next(this.get(key));
|
||||
|
||||
this.on("changed", (k, value) => {
|
||||
if (k === key) observer.next(value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
read() {
|
||||
this.log("Loading secrets");
|
||||
|
||||
@@ -97,6 +113,8 @@ export default class SecretsManager extends EventEmitter<EventMap> {
|
||||
secrets.i2pPrivateKey = json.i2pPrivateKey;
|
||||
secrets.i2pPublicKey = json.i2pPublicKey;
|
||||
|
||||
secrets.ownerAccount = json.ownerAccount;
|
||||
|
||||
this.secrets = secrets;
|
||||
|
||||
this.emit("loaded");
|
||||
@@ -116,6 +134,7 @@ export default class SecretsManager extends EventEmitter<EventMap> {
|
||||
hyperKey: this.secrets.hyperKey?.toString("hex"),
|
||||
i2pPrivateKey: this.secrets.i2pPrivateKey,
|
||||
i2pPublicKey: this.secrets.i2pPublicKey,
|
||||
ownerAccount: this.secrets.ownerAccount,
|
||||
};
|
||||
|
||||
fs.writeFileSync(this.path, JSON.stringify(json, null, 2), { encoding: "utf-8" });
|
||||
|
||||
6
src/services/bakery.ts
Normal file
6
src/services/bakery.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { SimpleSigner } from "applesauce-signers";
|
||||
import secrets from "./secrets.js";
|
||||
|
||||
const bakerySigner = new SimpleSigner(secrets.get("nostrKey"));
|
||||
|
||||
export default bakerySigner;
|
||||
@@ -1,5 +1,5 @@
|
||||
import "./resources.js";
|
||||
import "./tools.js";
|
||||
import "./tools/index.js";
|
||||
|
||||
import server from "./server.js";
|
||||
export default server;
|
||||
|
||||
@@ -14,6 +14,15 @@ server.resource("owner pubkey", "pubkey://owner", async (uri) => ({
|
||||
],
|
||||
}));
|
||||
|
||||
server.resource("config", "config://app", async (uri) => ({
|
||||
contents: [
|
||||
{
|
||||
uri: uri.href,
|
||||
text: JSON.stringify(config.data, null, 2),
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
server.resource(
|
||||
"user profile",
|
||||
new ResourceTemplate("users://{pubkey}/profile", { list: undefined }),
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import z from "zod";
|
||||
import { kinds } from "nostr-tools";
|
||||
|
||||
import server from "./server.js";
|
||||
import eventCache from "../event-cache.js";
|
||||
|
||||
server.tool(
|
||||
"Search events",
|
||||
"Search events by kind and query",
|
||||
{
|
||||
query: z.string(),
|
||||
kind: z.number().optional().default(kinds.ShortTextNote),
|
||||
limit: z.number().optional().default(50),
|
||||
},
|
||||
async ({ query, kind, limit }) => {
|
||||
const events = eventCache.getEventsForFilters([{ kinds: [kind], search: query, limit }]);
|
||||
|
||||
return {
|
||||
content: events.map((event) => {
|
||||
return {
|
||||
type: "text",
|
||||
text: JSON.stringify(event),
|
||||
};
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
32
src/services/mcp/tools/actions.ts
Normal file
32
src/services/mcp/tools/actions.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import z from "zod";
|
||||
import server from "../server.js";
|
||||
import { ownerActions, ownerPublish } from "../../owner.js";
|
||||
import { FollowUser, UnfollowUser } from "applesauce-actions/actions";
|
||||
|
||||
server.tool(
|
||||
"Follow user",
|
||||
"Adds another users pubkey to the owners following list",
|
||||
{ pubkey: z.string() },
|
||||
async ({ pubkey }) => {
|
||||
try {
|
||||
await ownerActions.exec(FollowUser, pubkey).forEach(ownerPublish);
|
||||
return { content: [{ type: "text", text: "Added user to following list" }] };
|
||||
} catch (error) {
|
||||
return { content: [{ type: "text", text: "Error following user" }] };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"Unfollow user",
|
||||
"Removes another users pubkey from the owners following list",
|
||||
{ pubkey: z.string() },
|
||||
async ({ pubkey }) => {
|
||||
try {
|
||||
await ownerActions.exec(UnfollowUser, pubkey).forEach(ownerPublish);
|
||||
return { content: [{ type: "text", text: "Removed user from following list" }] };
|
||||
} catch (error) {
|
||||
return { content: [{ type: "text", text: "Error unfollowing user" }] };
|
||||
}
|
||||
},
|
||||
);
|
||||
24
src/services/mcp/tools/config.ts
Normal file
24
src/services/mcp/tools/config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import server from "../server.js";
|
||||
import { ownerAccount$ } from "../../owner.js";
|
||||
import { NostrConnectSigner } from "applesauce-signers";
|
||||
import { NostrConnectAccount } from "applesauce-accounts/accounts";
|
||||
|
||||
server.tool(
|
||||
"Set owner nostr connect URI",
|
||||
"Sets the nostr connect URI that should be used to request signatures from the owners pubkey",
|
||||
{ uri: z.string().startsWith("bunker://") },
|
||||
async ({ uri }) => {
|
||||
try {
|
||||
const signer = await NostrConnectSigner.fromBunkerURI(uri, {});
|
||||
const pubkey = await signer.getPublicKey();
|
||||
const account = new NostrConnectAccount(pubkey, signer);
|
||||
ownerAccount$.next(account);
|
||||
|
||||
return { content: [{ type: "text", text: "Connected to the nostr signer" }] };
|
||||
} catch (error: any) {
|
||||
return { content: [{ type: "text", text: "Error connecting to the nostr signer: " + error.message }] };
|
||||
}
|
||||
},
|
||||
);
|
||||
14
src/services/mcp/tools/database.ts
Normal file
14
src/services/mcp/tools/database.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import database from "../../database.js";
|
||||
import server from "../server.js";
|
||||
|
||||
server.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 () => {
|
||||
const result = database.db
|
||||
.prepare<[], { users: number }>(`SELECT COUNT(*) AS users FROM events GROUP BY pubkey`)
|
||||
.get();
|
||||
return { content: [{ type: "text", text: `Total users: ${result?.users ?? 0}` }] };
|
||||
});
|
||||
77
src/services/mcp/tools/events.ts
Normal file
77
src/services/mcp/tools/events.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
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";
|
||||
import { rxNostr } from "../../rx-nostr.js";
|
||||
import { requestLoader } from "../../loaders.js";
|
||||
import config from "../../config.js";
|
||||
import { lastValueFrom, toArray } from "rxjs";
|
||||
|
||||
server.tool(
|
||||
"Sign draft event",
|
||||
"Signs a draft note event with the owners pubkey",
|
||||
{
|
||||
draft: z.object({
|
||||
content: z.string(),
|
||||
created_at: z.number(),
|
||||
tags: z.array(z.array(z.string())),
|
||||
kind: z.number(),
|
||||
}),
|
||||
},
|
||||
async ({ draft }) => {
|
||||
const unsigned = await ownerFactory.stamp(draft);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(await ownerFactory.sign(unsigned)),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"Publish event",
|
||||
"Publishes an event to the owners 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),
|
||||
}),
|
||||
},
|
||||
async ({ event }) => {
|
||||
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()));
|
||||
|
||||
return {
|
||||
content: results.map((result) => ({
|
||||
type: "text",
|
||||
text: `${result.from} ${result.ok ? "Success" : "Failed"}: ${result.message}`,
|
||||
})),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
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)),
|
||||
);
|
||||
4
src/services/mcp/tools/index.ts
Normal file
4
src/services/mcp/tools/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import "./actions.js";
|
||||
import "./events.js";
|
||||
import "./database.js";
|
||||
import "./config.js";
|
||||
83
src/services/owner.ts
Normal file
83
src/services/owner.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { NostrConnectSigner } from "applesauce-signers/signers/nostr-connect-signer";
|
||||
import { BehaviorSubject, filter, lastValueFrom, pairwise } from "rxjs";
|
||||
import { ActionHub } from "applesauce-actions";
|
||||
import { EventFactory } from "applesauce-factory";
|
||||
|
||||
import secrets from "./secrets.js";
|
||||
import { ProxySigner } from "../classes/proxy-signer.js";
|
||||
import { eventStore } from "./stores.js";
|
||||
import { nostrConnectPublish, nostrConnectSubscription } from "../helpers/applesauce.js";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import eventCache from "./event-cache.js";
|
||||
import { requestLoader } from "./loaders.js";
|
||||
import config from "./config.js";
|
||||
import { rxNostr } from "./rx-nostr.js";
|
||||
import { logger } from "../logger.js";
|
||||
import { NostrConnectAccount } from "applesauce-accounts/accounts";
|
||||
|
||||
NostrConnectSigner.subscriptionMethod = nostrConnectSubscription;
|
||||
NostrConnectSigner.publishMethod = nostrConnectPublish;
|
||||
|
||||
const log = logger.extend("Owner");
|
||||
|
||||
export const ownerAccount$ = new BehaviorSubject<NostrConnectAccount<any> | undefined>(undefined);
|
||||
|
||||
// Update account when secrets change
|
||||
secrets.watch("ownerAccount").subscribe((json) => {
|
||||
// only load the account if its a new account
|
||||
if (json && json.id !== ownerAccount$.value?.id) {
|
||||
log("Loading owner account");
|
||||
const account = NostrConnectAccount.fromJSON(json);
|
||||
ownerAccount$.next(account);
|
||||
|
||||
account.signer
|
||||
.connect()
|
||||
.then(() => {
|
||||
log("Owner account connected");
|
||||
})
|
||||
.catch((error) => {
|
||||
log("Error connecting to owner account", error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Save the account when it changes
|
||||
ownerAccount$.pipe(filter((account) => account !== undefined)).subscribe((account) => {
|
||||
if (secrets.get("ownerAccount")?.id !== account.id) {
|
||||
log("Saving owner account", account.id);
|
||||
secrets.set("ownerAccount", account.toJSON());
|
||||
}
|
||||
});
|
||||
|
||||
// close the previous signer and connect the new one
|
||||
ownerAccount$.pipe(pairwise()).subscribe(([prev, current]) => {
|
||||
if (prev) {
|
||||
log("Closing previous signer");
|
||||
prev.signer.close();
|
||||
}
|
||||
if (current) {
|
||||
log("Connecting to signer");
|
||||
current.signer.connect();
|
||||
}
|
||||
});
|
||||
|
||||
export const ownerSigner = new ProxySigner(ownerAccount$);
|
||||
export const ownerFactory = new EventFactory({ signer: ownerSigner });
|
||||
export const ownerActions = new ActionHub(eventStore, ownerFactory);
|
||||
|
||||
export async function ownerPublish(event: NostrEvent) {
|
||||
// save event to local stores
|
||||
eventStore.add(event);
|
||||
eventCache.addEvent(event);
|
||||
|
||||
// publish event to owners outboxes
|
||||
if (config.data.owner) {
|
||||
try {
|
||||
const mailboxes = await requestLoader.mailboxes({ pubkey: config.data.owner });
|
||||
await lastValueFrom(rxNostr.send(event, { on: { relays: mailboxes.outboxes } }));
|
||||
} catch (error) {
|
||||
// Failed to publish to outboxes, ignore error for now
|
||||
// TODO: this should retried at some point
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export class MigrationSet {
|
||||
// save the migration
|
||||
database.transaction(() => {
|
||||
const result = database
|
||||
.prepare<[string, number, number]>(`INSERT INTO migrations (name, version, date) VALUES (?1, ?2, ?3)`)
|
||||
.prepare<[string, number, number]>(`INSERT INTO migrations (name, version, date) VALUES (?, ?, ?)`)
|
||||
.run(this.name, script.version, unixNow());
|
||||
|
||||
const insertLog = database.prepare<[number | bigint, string]>(
|
||||
|
||||
Reference in New Issue
Block a user