mirror of
https://github.com/aljazceru/bakery.git
synced 2025-12-17 04:35:13 +01:00
Add MCP sdtio interface
This commit is contained in:
5
.changeset/cuddly-dragons-give.md
Normal file
5
.changeset/cuddly-dragons-give.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel-bakery": minor
|
||||
---
|
||||
|
||||
Add MCP sdtio interface
|
||||
@@ -10,6 +10,7 @@
|
||||
"prepack": "pnpm build",
|
||||
"start": "node .",
|
||||
"dev": "nodemon --loader @swc-node/register/esm src/index.ts",
|
||||
"mcp": "mcp-inspector node . --mcp --port 8080",
|
||||
"build": "tsc",
|
||||
"test": "vitest run",
|
||||
"format": "prettier -w ."
|
||||
@@ -26,6 +27,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@diva.exchange/i2p-sam": "^5.4.2",
|
||||
"@modelcontextprotocol/sdk": "^1.8.0",
|
||||
"@noble/hashes": "^1.7.1",
|
||||
"@satellite-earth/core": "^0.5.0",
|
||||
"applesauce-core": "^0.11.0",
|
||||
@@ -58,10 +60,12 @@
|
||||
"streamx": "^2.22.0",
|
||||
"unique-names-generator": "^4.7.1",
|
||||
"web-push": "^3.6.7",
|
||||
"ws": "^8.18.1"
|
||||
"ws": "^8.18.1",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.28.1",
|
||||
"@modelcontextprotocol/inspector": "^0.7.0",
|
||||
"@swc-node/register": "^1.10.9",
|
||||
"@swc/core": "^1.10.18",
|
||||
"@types/better-sqlite3": "^7.6.12",
|
||||
|
||||
5237
pnpm-lock.yaml
generated
5237
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -46,7 +46,7 @@ import secrets from "../services/secrets.js";
|
||||
import config from "../services/config.js";
|
||||
import logStore from "../services/log-store.js";
|
||||
import stateManager from "../services/state.js";
|
||||
import sqliteEventStore from "../services/event-cache.js";
|
||||
import eventCache from "../services/event-cache.js";
|
||||
import { inboundNetwork, outboundNetwork } from "../services/network.js";
|
||||
import { server } from "../services/server.js";
|
||||
import { SQLiteEventStore } from "../sqlite/event-store.js";
|
||||
@@ -156,7 +156,7 @@ export default class App extends EventEmitter<EventMap> {
|
||||
});
|
||||
|
||||
// Initialize the event store
|
||||
this.eventStore = sqliteEventStore;
|
||||
this.eventStore = eventCache;
|
||||
|
||||
// setup decryption cache
|
||||
this.decryptionCache = new DecryptionCache(this.database.db);
|
||||
|
||||
12
src/args.ts
Normal file
12
src/args.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { parseArgs, ParseArgsConfig } from "node:util";
|
||||
|
||||
const config = {
|
||||
options: {
|
||||
mcp: { type: "boolean" },
|
||||
port: { type: "string", short: "p" },
|
||||
},
|
||||
} as const satisfies ParseArgsConfig;
|
||||
|
||||
const args = parseArgs(config);
|
||||
|
||||
export default args;
|
||||
11
src/env.ts
11
src/env.ts
@@ -1,16 +1,17 @@
|
||||
import "dotenv/config";
|
||||
import { mkdirp } from "mkdirp";
|
||||
import { OUTBOUND_PROXY_TYPES } from "./const.js";
|
||||
|
||||
import { normalizeToHexPubkey } from "./helpers/nip19.js";
|
||||
import { normalizeURL } from "applesauce-core/helpers";
|
||||
|
||||
import { OUTBOUND_PROXY_TYPES } from "./const.js";
|
||||
import { normalizeToHexPubkey } from "./helpers/nip19.js";
|
||||
import args from "./args.js";
|
||||
|
||||
export const OWNER_PUBKEY = process.env.OWNER_PUBKEY ? normalizeToHexPubkey(process.env.OWNER_PUBKEY) : undefined;
|
||||
export const PUBLIC_ADDRESS = process.env.PUBLIC_ADDRESS;
|
||||
export const DATA_PATH = process.env.DATA_PATH || "./data";
|
||||
await mkdirp(DATA_PATH);
|
||||
|
||||
export const PORT = parseInt(process.env.PORT ?? "") || 3000;
|
||||
export const PORT = parseInt(args.values.port ?? process.env.PORT ?? "") || 3000;
|
||||
|
||||
// I2P config
|
||||
export const I2P_PROXY = process.env.I2P_PROXY;
|
||||
@@ -36,3 +37,5 @@ export const COMMON_CONTACT_RELAYS = process.env.COMMON_CONTACT_RELAYS
|
||||
: ["wss://purplepag.es", "wss://user.kindpag.es"].map(normalizeURL);
|
||||
|
||||
export const IS_DEV = process.env.NODE_ENV === "development";
|
||||
|
||||
export const IS_MCP = args.values.mcp;
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { getPubkeyFromDecodeResult, isHexKey } from "applesauce-core/helpers";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
export function normalizeToHexPubkey(hex: string) {
|
||||
export function normalizeToHexPubkey(hex: string, require?: boolean): string | null;
|
||||
export function normalizeToHexPubkey(hex: string, require: true): string;
|
||||
export function normalizeToHexPubkey(hex: string, require = false): string | null {
|
||||
if (isHexKey(hex)) return hex;
|
||||
try {
|
||||
const decode = nip19.decode(hex);
|
||||
return getPubkeyFromDecodeResult(decode) ?? null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
if (require) throw error;
|
||||
else return null;
|
||||
}
|
||||
}
|
||||
|
||||
17
src/index.ts
17
src/index.ts
@@ -9,7 +9,7 @@ import duration from "dayjs/plugin/duration.js";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat.js";
|
||||
|
||||
import App from "./app/index.js";
|
||||
import { PUBLIC_ADDRESS } from "./env.js";
|
||||
import { PUBLIC_ADDRESS, IS_MCP } from "./env.js";
|
||||
import { addListener, logger } from "./logger.js";
|
||||
import { pathExists } from "./helpers/fs.js";
|
||||
|
||||
@@ -67,12 +67,21 @@ process.on("unhandledRejection", (reason, promise) => {
|
||||
// start the app
|
||||
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);
|
||||
}
|
||||
|
||||
// shutdown process
|
||||
async function shutdown() {
|
||||
logger("shutting down");
|
||||
|
||||
await app.stop();
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
process.on("SIGINT", shutdown);
|
||||
@@ -80,10 +89,10 @@ process.on("SIGTERM", shutdown);
|
||||
|
||||
// catch unhandled errors
|
||||
process.on("uncaughtException", (error) => {
|
||||
console.error("Uncaught Exception:", error);
|
||||
if (!IS_MCP) console.error("Uncaught Exception:", error);
|
||||
});
|
||||
|
||||
// 2. Catch unhandled promise rejections
|
||||
process.on("unhandledRejection", (reason, promise) => {
|
||||
console.error("Unhandled Promise Rejection:", reason);
|
||||
if (!IS_MCP) console.error("Unhandled Promise Rejection:", reason);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import debug, { Debugger } from "debug";
|
||||
import { IS_MCP } from "./env.js";
|
||||
|
||||
if (!process.env.DEBUG) debug.enable("bakery,bakery:*");
|
||||
|
||||
@@ -17,7 +18,9 @@ debug.log = function (this: Debugger, ...args: any[]) {
|
||||
for (const listener of listeners) {
|
||||
listener(this, ...args);
|
||||
}
|
||||
console.log.apply(this, args);
|
||||
|
||||
// only log to console if not running in MCP mode
|
||||
if (!IS_MCP) console.log.apply(this, args);
|
||||
};
|
||||
|
||||
export const logger = debug("bakery");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SQLiteEventStore } from "../sqlite/event-store.js";
|
||||
import database from "./database.js";
|
||||
|
||||
const sqliteEventStore = new SQLiteEventStore(database.db);
|
||||
await sqliteEventStore.setup();
|
||||
const eventCache = new SQLiteEventStore(database.db);
|
||||
await eventCache.setup();
|
||||
|
||||
export default sqliteEventStore;
|
||||
export default eventCache;
|
||||
|
||||
@@ -5,11 +5,11 @@ import { ReplaceableLoader, RequestLoader } from "applesauce-loaders/loaders";
|
||||
|
||||
import { COMMON_CONTACT_RELAYS } from "../env.js";
|
||||
import { rxNostr } from "./rx-nostr.js";
|
||||
import sqliteEventStore from "./event-cache.js";
|
||||
import eventCache from "./event-cache.js";
|
||||
import { eventStore, queryStore } from "./stores.js";
|
||||
|
||||
function cacheRequest(filters: Filter[]) {
|
||||
const events = sqliteEventStore.getEventsForFilters(filters);
|
||||
const events = eventCache.getEventsForFilters(filters);
|
||||
return from(events).pipe(tap(markFromCache));
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ replaceableLoader.subscribe((packet) => {
|
||||
const event = eventStore.add(packet.event, packet.from);
|
||||
|
||||
// save it to the cache if its new
|
||||
if (!isFromCache(event)) sqliteEventStore.addEvent(event);
|
||||
if (!isFromCache(event)) eventCache.addEvent(event);
|
||||
});
|
||||
|
||||
export const requestLoader = new RequestLoader(queryStore);
|
||||
|
||||
5
src/services/mcp/index.ts
Normal file
5
src/services/mcp/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import "./resources.js";
|
||||
import "./tools.js";
|
||||
|
||||
import server from "./server.js";
|
||||
export default server;
|
||||
35
src/services/mcp/resources.ts
Normal file
35
src/services/mcp/resources.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
|
||||
import server from "./server.js";
|
||||
import config from "../config.js";
|
||||
import { normalizeToHexPubkey } from "../../helpers/nip19.js";
|
||||
import { requestLoader } from "../loaders.js";
|
||||
|
||||
server.resource("owner pubkey", "pubkey://owner", async (uri) => ({
|
||||
contents: [
|
||||
{
|
||||
uri: uri.href,
|
||||
text: config.data.owner ?? "undefined",
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
server.resource(
|
||||
"user profile",
|
||||
new ResourceTemplate("users://{pubkey}/profile", { list: undefined }),
|
||||
async (uri, { pubkey }) => {
|
||||
if (typeof pubkey !== "string") throw new Error("Pubkey must be a string");
|
||||
|
||||
pubkey = normalizeToHexPubkey(pubkey, true);
|
||||
const profile = await requestLoader.profile({ pubkey });
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: uri.href,
|
||||
text: JSON.stringify(profile, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
8
src/services/mcp/server.ts
Normal file
8
src/services/mcp/server.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
|
||||
const server = new McpServer({
|
||||
name: "Bakery",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
export default server;
|
||||
27
src/services/mcp/tools.ts
Normal file
27
src/services/mcp/tools.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
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),
|
||||
};
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user