mirror of
https://github.com/aljazceru/bakery.git
synced 2025-12-17 12:45:20 +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",
|
"prepack": "pnpm build",
|
||||||
"start": "node .",
|
"start": "node .",
|
||||||
"dev": "nodemon --loader @swc-node/register/esm src/index.ts",
|
"dev": "nodemon --loader @swc-node/register/esm src/index.ts",
|
||||||
|
"mcp": "mcp-inspector node . --mcp --port 8080",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"format": "prettier -w ."
|
"format": "prettier -w ."
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@diva.exchange/i2p-sam": "^5.4.2",
|
"@diva.exchange/i2p-sam": "^5.4.2",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.8.0",
|
||||||
"@noble/hashes": "^1.7.1",
|
"@noble/hashes": "^1.7.1",
|
||||||
"@satellite-earth/core": "^0.5.0",
|
"@satellite-earth/core": "^0.5.0",
|
||||||
"applesauce-core": "^0.11.0",
|
"applesauce-core": "^0.11.0",
|
||||||
@@ -58,10 +60,12 @@
|
|||||||
"streamx": "^2.22.0",
|
"streamx": "^2.22.0",
|
||||||
"unique-names-generator": "^4.7.1",
|
"unique-names-generator": "^4.7.1",
|
||||||
"web-push": "^3.6.7",
|
"web-push": "^3.6.7",
|
||||||
"ws": "^8.18.1"
|
"ws": "^8.18.1",
|
||||||
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/cli": "^2.28.1",
|
"@changesets/cli": "^2.28.1",
|
||||||
|
"@modelcontextprotocol/inspector": "^0.7.0",
|
||||||
"@swc-node/register": "^1.10.9",
|
"@swc-node/register": "^1.10.9",
|
||||||
"@swc/core": "^1.10.18",
|
"@swc/core": "^1.10.18",
|
||||||
"@types/better-sqlite3": "^7.6.12",
|
"@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 config from "../services/config.js";
|
||||||
import logStore from "../services/log-store.js";
|
import logStore from "../services/log-store.js";
|
||||||
import stateManager from "../services/state.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 { inboundNetwork, outboundNetwork } from "../services/network.js";
|
||||||
import { server } from "../services/server.js";
|
import { server } from "../services/server.js";
|
||||||
import { SQLiteEventStore } from "../sqlite/event-store.js";
|
import { SQLiteEventStore } from "../sqlite/event-store.js";
|
||||||
@@ -156,7 +156,7 @@ export default class App extends EventEmitter<EventMap> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Initialize the event store
|
// Initialize the event store
|
||||||
this.eventStore = sqliteEventStore;
|
this.eventStore = eventCache;
|
||||||
|
|
||||||
// setup decryption cache
|
// setup decryption cache
|
||||||
this.decryptionCache = new DecryptionCache(this.database.db);
|
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 "dotenv/config";
|
||||||
import { mkdirp } from "mkdirp";
|
import { mkdirp } from "mkdirp";
|
||||||
import { OUTBOUND_PROXY_TYPES } from "./const.js";
|
|
||||||
|
|
||||||
import { normalizeToHexPubkey } from "./helpers/nip19.js";
|
|
||||||
import { normalizeURL } from "applesauce-core/helpers";
|
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 OWNER_PUBKEY = process.env.OWNER_PUBKEY ? normalizeToHexPubkey(process.env.OWNER_PUBKEY) : undefined;
|
||||||
export const PUBLIC_ADDRESS = process.env.PUBLIC_ADDRESS;
|
export const PUBLIC_ADDRESS = process.env.PUBLIC_ADDRESS;
|
||||||
export const DATA_PATH = process.env.DATA_PATH || "./data";
|
export const DATA_PATH = process.env.DATA_PATH || "./data";
|
||||||
await mkdirp(DATA_PATH);
|
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
|
// I2P config
|
||||||
export const I2P_PROXY = process.env.I2P_PROXY;
|
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);
|
: ["wss://purplepag.es", "wss://user.kindpag.es"].map(normalizeURL);
|
||||||
|
|
||||||
export const IS_DEV = process.env.NODE_ENV === "development";
|
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 { getPubkeyFromDecodeResult, isHexKey } from "applesauce-core/helpers";
|
||||||
import { nip19 } from "nostr-tools";
|
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;
|
if (isHexKey(hex)) return hex;
|
||||||
try {
|
try {
|
||||||
const decode = nip19.decode(hex);
|
const decode = nip19.decode(hex);
|
||||||
return getPubkeyFromDecodeResult(decode) ?? null;
|
return getPubkeyFromDecodeResult(decode) ?? null;
|
||||||
} catch (error) {
|
} 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 localizedFormat from "dayjs/plugin/localizedFormat.js";
|
||||||
|
|
||||||
import App from "./app/index.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 { addListener, logger } from "./logger.js";
|
||||||
import { pathExists } from "./helpers/fs.js";
|
import { pathExists } from "./helpers/fs.js";
|
||||||
|
|
||||||
@@ -67,12 +67,21 @@ process.on("unhandledRejection", (reason, promise) => {
|
|||||||
// start the app
|
// start the app
|
||||||
await app.start();
|
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
|
// shutdown process
|
||||||
async function shutdown() {
|
async function shutdown() {
|
||||||
logger("shutting down");
|
logger("shutting down");
|
||||||
|
|
||||||
await app.stop();
|
await app.stop();
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
process.on("SIGINT", shutdown);
|
process.on("SIGINT", shutdown);
|
||||||
@@ -80,10 +89,10 @@ process.on("SIGTERM", shutdown);
|
|||||||
|
|
||||||
// catch unhandled errors
|
// catch unhandled errors
|
||||||
process.on("uncaughtException", (error) => {
|
process.on("uncaughtException", (error) => {
|
||||||
console.error("Uncaught Exception:", error);
|
if (!IS_MCP) console.error("Uncaught Exception:", error);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Catch unhandled promise rejections
|
// 2. Catch unhandled promise rejections
|
||||||
process.on("unhandledRejection", (reason, promise) => {
|
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 debug, { Debugger } from "debug";
|
||||||
|
import { IS_MCP } from "./env.js";
|
||||||
|
|
||||||
if (!process.env.DEBUG) debug.enable("bakery,bakery:*");
|
if (!process.env.DEBUG) debug.enable("bakery,bakery:*");
|
||||||
|
|
||||||
@@ -17,7 +18,9 @@ debug.log = function (this: Debugger, ...args: any[]) {
|
|||||||
for (const listener of listeners) {
|
for (const listener of listeners) {
|
||||||
listener(this, ...args);
|
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");
|
export const logger = debug("bakery");
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SQLiteEventStore } from "../sqlite/event-store.js";
|
import { SQLiteEventStore } from "../sqlite/event-store.js";
|
||||||
import database from "./database.js";
|
import database from "./database.js";
|
||||||
|
|
||||||
const sqliteEventStore = new SQLiteEventStore(database.db);
|
const eventCache = new SQLiteEventStore(database.db);
|
||||||
await sqliteEventStore.setup();
|
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 { COMMON_CONTACT_RELAYS } from "../env.js";
|
||||||
import { rxNostr } from "./rx-nostr.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";
|
import { eventStore, queryStore } from "./stores.js";
|
||||||
|
|
||||||
function cacheRequest(filters: Filter[]) {
|
function cacheRequest(filters: Filter[]) {
|
||||||
const events = sqliteEventStore.getEventsForFilters(filters);
|
const events = eventCache.getEventsForFilters(filters);
|
||||||
return from(events).pipe(tap(markFromCache));
|
return from(events).pipe(tap(markFromCache));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ replaceableLoader.subscribe((packet) => {
|
|||||||
const event = eventStore.add(packet.event, packet.from);
|
const event = eventStore.add(packet.event, packet.from);
|
||||||
|
|
||||||
// save it to the cache if its new
|
// 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);
|
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