mirror of
https://github.com/aljazceru/mcp-code.git
synced 2025-12-17 04:35:19 +01:00
initial commit
This commit is contained in:
100
logic/create-pubkey.ts
Normal file
100
logic/create-pubkey.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import NDK, { NDKPrivateKeySigner, NDKUser, NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { z } from "zod";
|
||||
import { getUser, saveUser } from "../config.js";
|
||||
import { log } from "../lib/utils/log.js";
|
||||
import { ndk } from "../ndk.js";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
|
||||
export async function createPubkey({
|
||||
username,
|
||||
display_name,
|
||||
about,
|
||||
}: {
|
||||
username: string;
|
||||
display_name: string;
|
||||
about?: string;
|
||||
}) {
|
||||
try {
|
||||
// Check if username already exists
|
||||
const existingUser = getUser(username);
|
||||
if (existingUser) {
|
||||
throw new Error(`Username "${username}" already exists`);
|
||||
}
|
||||
|
||||
// Generate a new keypair for this user
|
||||
const signer = NDKPrivateKeySigner.generate();
|
||||
|
||||
// Create profile metadata event (kind 0)
|
||||
const profileContent = JSON.stringify({
|
||||
display_name,
|
||||
name: display_name,
|
||||
about: about || "",
|
||||
});
|
||||
|
||||
// Create the event
|
||||
const event = new NDKEvent(ndk, {
|
||||
kind: 0,
|
||||
content: profileContent,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
// Sign with the new signer
|
||||
await event.sign(signer);
|
||||
|
||||
// Publish the event
|
||||
await event.publish();
|
||||
|
||||
// Save the user to the config
|
||||
saveUser(username, {
|
||||
nsec: signer.privateKey,
|
||||
display_name,
|
||||
about: about || "",
|
||||
});
|
||||
|
||||
// Create user object for return
|
||||
const user = ndk.getUser({ pubkey: signer.pubkey });
|
||||
|
||||
// Return success message
|
||||
log(`Created pubkey for ${username} with public key ${signer.pubkey}`);
|
||||
|
||||
return {
|
||||
user,
|
||||
message: `Created pubkey for ${username} with npub ${user.npub}`,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to create pubkey: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function addCreatePubkeyCommand(server: McpServer) {
|
||||
server.tool(
|
||||
"create_pubkey",
|
||||
"Create a new keypair and save it to config",
|
||||
{
|
||||
username: z.string().describe("Username to create"),
|
||||
display_name: z.string().describe("Display name for the pubkey"),
|
||||
about: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("About information for this pubkey"),
|
||||
},
|
||||
async ({ username, display_name, about }) => {
|
||||
const result = await createPubkey({
|
||||
username,
|
||||
display_name,
|
||||
about,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: result.message,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
95
logic/find_snippets.ts
Normal file
95
logic/find_snippets.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
formatPartialMatches,
|
||||
formatSnippets,
|
||||
getSnippets,
|
||||
} from "../lib/nostr/snippets.js";
|
||||
import type { FindSnippetsParams } from "../lib/types/index.js";
|
||||
import { log } from "../lib/utils/log.js";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
|
||||
/**
|
||||
* Find code snippets with optional filtering
|
||||
* @param params Search parameters for filtering snippets
|
||||
* @returns Formatted snippets that match the criteria
|
||||
*/
|
||||
export async function findSnippets({
|
||||
since,
|
||||
until,
|
||||
authors,
|
||||
languages,
|
||||
tags,
|
||||
}: FindSnippetsParams) {
|
||||
try {
|
||||
const { snippets, otherSnippets } = await getSnippets({
|
||||
limit: 500, // Get more snippets to find the max matches
|
||||
since,
|
||||
until,
|
||||
authors,
|
||||
languages,
|
||||
tags,
|
||||
});
|
||||
|
||||
if (snippets.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: "No code snippets found matching the criteria.",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Format snippets for display
|
||||
const formattedSnippets = formatSnippets(snippets);
|
||||
const partialMatchesText = formatPartialMatches(otherSnippets);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: `Found ${snippets.length} code snippets:\n\n${formattedSnippets}${partialMatchesText}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to find snippets: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function addFindSnippetsCommand(server: McpServer) {
|
||||
server.tool(
|
||||
"find_snippets",
|
||||
"Find code snippets with optional filtering by author, language, and tags",
|
||||
{
|
||||
since: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Unix timestamp to fetch snippets from"),
|
||||
until: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Unix timestamp to fetch snippets until"),
|
||||
authors: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
"List of author names to filter by (in username format!)"
|
||||
),
|
||||
languages: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("List of programming languages to filter by"),
|
||||
tags: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
"List of tags to filter by, be exhaustive, e.g. [ 'ndk', 'nostr', 'pubkey', 'signer' ]"
|
||||
),
|
||||
},
|
||||
async (args) => findSnippets(args)
|
||||
);
|
||||
}
|
||||
77
logic/find_user.ts
Normal file
77
logic/find_user.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { z } from "zod";
|
||||
import { ndk } from "../ndk.js";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { knownUsers } from "../users.js";
|
||||
import { queryUser } from "../users.js";
|
||||
|
||||
/**
|
||||
* Find a user by name, npub, or other profile information
|
||||
* @param query The search query to find a user
|
||||
* @returns Results with formatted user information
|
||||
*/
|
||||
export async function findUser(query: string) {
|
||||
try {
|
||||
const pubkeys = queryUser(query);
|
||||
|
||||
if (pubkeys.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: "No users found matching the query.",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Format the found users for display
|
||||
const formattedUsers = pubkeys.map(formatUser).join("\n\n---\n\n");
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: `Found ${pubkeys.length} users:\n\n${formattedUsers}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to find user: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function addFindUserCommand(server: McpServer) {
|
||||
server.tool(
|
||||
"find_user",
|
||||
"Find a user by name, npub, or other profile information",
|
||||
{
|
||||
query: z.string().describe("The search query to find a user"),
|
||||
},
|
||||
async ({ query }) => {
|
||||
return findUser(query);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format user profile data for display
|
||||
* @param pubkey User public key
|
||||
* @returns Formatted string representation
|
||||
*/
|
||||
function formatUser(pubkey: string): string {
|
||||
const profile = knownUsers[pubkey]?.profile;
|
||||
const user = ndk.getUser({ pubkey });
|
||||
const keys: Record<string, string> = {
|
||||
Npub: user.npub,
|
||||
};
|
||||
|
||||
if (profile?.name) keys.Name = profile.name;
|
||||
if (profile?.about) keys.About = profile.about;
|
||||
if (profile?.picture) keys.Picture = profile.picture;
|
||||
|
||||
return Object.entries(keys)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join("\n");
|
||||
}
|
||||
68
logic/list_usernames.ts
Normal file
68
logic/list_usernames.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { z } from "zod";
|
||||
import { ndk } from "../ndk.js";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { getAllUsers } from "../config.js";
|
||||
import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||
|
||||
/**
|
||||
* List all available usernames in the system
|
||||
* @returns Formatted list of available usernames
|
||||
*/
|
||||
export async function listUsernames() {
|
||||
try {
|
||||
const users = await Promise.all(
|
||||
Object.values(getAllUsers()).map(async (user) => {
|
||||
const signer = new NDKPrivateKeySigner(user.nsec);
|
||||
const npub = (await signer.user()).npub;
|
||||
return {
|
||||
name: user.display_name,
|
||||
npub: npub,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
if (users.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: "No users found. Try creating a user with the create_pubkey command first.",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const usersList = users
|
||||
.map((user) => `${user.name} - ${user.npub}`)
|
||||
.join("\n");
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: `Available users (${users.length}):\n${usersList}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to list usernames: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function addListUsernamesCommand(server: McpServer) {
|
||||
server.tool(
|
||||
"list_usernames",
|
||||
"List all available usernames in the system",
|
||||
{
|
||||
random_string: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Dummy parameter for no-parameter tools"),
|
||||
},
|
||||
async () => {
|
||||
return listUsernames();
|
||||
}
|
||||
);
|
||||
}
|
||||
253
logic/publish-code-snippet.ts
Normal file
253
logic/publish-code-snippet.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { z } from "zod";
|
||||
import { SNIPPET_KIND, getSigner } from "../lib/nostr/utils.js";
|
||||
import { ndk } from "../ndk.js";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { writeFileSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import { readConfig, getUser } from "../config.js";
|
||||
import { existsSync } from "node:fs";
|
||||
import * as Bun from "bun";
|
||||
|
||||
function log(message: string, ...args: any[]) {
|
||||
// append to ~/.nmcp-nostr.log
|
||||
const logFilePath = join(homedir(), ".nmcp-nostr.log");
|
||||
const logMessage = `${new Date().toISOString()} - ${message}\n`;
|
||||
writeFileSync(logFilePath, logMessage, { flag: "a" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse metadata from the beginning of a file
|
||||
* Format:
|
||||
* ---METADATA---
|
||||
* Title: My Title
|
||||
* Description: My description goes here...
|
||||
* Language: javascript
|
||||
* Tags: tag1, tag2, tag3, tag4, tag5
|
||||
* ---CODE---
|
||||
*/
|
||||
export function parseMetadata(fileContent: string): {
|
||||
metadata: { title: string; description: string; language: string; tags: string[] };
|
||||
code: string
|
||||
} {
|
||||
// Match the metadata and code sections - this regex was matching incorrectly
|
||||
// Making it non-greedy for the first part and fixing the boundary for CODE marker
|
||||
const metadataRegex = /^---METADATA---([\s\S]*?)(?=^---CODE---$)(^---CODE---$)([\s\S]*)$/m;
|
||||
const matches = fileContent.match(metadataRegex);
|
||||
|
||||
if (!matches || matches.length < 4) {
|
||||
throw new Error("Invalid file format: metadata section not found");
|
||||
}
|
||||
|
||||
const metadataSection = matches[1] || "";
|
||||
let codeSection = matches[3] || "";
|
||||
|
||||
// Remove leading newline from code section if present
|
||||
if (codeSection.startsWith("\n")) {
|
||||
codeSection = codeSection.substring(1);
|
||||
}
|
||||
|
||||
// Parse each field with proper multiline flag
|
||||
const titleMatch = metadataSection.match(/^Title:\s*(.+)$/m);
|
||||
const title = titleMatch && titleMatch[1] ? titleMatch[1].trim() : "";
|
||||
|
||||
// Extract description which can be multiline but should stop at Language: or Tags:
|
||||
const descriptionLines = [];
|
||||
let inDescription = false;
|
||||
|
||||
// Process line by line
|
||||
const lines = metadataSection.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.trim().startsWith('Description:')) {
|
||||
inDescription = true;
|
||||
const content = line.replace(/^Description:\s*/, '').trim();
|
||||
if (content) {
|
||||
descriptionLines.push(content);
|
||||
}
|
||||
} else if (line.trim().startsWith('Language:') || line.trim().startsWith('Tags:')) {
|
||||
inDescription = false;
|
||||
} else if (inDescription) {
|
||||
descriptionLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
const description = descriptionLines.join('\n').trim();
|
||||
|
||||
const languageMatch = metadataSection.match(/^Language:\s*(.+)$/m);
|
||||
const language = languageMatch && languageMatch[1] ? languageMatch[1].trim() : "";
|
||||
|
||||
const tagsMatch = metadataSection.match(/^Tags:\s*(.+)$/m);
|
||||
const tagsString = tagsMatch && tagsMatch[1] ? tagsMatch[1].trim() : "";
|
||||
const tags = tagsString.split(',').map(tag => tag.trim()).filter(Boolean);
|
||||
|
||||
return {
|
||||
metadata: {
|
||||
title,
|
||||
description,
|
||||
language,
|
||||
tags
|
||||
},
|
||||
code: codeSection
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a file with metadata and code sections
|
||||
*/
|
||||
export function createFileWithMetadata(
|
||||
title: string,
|
||||
description: string,
|
||||
language: string,
|
||||
tags: string[],
|
||||
code: string
|
||||
): string {
|
||||
return `---METADATA---
|
||||
# Edit the metadata below. Keep the format exactly as shown (Title:, Description:, Language:, Tags:)
|
||||
# Description needs to be at least 140 characters and Tags need at least 5 entries
|
||||
# Don't remove the ---METADATA--- and ---CODE--- markers!
|
||||
|
||||
Title: ${title}
|
||||
Description: ${description}
|
||||
Language: ${language}
|
||||
Tags: ${tags.join(', ')}
|
||||
---CODE---
|
||||
${code}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a code snippet to Nostr
|
||||
* @param title Title of the snippet
|
||||
* @param description Description of the snippet
|
||||
* @param language Programming language
|
||||
* @param code The code snippet content
|
||||
* @param tags Tags to categorize the snippet
|
||||
* @param username Username to publish as
|
||||
* @returns Publication results
|
||||
*/
|
||||
export async function publishCodeSnippet(
|
||||
title: string,
|
||||
description: string,
|
||||
language: string,
|
||||
code: string,
|
||||
tags: string[] = [],
|
||||
username?: string
|
||||
): Promise<{ content: Array<{ type: "text", text: string }> }> {
|
||||
try {
|
||||
// Validate minimum requirements
|
||||
// if (tags.length < 5) {
|
||||
// throw new Error(
|
||||
// "Insufficient tags. At least 5 tags are required. Please add more relevant and accurate information."
|
||||
// );
|
||||
// }
|
||||
|
||||
// if (description.length < 140) {
|
||||
// throw new Error(
|
||||
// "Description is too short. At least 140 characters are required. Please add more relevant and accurate information."
|
||||
// );
|
||||
// }
|
||||
|
||||
// put the code snippet in a temp file and run the command in config.editor or `code` and wait until it's closed -- then read the file and publish it
|
||||
const config = readConfig();
|
||||
const tempFilePath = join(tmpdir(), `snippet-${Date.now()}.${language}`);
|
||||
|
||||
// Create file content with metadata section for editing
|
||||
const fileContent = createFileWithMetadata(title, description, language, tags, code);
|
||||
|
||||
// Write the content to the temp file
|
||||
writeFileSync(tempFilePath, fileContent);
|
||||
|
||||
// Use the editor specified in config, or default to 'code' (VS Code)
|
||||
const editorCommand = (config.editor || 'code --wait').split(' ');
|
||||
|
||||
// Spawn the editor process - first arg is the command array including both the command and its arguments
|
||||
const process = Bun.spawn([...editorCommand, tempFilePath]);
|
||||
|
||||
log("spawned editor process to edit " + tempFilePath);
|
||||
|
||||
// Wait for the editor to close
|
||||
await process.exited;
|
||||
|
||||
// Read the potentially modified content from the temp file
|
||||
let updatedTitle = title;
|
||||
let updatedDescription = description;
|
||||
let updatedLanguage = language;
|
||||
let updatedTags = tags;
|
||||
let updatedCode = code;
|
||||
|
||||
if (existsSync(tempFilePath)) {
|
||||
const updatedContent = readFileSync(tempFilePath, "utf-8");
|
||||
try {
|
||||
log("updatedContent: " + updatedContent);
|
||||
const parsed = parseMetadata(updatedContent);
|
||||
updatedTitle = parsed.metadata.title || title;
|
||||
updatedDescription = parsed.metadata.description || description;
|
||||
updatedLanguage = parsed.metadata.language || language;
|
||||
updatedTags = parsed.metadata.tags.length >= 5 ? parsed.metadata.tags : tags;
|
||||
updatedCode = parsed.code;
|
||||
} catch (error) {
|
||||
log("error " + error);
|
||||
console.error("Error parsing metadata:", error);
|
||||
// Fallback to using the file content as just code if metadata parsing fails
|
||||
updatedCode = updatedContent;
|
||||
}
|
||||
} else {
|
||||
log("tempFilePath does not exist", tempFilePath);
|
||||
}
|
||||
|
||||
const eventTags = [
|
||||
["name", updatedTitle],
|
||||
["description", updatedDescription],
|
||||
["l", updatedLanguage],
|
||||
...updatedTags.map((tag) => ["t", tag]),
|
||||
];
|
||||
|
||||
const event = new NDKEvent(ndk, {
|
||||
kind: SNIPPET_KIND,
|
||||
content: updatedCode,
|
||||
tags: eventTags,
|
||||
});
|
||||
|
||||
// Get the appropriate signer based on username
|
||||
const signer = await getSigner(username);
|
||||
|
||||
// Sign the event with the selected signer
|
||||
await event.sign(signer);
|
||||
|
||||
// Publish the already signed event
|
||||
await event.publish();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Published code snippet "${updatedTitle}" to Nostr: The snippet can be seeen in https://snipsnip.dev/snippet/${event.id} or https://njump.me/${event.encode()}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to publish code snippet: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function addPublishCodeSnippetCommand(server: McpServer) {
|
||||
server.tool(
|
||||
"publish-new-code-snippet",
|
||||
"Publish a new code snippet to Nostr",
|
||||
{
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
language: z.string(),
|
||||
code: z.string(),
|
||||
tags: z.array(z.string()),
|
||||
username: z.string().optional().describe(
|
||||
"Username to publish as (you can see list_usernames to see available usernames)"
|
||||
),
|
||||
},
|
||||
async ({ title, description, language, code, tags = [], username }, _extra) => {
|
||||
return publishCodeSnippet(title, description, language, code, tags, username);
|
||||
}
|
||||
);
|
||||
}
|
||||
139
logic/publish.ts
Normal file
139
logic/publish.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
NDKEvent
|
||||
} from "@nostr-dev-kit/ndk";
|
||||
import type { NDKFilter } from "@nostr-dev-kit/ndk";
|
||||
import { z } from "zod";
|
||||
import { identifierToPubkeys, getSigner } from "../lib/nostr/utils.js";
|
||||
import { ndk } from "../ndk.js";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { knownUsers } from "../users.js";
|
||||
|
||||
/**
|
||||
* Publish a note to Nostr
|
||||
* @param content The content of the note to publish
|
||||
* @param username Username to publish as
|
||||
* @param obey Array of pubkeys, npubs, or names to wait for replies from
|
||||
* @returns Publication results
|
||||
*/
|
||||
export async function publishNote(
|
||||
content: string,
|
||||
username?: string,
|
||||
obey?: string[]
|
||||
): Promise<{ content: Array<{ type: "text", text: string }> }> {
|
||||
const userToPublishAs = username ?? "main";
|
||||
|
||||
try {
|
||||
// Create the event
|
||||
const event = new NDKEvent(ndk, {
|
||||
kind: 1,
|
||||
content,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
// Get the appropriate signer based on username
|
||||
const signer = await getSigner(userToPublishAs);
|
||||
|
||||
// Sign the event with the selected signer
|
||||
await event.sign(signer);
|
||||
|
||||
// Publish the already signed event
|
||||
await event.publish();
|
||||
const eventId = event.id;
|
||||
|
||||
// If obey parameter exists, wait for replies from the specified users
|
||||
if (obey && obey.length > 0) {
|
||||
// Convert all identifiers to pubkeys
|
||||
const pubkeysToObey: string[] = [];
|
||||
for (const identifier of obey) {
|
||||
const pubkeys = identifierToPubkeys(identifier);
|
||||
if (pubkeys.length > 0) {
|
||||
pubkeysToObey.push(...pubkeys);
|
||||
}
|
||||
}
|
||||
|
||||
if (pubkeysToObey.length === 0) {
|
||||
throw new Error("No valid users found to obey");
|
||||
}
|
||||
|
||||
// Set up filter to listen for replies
|
||||
const filter: NDKFilter = {
|
||||
kinds: [1], // Normal notes
|
||||
authors: pubkeysToObey,
|
||||
"#e": [eventId], // References to our event
|
||||
};
|
||||
|
||||
// Wait for a reply
|
||||
return await new Promise((resolve, reject) => {
|
||||
// Set a timeout of 5 minutes
|
||||
const timeout = setTimeout(
|
||||
() => {
|
||||
sub.stop();
|
||||
reject(new Error("Timed out waiting for reply"));
|
||||
},
|
||||
5 * 60 * 1000
|
||||
);
|
||||
|
||||
// Subscribe to replies
|
||||
const sub = ndk.subscribe(filter);
|
||||
sub.on("event", (replyEvent) => {
|
||||
clearTimeout(timeout);
|
||||
sub.stop();
|
||||
|
||||
// Format the reply output
|
||||
const npub = replyEvent.author.npub;
|
||||
const name = knownUsers[replyEvent.pubkey]?.profile?.name;
|
||||
const replyFrom = name ? `${name} (${npub})` : npub;
|
||||
resolve({
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `${replyFrom} says:\n\n${replyEvent.content}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Published to Nostr with ID: ${event.encode()}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to publish: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function addPublishCommand(server: McpServer) {
|
||||
// Add publish tool
|
||||
server.tool(
|
||||
"publish",
|
||||
"Publish a tweet to Nostr",
|
||||
{
|
||||
content: z
|
||||
.string()
|
||||
.describe("The content of the tweet you want to publish"),
|
||||
username: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Username to publish as (you can see list_usernames to see available usernames)"
|
||||
),
|
||||
obey: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
"Array of pubkeys, npubs, or names to wait for replies from"
|
||||
),
|
||||
},
|
||||
async ({ content, username, obey }, _extra) => {
|
||||
return await publishNote(content, username, obey);
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user