mirror of
https://github.com/aljazceru/mcp-code.git
synced 2025-12-17 12:45:28 +01:00
feat: Add zap command for sending Bitcoin Lightning tips
- Implemented the `zap` command in the CLI to allow users to send sats to a user, event, or snippet using a NIP-60 wallet. - Created a new `zap.ts` file to handle the command logic and integrated it into the MCP server. - Added wallet balance command to check the balance of a user's wallet. - Enhanced the MCP server to register the new zap command and wallet balance command. - Introduced caching for wallets to optimize performance and reduce redundant network requests. - Updated database schema to include snippets table for storing code snippets. - Improved logging functionality for better debugging and tracking of operations. - Added functionality to save snippets to the database upon retrieval. - Updated project overview documentation to reflect new features and structure. - Refactored existing commands and logic for better modularity and maintainability.
This commit is contained in:
@@ -4,6 +4,7 @@ import { registerWotCommand } from './wot.js';
|
|||||||
import { registerListUsernamesCommand } from './list-usernames.js';
|
import { registerListUsernamesCommand } from './list-usernames.js';
|
||||||
import { registerMcpCommand } from './mcp.js';
|
import { registerMcpCommand } from './mcp.js';
|
||||||
import { registerSetupCommand } from './setup.js';
|
import { registerSetupCommand } from './setup.js';
|
||||||
|
import { registerZapCommand } from './zap.js';
|
||||||
|
|
||||||
// Create a new Commander program
|
// Create a new Commander program
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
@@ -20,6 +21,7 @@ registerFindSnippetsCommand(program);
|
|||||||
registerWotCommand(program);
|
registerWotCommand(program);
|
||||||
registerListUsernamesCommand(program);
|
registerListUsernamesCommand(program);
|
||||||
registerSetupCommand(program);
|
registerSetupCommand(program);
|
||||||
|
registerZapCommand(program);
|
||||||
|
|
||||||
// Function to run the CLI
|
// Function to run the CLI
|
||||||
export async function runCli(args: string[]) {
|
export async function runCli(args: string[]) {
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import type { Command } from 'commander';
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import type { Command } from "commander";
|
||||||
import { readConfig } from "../config.js";
|
import { readConfig } from "../config.js";
|
||||||
import { addCreatePubkeyCommand } from "../logic/create-pubkey.js";
|
import { addCreatePubkeyCommand } from "../logic/create-pubkey.js";
|
||||||
|
import { addFetchSnippetByIdCommand } from "../logic/fetch_snippet_by_id.js";
|
||||||
import { addFindSnippetsCommand } from "../logic/find_snippets.js";
|
import { addFindSnippetsCommand } from "../logic/find_snippets.js";
|
||||||
import { addFindUserCommand } from "../logic/find_user.js";
|
import { addFindUserCommand } from "../logic/find_user.js";
|
||||||
|
import { addListSnippetsCommand } from "../logic/list_snippets.js";
|
||||||
import { addListUsernamesCommand } from "../logic/list_usernames.js";
|
import { addListUsernamesCommand } from "../logic/list_usernames.js";
|
||||||
import { addPublishCodeSnippetCommand } from "../logic/publish-code-snippet.js";
|
import { addPublishCodeSnippetCommand } from "../logic/publish-code-snippet.js";
|
||||||
import { addPublishCommand } from "../logic/publish.js";
|
import { addPublishCommand } from "../logic/publish.js";
|
||||||
import { addListSnippetsCommand } from "../logic/list_snippets.js";
|
import { addWalletBalanceCommand } from "../logic/wallet-balance.js";
|
||||||
import { addFetchSnippetByIdCommand } from "../logic/fetch_snippet_by_id.js";
|
import { addZapCommand } from "../logic/zap.js";
|
||||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
import { log } from "../utils/log.js";
|
||||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
||||||
|
|
||||||
// Define type for command functions
|
// Define type for command functions
|
||||||
type CommandFunction = (server: McpServer) => void;
|
type CommandFunction = (server: McpServer) => void;
|
||||||
@@ -24,27 +27,32 @@ const commandMap: Record<string, CommandFunction> = {
|
|||||||
"list-usernames": addListUsernamesCommand,
|
"list-usernames": addListUsernamesCommand,
|
||||||
"list-snippets": addListSnippetsCommand,
|
"list-snippets": addListSnippetsCommand,
|
||||||
"fetch-snippet-by-id": addFetchSnippetByIdCommand,
|
"fetch-snippet-by-id": addFetchSnippetByIdCommand,
|
||||||
|
zap: addZapCommand,
|
||||||
|
"wallet-balance": addWalletBalanceCommand,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Global server instance
|
// Global server instance
|
||||||
let mcpServer: McpServer | null = null;
|
const mcpServer = new McpServer({
|
||||||
|
name: "Nostr Publisher",
|
||||||
|
version: "1.0.0",
|
||||||
|
capabilities: {
|
||||||
|
resources: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export function registerMcpCommand(program: Command): void {
|
export function registerMcpCommand(program: Command): void {
|
||||||
program
|
program
|
||||||
.command('mcp')
|
.command("mcp")
|
||||||
.description('Start the MCP server')
|
.description("Start the MCP server")
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
try {
|
try {
|
||||||
// Create the MCP server
|
// Create the MCP server
|
||||||
mcpServer = new McpServer({
|
|
||||||
name: "Nostr Publisher",
|
|
||||||
version: "1.0.0",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register all MCP commands
|
// Register all MCP commands
|
||||||
registerMcpCommands(mcpServer);
|
registerMcpCommands(mcpServer);
|
||||||
|
|
||||||
// Connect the server to the transport
|
// Connect the server to the transport
|
||||||
|
log("Starting MCP server...");
|
||||||
const transport = new StdioServerTransport();
|
const transport = new StdioServerTransport();
|
||||||
await mcpServer.connect(transport);
|
await mcpServer.connect(transport);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -76,5 +84,7 @@ export function registerMcpCommands(server: McpServer) {
|
|||||||
addListUsernamesCommand(server);
|
addListUsernamesCommand(server);
|
||||||
addListSnippetsCommand(server);
|
addListSnippetsCommand(server);
|
||||||
addFetchSnippetByIdCommand(server);
|
addFetchSnippetByIdCommand(server);
|
||||||
|
addZapCommand(server);
|
||||||
|
addWalletBalanceCommand(server);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
commands/zap.ts
Normal file
35
commands/zap.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Command } from "commander";
|
||||||
|
import { sendZap } from "../logic/zap.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the zap command with the Commander program
|
||||||
|
* @param program The Commander program instance
|
||||||
|
*/
|
||||||
|
export function registerZapCommand(program: Command) {
|
||||||
|
program
|
||||||
|
.command("zap")
|
||||||
|
.description("Send sats to a user, event, or snippet using a NIP-60 wallet")
|
||||||
|
.requiredOption("-a, --amount <amount>", "Amount in sats to send", Number.parseInt)
|
||||||
|
.option("-r, --recipient <recipient>", "Recipient (username, npub, or pubkey) to zap directly")
|
||||||
|
.option("-e, --event-id <eventId>", "Event ID to zap")
|
||||||
|
.option("-t, --title <title>", "Snippet title to look up and zap")
|
||||||
|
.option("-m, --message <message>", "Thank you message to include with the zap")
|
||||||
|
.option("-u, --username <username>", "Username to zap from (uses wallet associated with this user)")
|
||||||
|
.action(async (options) => {
|
||||||
|
try {
|
||||||
|
const result = await sendZap(
|
||||||
|
options.amount,
|
||||||
|
options.recipient,
|
||||||
|
options.eventId,
|
||||||
|
options.title,
|
||||||
|
options.message,
|
||||||
|
options.username
|
||||||
|
);
|
||||||
|
const message = result?.content?.[0]?.text;
|
||||||
|
console.log(message || "Zap sent successfully!");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error instanceof Error ? error.message : String(error));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
31
context/project_overview.md
Normal file
31
context/project_overview.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# MCP-Code (MCP-NOSTR) Project Overview
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This project acts as a bridge between the Model Context Protocol (MCP) and the Nostr network. Its primary function is to enable AI language models, interacting via MCP, to publish content directly to the Nostr decentralized social network.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- **MCP Server Implementation:** Listens for MCP messages (stdin) and responds (stdout), allowing interaction with AI models.
|
||||||
|
- **Nostr Publishing:** Facilitates publishing content generated by AI models to the Nostr network.
|
||||||
|
- **CLI Interface:** Provides command-line tools for managing Nostr identities, profiles, content publishing, and other Nostr-related tasks.
|
||||||
|
- **Identity Management:** Handles Nostr keys and profiles.
|
||||||
|
- **Web of Trust (WoT):** Includes support for WoT features.
|
||||||
|
- **User Management:** Manages user profiles and follow lists within the Nostr context.
|
||||||
|
- **Code Snippet Sharing:** Allows publishing and finding code snippets on Nostr.
|
||||||
|
- **Zaps:** Supports sending and receiving zaps (Bitcoin Lightning tips).
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
- **Runtime:** Bun
|
||||||
|
- **Language:** TypeScript
|
||||||
|
- **Core Libraries:**
|
||||||
|
- `@modelcontextprotocol/sdk`: For MCP communication.
|
||||||
|
- `@nostr-dev-kit/ndk`: For Nostr protocol interactions.
|
||||||
|
- `@nostr-dev-kit/ndk-wallet`: For Nostr wallet integration (Zaps).
|
||||||
|
- `commander`: For building the CLI interface.
|
||||||
|
- `better-sqlite3`: For local database storage.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configuration details (like user keys, relays) are typically stored in `~/.mcp-nostr.json` (Note: This path might need confirmation or could be configurable).
|
||||||
1
db.ts
1
db.ts
@@ -31,6 +31,7 @@ export const migrations: Migration[] = [
|
|||||||
// Example:
|
// Example:
|
||||||
// { name: '001_initial_schema', module: () => import('./migrations/001_initial_schema.js') },
|
// { name: '001_initial_schema', module: () => import('./migrations/001_initial_schema.js') },
|
||||||
// Add all migrations here
|
// Add all migrations here
|
||||||
|
{ name: '003-create-snippets-table', module: () => import('./migrations/003-create-snippets-table.js') },
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
7
index.ts
7
index.ts
@@ -2,17 +2,20 @@ import { initConfig, readConfig, writeConfig } from "./config.js";
|
|||||||
import { initNDK, ndk } from "./ndk.js";
|
import { initNDK, ndk } from "./ndk.js";
|
||||||
import "./db.js";
|
import "./db.js";
|
||||||
import { runCli } from "./commands/index.js";
|
import { runCli } from "./commands/index.js";
|
||||||
|
import { log } from "./lib/utils/log.js";
|
||||||
import { runConfigWizard } from "./wizard";
|
import { runConfigWizard } from "./wizard";
|
||||||
|
|
||||||
|
log("starting up...: args: " + process.argv.join(" "));
|
||||||
|
|
||||||
// Load config and ensure defaults
|
// Load config and ensure defaults
|
||||||
const config = initConfig();
|
const config = initConfig();
|
||||||
|
|
||||||
// If there's no privateKey or bunker configured, run the setup wizard
|
// If there's no privateKey or bunker configured, run the setup wizard
|
||||||
if (!config.privateKey && !config.bunker) {
|
if (!config.privateKey && !config.bunker) {
|
||||||
const updatedConfig = await runConfigWizard(config);
|
const updatedConfig = await runConfigWizard(config);
|
||||||
await initNDK(updatedConfig);
|
initNDK(updatedConfig);
|
||||||
} else {
|
} else {
|
||||||
await initNDK(config);
|
initNDK(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process command-line arguments
|
// Process command-line arguments
|
||||||
|
|||||||
108
lib/cache/wallets.ts
vendored
Normal file
108
lib/cache/wallets.ts
vendored
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { NDKCashuWallet, NDKNutzapMonitor } from "@nostr-dev-kit/ndk-wallet";
|
||||||
|
import { NDKCashuMintList, NDKPrivateKeySigner, type NDKSigner } from "@nostr-dev-kit/ndk";
|
||||||
|
import { ndk } from "../../ndk.js";
|
||||||
|
import { log } from "../utils/log.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory cache to store NDKCashuWallets by pubkey for efficient retrieval
|
||||||
|
* Used by the zap command to avoid repeatedly fetching wallet events
|
||||||
|
*/
|
||||||
|
export const walletsCache: Record<string, NDKCashuWallet> = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a wallet for a pubkey, retrieving from cache if available,
|
||||||
|
* otherwise loading from Nostr
|
||||||
|
* @param pubkey The public key to get the wallet for
|
||||||
|
* @returns The wallet (from cache or newly loaded)
|
||||||
|
*/
|
||||||
|
export async function getWallet(pubkey: string, signer: NDKSigner): Promise<NDKCashuWallet | undefined> {
|
||||||
|
// Return from cache if available
|
||||||
|
if (walletsCache[pubkey]) {
|
||||||
|
console.log(`Returning cached wallet for ${pubkey}`);
|
||||||
|
return walletsCache[pubkey];
|
||||||
|
}
|
||||||
|
|
||||||
|
ndk.signer = signer;
|
||||||
|
|
||||||
|
// Not in cache, fetch from Nostr
|
||||||
|
try {
|
||||||
|
let wallet: NDKCashuWallet | undefined;
|
||||||
|
const user = ndk.getUser({pubkey});
|
||||||
|
const event = await ndk.fetchEvent({ kinds: [17375], authors: [pubkey] });
|
||||||
|
|
||||||
|
// Use the existing wallet
|
||||||
|
if (event) {
|
||||||
|
console.log(`Found wallet event for ${pubkey}: ${event.id}`, event.inspect);
|
||||||
|
wallet = await NDKCashuWallet.from(event);
|
||||||
|
} else {
|
||||||
|
console.log('No wallet event found for', pubkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wallet) {
|
||||||
|
wallet = new NDKCashuWallet(ndk);
|
||||||
|
wallet.mints = ["https://mint.coinos.io"];
|
||||||
|
|
||||||
|
// Generate a P2PK address for receiving nutzaps
|
||||||
|
await wallet.getP2pk();
|
||||||
|
log(`Generated P2PK address for ${pubkey}: ${wallet.p2pk}`);
|
||||||
|
|
||||||
|
// Publish the wallet info event (kind 17375)
|
||||||
|
await wallet.publish();
|
||||||
|
log(`Published wallet info event for ${pubkey}`);
|
||||||
|
|
||||||
|
// Set up the mint list for nutzap reception (kind 10019)
|
||||||
|
const mintList = new NDKCashuMintList(ndk);
|
||||||
|
mintList.mints = wallet.mints;
|
||||||
|
mintList.relays = ['wss://relay.pri']
|
||||||
|
mintList.p2pk = wallet.p2pk;
|
||||||
|
|
||||||
|
// Publish the mint list
|
||||||
|
await mintList.publish();
|
||||||
|
log(`Published mint list for ${pubkey} with mints: ${mintList.mints.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Start wallet for monitoring balance and nutzaps
|
||||||
|
console.log(`Starting wallet for ${pubkey}`);
|
||||||
|
await wallet.start();
|
||||||
|
console.log(`Wallet started for ${pubkey}: ${wallet.balance}`);
|
||||||
|
log(`Started wallet for ${pubkey}: ${wallet.balance}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nutzapMonitor = new NDKNutzapMonitor(ndk, user, {});
|
||||||
|
nutzapMonitor.wallet = wallet;
|
||||||
|
nutzapMonitor.on("seen", () => {
|
||||||
|
log('seen nutzap');
|
||||||
|
});
|
||||||
|
nutzapMonitor.on("redeemed", (events) => {
|
||||||
|
log(`Nutzap redeemed for ${pubkey}: ${events.reduce((acc, event) => acc + event.amount, 0)} sats`);
|
||||||
|
});
|
||||||
|
nutzapMonitor.start({});
|
||||||
|
log(`Started nutzap monitor for ${pubkey}`);
|
||||||
|
|
||||||
|
// Set up balance update listener
|
||||||
|
wallet.on('balance_updated', (newBalance) => {
|
||||||
|
log(`Balance updated for ${pubkey}: ${newBalance?.amount || 0} sats`);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error starting nutzap monitor for ${pubkey}:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
walletsCache[pubkey] = wallet;
|
||||||
|
|
||||||
|
// No wallet found
|
||||||
|
return wallet;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching wallet for ${pubkey}:`, error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the wallet cache for testing or memory management
|
||||||
|
*/
|
||||||
|
export function clearWalletsCache(): void {
|
||||||
|
for (const key of Object.keys(walletsCache)) {
|
||||||
|
delete walletsCache[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,10 +70,13 @@ export function formatSnippets(snippets: CodeSnippet[]): string {
|
|||||||
.map((snippet) => {
|
.map((snippet) => {
|
||||||
const author = knownUsers[snippet.pubkey];
|
const author = knownUsers[snippet.pubkey];
|
||||||
const keys: Record<string, string> = {
|
const keys: Record<string, string> = {
|
||||||
|
ID: snippet.id,
|
||||||
Title: snippet.title,
|
Title: snippet.title,
|
||||||
|
Description: snippet.description,
|
||||||
Language: snippet.language,
|
Language: snippet.language,
|
||||||
Tags: snippet.tags.join(", "),
|
Tags: snippet.tags.join(", "),
|
||||||
Code: snippet.code,
|
Code: snippet.code,
|
||||||
|
Date: new Date(snippet.createdAt * 1000).toISOString(),
|
||||||
};
|
};
|
||||||
if (author?.profile?.name) keys.Author = author.profile.name;
|
if (author?.profile?.name) keys.Author = author.profile.name;
|
||||||
return Object.entries(keys)
|
return Object.entries(keys)
|
||||||
@@ -95,7 +98,7 @@ export function formatPartialMatches(snippets: CodeSnippet[]): string {
|
|||||||
"\n\nSome other events not included in this result since they had less in common with your search, here is a list of the events that had partial matches:\n\n";
|
"\n\nSome other events not included in this result since they had less in common with your search, here is a list of the events that had partial matches:\n\n";
|
||||||
text += snippets
|
text += snippets
|
||||||
.map((snippet) => {
|
.map((snippet) => {
|
||||||
return ` * ${snippet.title}:\n Tags: ${snippet.tags.join(", ")}`;
|
return ` * ${snippet.title}:\n Tags: ${snippet.tags.join(", ")} (ID: ${snippet.id})`;
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk";
|
import type { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk";
|
||||||
|
import { db } from "../../db.js";
|
||||||
import { ndk } from "../../ndk.js";
|
import { ndk } from "../../ndk.js";
|
||||||
import type { CodeSnippet, FindSnippetsParams } from "../types/index.js";
|
import type { CodeSnippet, FindSnippetsParams } from "../types/index.js";
|
||||||
import { log } from "../utils/log.js";
|
import { log } from "../utils/log.js";
|
||||||
@@ -58,15 +59,22 @@ export async function getSnippets(params: FindSnippetsParams = {}): Promise<{
|
|||||||
|
|
||||||
let maxMatchCount = 0;
|
let maxMatchCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to calculate the number of tags in an event that match the search tags.
|
||||||
|
* Used for ranking snippets based on tag relevance.
|
||||||
|
* If no tags are provided in params, all snippets are considered equally relevant (match count = 1).
|
||||||
|
*/
|
||||||
function getMatchCount(event: NDKEvent) {
|
function getMatchCount(event: NDKEvent) {
|
||||||
if (!params.tags || params.tags.length === 0) return 1;
|
if (!params.tags || params.tags.length === 0) return 1;
|
||||||
|
|
||||||
const aTags = event.tags
|
const aTags = event.tags
|
||||||
.filter((tag) => tag[0] === "t")
|
.filter((tag) => tag[0] === "t") // Filter for 't' tags
|
||||||
.map((tag) => tag[1])
|
.map((tag) => tag[1]) // Get the tag value
|
||||||
.filter((t) => t !== undefined);
|
.filter((t): t is string => t !== undefined); // Ensure tag value exists and narrow type
|
||||||
return params.tags.filter((tag) =>
|
|
||||||
aTags.some((t) => t.match(new RegExp(tag, "i")))
|
// Count how many of the searched tags are present in the event's tags
|
||||||
|
return params.tags.filter((searchTag) =>
|
||||||
|
aTags.some((eventTag) => eventTag.match(new RegExp(searchTag, "i"))) // Case-insensitive tag matching
|
||||||
).length;
|
).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +98,39 @@ export async function getSnippets(params: FindSnippetsParams = {}): Promise<{
|
|||||||
const snippets = selectedEvents.map(toSnippet);
|
const snippets = selectedEvents.map(toSnippet);
|
||||||
const otherSnippets = notSelectedEvents.map(toSnippet);
|
const otherSnippets = notSelectedEvents.map(toSnippet);
|
||||||
|
|
||||||
|
|
||||||
|
// --- BEGIN DATABASE INSERTION ---
|
||||||
|
const allSnippets = [...snippets, ...otherSnippets];
|
||||||
|
if (allSnippets.length > 0) {
|
||||||
|
log(`Saving ${allSnippets.length} snippets to the database...`);
|
||||||
|
const insertStmt = db.prepare(`
|
||||||
|
INSERT OR REPLACE INTO snippets (id, title, description, code, language, pubkey, createdAt, tags)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.transaction(() => {
|
||||||
|
for (const snippet of allSnippets) {
|
||||||
|
insertStmt.run(
|
||||||
|
snippet.id,
|
||||||
|
snippet.title,
|
||||||
|
snippet.description,
|
||||||
|
snippet.code,
|
||||||
|
snippet.language,
|
||||||
|
snippet.pubkey,
|
||||||
|
snippet.createdAt,
|
||||||
|
JSON.stringify(snippet.tags) // Store tags as JSON string
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})(); // Immediately invoke the transaction
|
||||||
|
log("Snippets saved successfully.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save snippets to database:", error);
|
||||||
|
// Decide if we should throw or just log the error
|
||||||
|
// For now, just log it and continue returning fetched snippets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- END DATABASE INSERTION ---
|
||||||
return { snippets, otherSnippets };
|
return { snippets, otherSnippets };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import { toPubkeys, toSnippet } from "../converters/index.js";
|
|||||||
|
|
||||||
export const SNIPPET_KIND = 1337;
|
export const SNIPPET_KIND = 1337;
|
||||||
|
|
||||||
|
export function getUsernameFromPubkey(pubkey: string): string | null {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the appropriate signer based on the username
|
* Gets the appropriate signer based on the username
|
||||||
* @param username Username to get signer for (or "main" for default)
|
* @param username Username to get signer for (or "main" for default)
|
||||||
|
|||||||
@@ -1,5 +1,30 @@
|
|||||||
|
import * as fs from "node:fs";
|
||||||
|
import { promises as fsPromises } from "node:fs";
|
||||||
|
import * as os from "node:os";
|
||||||
|
import * as path from "node:path";
|
||||||
|
|
||||||
|
const timeZero = Date.now();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple logging utility
|
* Simple logging utility
|
||||||
* @param message Message to log
|
* @param message Message to log
|
||||||
|
* @param logFilePath Optional custom log file path
|
||||||
*/
|
*/
|
||||||
export function log(_message: string): void {}
|
export function log(
|
||||||
|
message: string,
|
||||||
|
logFilePath: string = path.join(os.homedir(), ".mcp-code23.log")
|
||||||
|
): void {
|
||||||
|
// Ensure the directory exists
|
||||||
|
const logDir = path.dirname(logFilePath);
|
||||||
|
fs.mkdirSync(logDir, { recursive: true });
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const relativeTime = now.getTime() - timeZero;
|
||||||
|
const logMessage = `[${relativeTime}ms] ${timestamp} - ${message}\n`;
|
||||||
|
fs.appendFile(logFilePath, logMessage, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error("Error writing to log file:", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import NDK, { NDKPrivateKeySigner, NDKUser, NDKEvent } from "@nostr-dev-kit/ndk";
|
import NDK, { NDKPrivateKeySigner, NDKUser, NDKEvent } from "@nostr-dev-kit/ndk";
|
||||||
|
import { NDKCashuWallet } from "@nostr-dev-kit/ndk-wallet";
|
||||||
|
import { NDKCashuMintList } from "@nostr-dev-kit/ndk";
|
||||||
|
import { getWallet } from "../lib/cache/wallets.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getUser, saveUser } from "../config.js";
|
import { getUser, saveUser } from "../config.js";
|
||||||
import { log } from "../lib/utils/log.js";
|
import { log } from "../lib/utils/log.js";
|
||||||
import { ndk } from "../ndk.js";
|
import { ndk } from "../ndk.js";
|
||||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
|
||||||
export async function createPubkey({
|
export async function createPubkey({
|
||||||
username,
|
username,
|
||||||
@@ -44,6 +47,27 @@ export async function createPubkey({
|
|||||||
// Publish the event
|
// Publish the event
|
||||||
await event.publish();
|
await event.publish();
|
||||||
|
|
||||||
|
|
||||||
|
// --- Setup Cashu Wallet (NIP-60/NIP-61) ---
|
||||||
|
log(`Setting up Cashu wallet for ${username}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use getWallet to retrieve or create a wallet for the new pubkey
|
||||||
|
const wallet = await getWallet(signer.pubkey);
|
||||||
|
|
||||||
|
if (wallet) {
|
||||||
|
log(`Cashu wallet setup complete for ${username}.`);
|
||||||
|
log(` -> P2PK: ${wallet.p2pk}`);
|
||||||
|
log(` -> Mints: ${wallet.mints.join(', ')}`);
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to create wallet");
|
||||||
|
}
|
||||||
|
} catch (walletError) {
|
||||||
|
console.error(`Failed to set up Cashu wallet for ${username}:`, walletError);
|
||||||
|
// Decide if this should be a fatal error for pubkey creation
|
||||||
|
// For now, log the error and continue
|
||||||
|
}
|
||||||
|
// --- End Cashu Wallet Setup ---
|
||||||
// Save the user to the config
|
// Save the user to the config
|
||||||
saveUser(username, {
|
saveUser(username, {
|
||||||
nsec: signer.privateKey,
|
nsec: signer.privateKey,
|
||||||
@@ -59,7 +83,7 @@ export async function createPubkey({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
message: `Created pubkey for ${username} with npub ${user.npub}`,
|
message: `Created pubkey for ${username} with npub ${user.npub} and a NIP-60 Cashu wallet for nutzaps`,
|
||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
|
|||||||
@@ -55,12 +55,7 @@ export function addListUsernamesCommand(server: McpServer) {
|
|||||||
server.tool(
|
server.tool(
|
||||||
"list_usernames",
|
"list_usernames",
|
||||||
"List all available usernames in the system",
|
"List all available usernames in the system",
|
||||||
{
|
{ },
|
||||||
random_string: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.describe("Dummy parameter for no-parameter tools"),
|
|
||||||
},
|
|
||||||
async () => {
|
async () => {
|
||||||
return listUsernames();
|
return listUsernames();
|
||||||
}
|
}
|
||||||
|
|||||||
87
logic/wallet-balance.ts
Normal file
87
logic/wallet-balance.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { getUser } from "../config.js";
|
||||||
|
import { getWallet } from "../lib/cache/wallets.js";
|
||||||
|
import { getSigner } from "../lib/nostr/utils.js";
|
||||||
|
import { log } from "../lib/utils/log.js";
|
||||||
|
import { ndk } from "../ndk.js";
|
||||||
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the balance for a user's wallet
|
||||||
|
* @param username Username to check balance for
|
||||||
|
* @returns Balance information
|
||||||
|
*/
|
||||||
|
export async function getWalletBalance(username: string): Promise<{
|
||||||
|
balance: number;
|
||||||
|
mint_balances: Record<string, number>;
|
||||||
|
message: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
// Get the appropriate signer based on username
|
||||||
|
const signer = await getSigner(username);
|
||||||
|
|
||||||
|
// Set the signer for this operation
|
||||||
|
ndk.signer = signer;
|
||||||
|
|
||||||
|
const user = await signer.user();
|
||||||
|
const pubkey = user.pubkey;
|
||||||
|
|
||||||
|
// Get wallet for the pubkey
|
||||||
|
const wallet = await getWallet(pubkey, signer);
|
||||||
|
if (!wallet) {
|
||||||
|
throw new Error(`No wallet found for ${username}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get wallet balance
|
||||||
|
const totalBalance = wallet.balance?.amount || 0;
|
||||||
|
log(`Wallet balance for ${username}: ${totalBalance} sats`);
|
||||||
|
|
||||||
|
// Get individual mint balances
|
||||||
|
const mintBalances: Record<string, number> = {};
|
||||||
|
for (const [mintUrl, balance] of Object.entries(wallet.mintBalances)) {
|
||||||
|
mintBalances[mintUrl] = balance;
|
||||||
|
log(` - ${mintUrl}: ${balance} sats`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
balance: totalBalance,
|
||||||
|
mint_balances: mintBalances,
|
||||||
|
message: `Wallet balance for ${username}: ${totalBalance} sats`,
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
throw new Error(`Failed to get wallet balance: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the wallet-balance command to the MCP server
|
||||||
|
* @param server MCP server to add the command to
|
||||||
|
*/
|
||||||
|
export function addWalletBalanceCommand(server: McpServer) {
|
||||||
|
server.tool(
|
||||||
|
"wallet_balance",
|
||||||
|
"Get the balance of a user's wallet",
|
||||||
|
{
|
||||||
|
username: z.string().describe("Username to check balance for"),
|
||||||
|
},
|
||||||
|
async ({ username }) => {
|
||||||
|
const result = await getWalletBalance(username);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: result.message,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Include detailed balance information in the response
|
||||||
|
wallet_balance: {
|
||||||
|
total: result.balance,
|
||||||
|
mints: result.mint_balances,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
235
logic/zap.ts
Normal file
235
logic/zap.ts
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { NDKZapper } from "@nostr-dev-kit/ndk";
|
||||||
|
import type { CashuPaymentInfo, LnPaymentInfo, NDKEvent, NDKUser, NDKZapDetails } from "@nostr-dev-kit/ndk";
|
||||||
|
import type { NDKCashuWallet } from "@nostr-dev-kit/ndk-wallet";
|
||||||
|
import { ndk } from "../ndk.js";
|
||||||
|
import { getSigner } from "../lib/nostr/utils.js";
|
||||||
|
import { snippetsCache } from "../lib/cache/snippets.js";
|
||||||
|
import { getWallet } from "../lib/cache/wallets.js";
|
||||||
|
import { SNIPPET_KIND } from "../lib/nostr/utils.js";
|
||||||
|
import { toPubkeys } from "../lib/converters/users.js";
|
||||||
|
import { log } from "../lib/utils/log.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a snippet by title in the cache
|
||||||
|
* @param title The title to search for
|
||||||
|
* @returns Matching event ID or null if not found
|
||||||
|
*/
|
||||||
|
function findSnippetIdByTitle(title: string): string | null {
|
||||||
|
// First search in the cache (case insensitive)
|
||||||
|
const normalizedTitle = title.toLowerCase();
|
||||||
|
for (const id in snippetsCache) {
|
||||||
|
const snippet = snippetsCache[id];
|
||||||
|
if (snippet && snippet.title.toLowerCase() === normalizedTitle) {
|
||||||
|
return snippet.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a nutzap to an event, snippet, or user
|
||||||
|
* @param amount Amount in sats to send
|
||||||
|
* @param recipient Optional recipient (username, npub, or pubkey) to zap directly
|
||||||
|
* @param eventId Optional event ID to zap
|
||||||
|
* @param title Optional snippet title to look up and zap
|
||||||
|
* @param thanks_message Optional thank you message to include with the zap
|
||||||
|
* @param username Username to zap from (uses wallet associated with this user)
|
||||||
|
* @returns Zap results
|
||||||
|
*/
|
||||||
|
export async function sendZap(
|
||||||
|
amount: number,
|
||||||
|
recipient?: string,
|
||||||
|
eventId?: string,
|
||||||
|
title?: string,
|
||||||
|
thanks_message?: string,
|
||||||
|
username?: string
|
||||||
|
): Promise<{ content: Array<{ type: "text", text: string }> }> {
|
||||||
|
const userToZapFrom = username ?? "main";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the appropriate signer based on username
|
||||||
|
const signer = await getSigner(userToZapFrom);
|
||||||
|
|
||||||
|
// Set the signer for this operation
|
||||||
|
ndk.signer = signer;
|
||||||
|
|
||||||
|
// Get the user's pubkey
|
||||||
|
const user = await signer.user();
|
||||||
|
const senderPubkey = user.pubkey;
|
||||||
|
|
||||||
|
// Determine what to zap based on provided parameters
|
||||||
|
let targetEvent: NDKEvent | undefined;
|
||||||
|
let targetUser: NDKUser | undefined;
|
||||||
|
let zapTarget: NDKEvent | NDKUser;
|
||||||
|
let targetDescription: string;
|
||||||
|
|
||||||
|
// 1. If an event ID is provided, use that
|
||||||
|
if (eventId) {
|
||||||
|
if (eventId.length === 64 && /^[0-9a-f]+$/i.test(eventId)) {
|
||||||
|
const fetchedEvent = await ndk.fetchEvent(eventId);
|
||||||
|
if (!fetchedEvent) {
|
||||||
|
throw new Error(`Event not found with ID: ${eventId}`);
|
||||||
|
}
|
||||||
|
targetEvent = fetchedEvent;
|
||||||
|
targetDescription = `event: ${eventId}`;
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid event ID format");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2. If a title is provided, look up the matching event
|
||||||
|
else if (title) {
|
||||||
|
// Try to find in cache first
|
||||||
|
const cachedId = findSnippetIdByTitle(title);
|
||||||
|
|
||||||
|
if (cachedId) {
|
||||||
|
const fetchedEvent = await ndk.fetchEvent(cachedId);
|
||||||
|
if (!fetchedEvent) {
|
||||||
|
throw new Error(`Event with cached ID ${cachedId} not found`);
|
||||||
|
}
|
||||||
|
targetEvent = fetchedEvent;
|
||||||
|
targetDescription = `snippet: ${title}`;
|
||||||
|
} else {
|
||||||
|
// Not in cache, search on the network
|
||||||
|
console.log(`Searching for snippet with title: ${title}`);
|
||||||
|
|
||||||
|
// Search for events with this title
|
||||||
|
const events = await ndk.fetchEvents({
|
||||||
|
kinds: [SNIPPET_KIND as number],
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find the first event where the title matches
|
||||||
|
for (const event of events) {
|
||||||
|
const eventTitle = event.tagValue("title") ?? event.tagValue("name");
|
||||||
|
if (eventTitle && eventTitle.toLowerCase() === title.toLowerCase()) {
|
||||||
|
targetEvent = event;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetEvent) {
|
||||||
|
throw new Error(`No snippet found with title: ${title}`);
|
||||||
|
}
|
||||||
|
targetDescription = `snippet: ${title}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 3. If a recipient is provided, get the user
|
||||||
|
else if (recipient) {
|
||||||
|
const pubkeys = toPubkeys(recipient);
|
||||||
|
if (pubkeys.length === 0) {
|
||||||
|
throw new Error(`No user found for identifier: ${recipient}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
targetUser = ndk.getUser({ pubkey: pubkeys[0] });
|
||||||
|
targetDescription = `user: ${recipient} (${pubkeys[0]})`;
|
||||||
|
}
|
||||||
|
// 4. If none are provided, throw an error
|
||||||
|
else {
|
||||||
|
throw new Error("Must provide either an event ID, snippet title, or recipient to zap");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the zap target based on what was provided
|
||||||
|
if (targetEvent) {
|
||||||
|
zapTarget = targetEvent;
|
||||||
|
} else if (targetUser) {
|
||||||
|
zapTarget = targetUser;
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to determine zap target");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get wallet for this user
|
||||||
|
console.log(`Getting wallet for user: ${senderPubkey}`);
|
||||||
|
const wallet = await getWallet(senderPubkey, signer);
|
||||||
|
|
||||||
|
if (!wallet) {
|
||||||
|
throw new Error("No NIP-60 wallet found for this user. Create a wallet first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wallet.balance === undefined || wallet.balance.amount < amount) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `I don't have enough money, damn, man. I have ${wallet.balance?.amount ?? 0} sats.`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the zap using NDKZapper as explicitly requested
|
||||||
|
log(`Sending ${amount} sats zap to ${targetDescription}`);
|
||||||
|
|
||||||
|
ndk.wallet = wallet;
|
||||||
|
|
||||||
|
// Create an NDKZapper instance
|
||||||
|
// @ts-ignore - NDKZapper constructor signature may differ from types
|
||||||
|
const zapper = new NDKZapper(zapTarget, amount * 1000, 'msat', {
|
||||||
|
nutzapAsFallback: true,
|
||||||
|
comment: thanks_message,
|
||||||
|
cashuPay: (payment: NDKZapDetails<CashuPaymentInfo>) => {
|
||||||
|
log(`Cashu payment: ${JSON.stringify(payment)}`);
|
||||||
|
return wallet.cashuPay(payment);
|
||||||
|
},
|
||||||
|
lnPay: (payment: NDKZapDetails<LnPaymentInfo>) => {
|
||||||
|
log(`LN payment: ${JSON.stringify(payment)}`);
|
||||||
|
return wallet.lnPay(payment);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send the zap using NDKZapper as explicitly requested
|
||||||
|
// @ts-ignore - zap method parameters might differ from types
|
||||||
|
const ret = await zapper.zap(['nip61']);
|
||||||
|
log('return'+ ret?.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Successfully sent ${amount} sats zap to ${targetDescription}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
throw new Error(`Failed to send zap: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addZapCommand(server: McpServer) {
|
||||||
|
// Add zap tool
|
||||||
|
server.tool(
|
||||||
|
"zap",
|
||||||
|
"Send a nutzap to an event, snippet, or user using a NIP-60 wallet. Specify one of event_id, title, or recipient, no need for all of them.",
|
||||||
|
{
|
||||||
|
amount: z
|
||||||
|
.number()
|
||||||
|
.describe("Amount in sats to send"),
|
||||||
|
recipient: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Recipient (username, npub, or pubkey) to zap directly"),
|
||||||
|
event_id: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Event ID to zap"),
|
||||||
|
title: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Snippet title to look up and zap"),
|
||||||
|
thanks_message: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Optional thank you message to include with the zap"),
|
||||||
|
username: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("Username to zap from (uses wallet associated with this user)"),
|
||||||
|
},
|
||||||
|
async ({ amount, recipient, event_id, title, thanks_message, username }) => {
|
||||||
|
return await sendZap(amount, recipient, event_id, title, thanks_message, username);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
26
migrations/003-create-snippets-table.ts
Normal file
26
migrations/003-create-snippets-table.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { Database } from "bun:sqlite";
|
||||||
|
|
||||||
|
export async function up(db: Database): Promise<void> {
|
||||||
|
db.run(`
|
||||||
|
CREATE TABLE IF NOT EXISTS snippets (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL, -- Nostr event ID
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
code TEXT NOT NULL,
|
||||||
|
language TEXT NOT NULL,
|
||||||
|
pubkey TEXT NOT NULL, -- Author's pubkey
|
||||||
|
createdAt INTEGER NOT NULL, -- Unix timestamp
|
||||||
|
tags TEXT NOT NULL -- JSON string array of tags
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Optional: Add indexes for frequently queried columns
|
||||||
|
db.run('CREATE INDEX IF NOT EXISTS idx_snippets_pubkey ON snippets (pubkey)');
|
||||||
|
db.run('CREATE INDEX IF NOT EXISTS idx_snippets_language ON snippets (language)');
|
||||||
|
db.run('CREATE INDEX IF NOT EXISTS idx_snippets_createdAt ON snippets (createdAt)');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function down(db: Database): Promise<void> {
|
||||||
|
db.run("DROP TABLE IF EXISTS snippets");
|
||||||
|
}
|
||||||
6
ndk.ts
6
ndk.ts
@@ -22,9 +22,7 @@ export async function initNDK(config: ConfigData) {
|
|||||||
|
|
||||||
let signer: NDKSigner;
|
let signer: NDKSigner;
|
||||||
|
|
||||||
if (config.privateKey) {
|
if (config.bunker) {
|
||||||
signer = new NDKPrivateKeySigner(config.privateKey);
|
|
||||||
} else if (config.bunker) {
|
|
||||||
let localSigner: NDKPrivateKeySigner;
|
let localSigner: NDKPrivateKeySigner;
|
||||||
if (config.bunkerLocalKey) {
|
if (config.bunkerLocalKey) {
|
||||||
localSigner = new NDKPrivateKeySigner(config.bunkerLocalKey);
|
localSigner = new NDKPrivateKeySigner(config.bunkerLocalKey);
|
||||||
@@ -38,6 +36,8 @@ export async function initNDK(config: ConfigData) {
|
|||||||
|
|
||||||
signer = new NDKNip46Signer(ndk, config.bunker, localSigner);
|
signer = new NDKNip46Signer(ndk, config.bunker, localSigner);
|
||||||
await signer.blockUntilReady();
|
await signer.blockUntilReady();
|
||||||
|
} else if (config.privateKey) {
|
||||||
|
signer = new NDKPrivateKeySigner(config.privateKey);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("No private key or bunker provided");
|
throw new Error("No private key or bunker provided");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mcp-code",
|
"name": "mcp-code",
|
||||||
"version": "0.1.1",
|
"version": "0.2.0",
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -23,8 +23,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.7.0",
|
"@modelcontextprotocol/sdk": "^1.7.0",
|
||||||
"@nostr-dev-kit/ndk": "^2.13.0-rc2",
|
"@nostr-dev-kit/ndk": "2.13.1-rc2",
|
||||||
"@nostr-dev-kit/ndk-wallet": "0.5.0",
|
"@nostr-dev-kit/ndk-wallet": "0.5.3-1",
|
||||||
"commander": "^13.1.0",
|
"commander": "^13.1.0",
|
||||||
"inquirer": "^12.5.0",
|
"inquirer": "^12.5.0",
|
||||||
"yaml": "^2.7.0"
|
"yaml": "^2.7.0"
|
||||||
|
|||||||
58
project-structure.json
Normal file
58
project-structure.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"project_name": "MCP-Nostr",
|
||||||
|
"description": "A bridge between the Model Context Protocol (MCP) and the Nostr network, enabling AI language models to publish content to Nostr and interact with the Nostr ecosystem.",
|
||||||
|
"directory_structure": {
|
||||||
|
"commands/": "Contains command-line interface (CLI) command definitions using 'commander'. Each file represents a command or a group of related commands (e.g., `find-snippets.ts`, `wot.ts`). These commands are entry points for user interaction.",
|
||||||
|
"lib/": "Houses core libraries and reusable modules. Organized by functionality for better modularity and reduced code duplication. Subdirectories include:",
|
||||||
|
"lib/cache/": "In-memory caching mechanisms for performance optimization. Currently used for 'snippets' and 'wallets' to avoid redundant network requests.",
|
||||||
|
"lib/converters/": "Modules responsible for data conversion between different formats. Includes converters for 'snippets' and 'users', handling data transformation to and from Nostr events and internal data structures.",
|
||||||
|
"lib/nostr/": "Nostr-specific logic and utilities. Contains modules for interacting with Nostr, such as 'snippets' management and utility functions for Nostr events and data.",
|
||||||
|
"lib/types/": "TypeScript type definitions shared across the project, ensuring type safety and consistency.",
|
||||||
|
"lib/utils/": "General utility functions, like logging, used throughout the application.",
|
||||||
|
"logic/": "Implements the core business logic of the application. Each file typically represents a specific use case or feature, often corresponding to MCP commands or CLI actions (e.g., `create-pubkey.ts`, `publish-code-snippet.ts`). This layer orchestrates interactions between libraries and external services.",
|
||||||
|
"migrations/": "Database migration scripts for schema management using Bun SQLite. Ensures database schema evolution is tracked and applied consistently.",
|
||||||
|
"tests/": "Unit tests for specific functionalities. Currently includes tests for 'metadata-parser'.",
|
||||||
|
"utils/": "Utility functions that are specific to the top-level application, potentially acting as wrappers or adapters around 'lib/utils'. Currently redirects to `lib/utils/log.ts`.",
|
||||||
|
"root_level/": "Files at the project root define the application's entry point, configuration, database setup, and dependencies. Includes configuration files, package management, and the main application index."
|
||||||
|
},
|
||||||
|
"modules": {
|
||||||
|
"cli_commands": "Responsible for defining and parsing command-line arguments using the 'commander' library. Acts as the user interface for interacting with the application via the terminal. Located in `commands/`.",
|
||||||
|
"mcp_server": "Implements the Model Context Protocol server using `@modelcontextprotocol/sdk`. Enables the application to act as an MCP tool, receiving commands and sending responses in MCP format. Defined in `commands/mcp.ts` and logic functions in `logic/`.",
|
||||||
|
"nostr_interaction": "Handles all interactions with the Nostr network using `@nostr-dev-kit/ndk`. Includes publishing events, fetching data, and managing subscriptions. Modules are located in `lib/nostr/` and are used throughout the application.",
|
||||||
|
"data_conversion": "Provides modules to convert data between different formats, such as Nostr events to internal data structures and vice versa. Ensures data consistency across different layers of the application. Implemented in `lib/converters/`.",
|
||||||
|
"data_caching": "Implements in-memory caching to improve performance by reducing redundant data fetching. Currently used for snippets and wallets. Found in `lib/cache/`.",
|
||||||
|
"database_management": "Sets up and manages the SQLite database using Bun SQLite. Includes migration management for schema evolution and data persistence. Files are located in `db.ts` and `migrations/`.",
|
||||||
|
"configuration_management": "Handles application configuration, including reading from and writing to a configuration file. Manages API keys, relay lists, and user settings. Implemented in `config.ts` and `wizard.ts` for setup.",
|
||||||
|
"web_of_trust": "Implements Web of Trust functionality to manage social connections and reputation. Logic related to WoT is in `wot.ts` and data storage in the database.",
|
||||||
|
"user_management": "Manages user profiles, identities, and authentication. Includes user creation, profile management, and key handling. Logic is spread across `config.ts`, `users.ts`, and command logic in `logic/`."
|
||||||
|
},
|
||||||
|
"key_components": {
|
||||||
|
"cli": "The command-line interface, built using 'commander', provides users with direct access to application functionalities via terminal commands. Commands are defined in `commands/` and delegate logic to other modules.",
|
||||||
|
"mcp_server_integration": "The application integrates as an MCP server, allowing AI agents and other MCP-compatible tools to interact with it. This integration is primarily managed in `commands/mcp.ts` and logic functions in `logic/`.",
|
||||||
|
"nostr_ndk_integration": "NDK (@nostr-dev-kit/ndk) is the core library for interacting with the Nostr network. It handles event signing, publishing, subscription, and data retrieval. Integration is throughout the codebase, especially in `lib/nostr/` and `logic/`.",
|
||||||
|
"sqlite_database": "Bun SQLite is used for local data persistence, storing user profiles, Web of Trust data, and potentially other application-specific data. Database setup and migrations are handled in `db.ts` and `migrations/`.",
|
||||||
|
"configuration_file": "`~/.mcp-nostr.json` stores application configuration, including user credentials, relay lists, and enabled MCP commands. Configuration is managed by `config.ts` and set up via `wizard.ts`.",
|
||||||
|
"wizard_setup": "The `wizard.ts` module provides an interactive command-line wizard for first-time setup, guiding users through configuration and authentication setup."
|
||||||
|
},
|
||||||
|
"modularity_and_separation_of_concerns": "The codebase is designed with a strong emphasis on modularity and separation of concerns. This is achieved through:",
|
||||||
|
"modularity_principles": [
|
||||||
|
"Directory-based Module Organization: Functionality is grouped into directories based on domain (e.g., 'commands', 'lib', 'logic', 'migrations').",
|
||||||
|
"Library Modules ('lib/'): Reusable components are placed in 'lib/' to promote code reuse and reduce duplication. Subdirectories within 'lib/' further categorize functionality (e.g., 'cache', 'converters', 'nostr', 'utils').",
|
||||||
|
"Logic Layer ('logic/'): Business logic is separated from command handling and data access, making the code more maintainable and testable.",
|
||||||
|
"Command Layer ('commands/'): CLI command definitions are isolated, focusing on parsing user input and delegating actions to the logic layer.",
|
||||||
|
"Data Conversion Modules ('lib/converters/'): Data transformation logic is encapsulated in converter modules, separating data representation concerns.",
|
||||||
|
"Configuration Modules ('config.ts', 'wizard.ts'): Configuration management is handled by dedicated modules, isolating configuration loading, saving, and setup processes."
|
||||||
|
],
|
||||||
|
"technology_stack": [
|
||||||
|
"Bun.js": "JavaScript runtime environment",
|
||||||
|
"TypeScript": "Programming language",
|
||||||
|
"Model Context Protocol (MCP)": "Protocol for AI tool interaction",
|
||||||
|
"Nostr": "Decentralized social network protocol",
|
||||||
|
"@nostr-dev-kit/ndk": "Nostr Development Kit for JavaScript",
|
||||||
|
"@nostr-dev-kit/ndk-wallet": "NDK Wallet library for NIP-60 integration",
|
||||||
|
"@modelcontextprotocol/sdk": "MCP SDK for JavaScript",
|
||||||
|
"commander": "Node.js library for command-line interfaces",
|
||||||
|
"inquirer": "Node.js library for interactive command-line prompts",
|
||||||
|
"Bun SQLite": "SQLite database binding for Bun.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user