refactor and add list_snippets

This commit is contained in:
pablof7z
2025-03-31 12:26:50 +01:00
parent 6d0e5a8e79
commit f74736191a
15 changed files with 494 additions and 190 deletions

View File

@@ -0,0 +1,78 @@
import { z } from "zod";
import { formatSnippets } from "../lib/converters/index.js";
import { ndk } from "../ndk.js";
import { SNIPPET_KIND } from "../lib/nostr/utils.js";
import { log } from "../lib/utils/log.js";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { toSnippet } from "../lib/converters/index.js";
/**
* Fetch a snippet by its ID
* @param id Snippet ID to fetch
* @returns The snippet content or an error message
*/
export async function fetchSnippetById(id: string): Promise<{
content: Array<{ type: "text"; text: string }>;
}> {
try {
log(`Fetching snippet with ID: ${id}`);
// Create filter for the specific event ID
const filter = {
kinds: [SNIPPET_KIND as number],
ids: [id],
};
// Fetch the event
const events = await ndk.fetchEvents(filter);
const event = Array.from(events)[0]; // Get the first (and should be only) event
if (!event) {
return {
content: [
{
type: "text",
text: `No snippet found with ID: ${id}`,
},
],
};
}
// Convert event to snippet
const snippet = toSnippet(event);
// Format the snippet for display
const formattedSnippet = formatSnippets([snippet]);
return {
content: [
{
type: "text",
text: formattedSnippet,
},
],
};
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: `Error fetching snippet: ${errorMessage}`,
},
],
};
}
}
export function addFetchSnippetByIdCommand(server: McpServer) {
server.tool(
"fetch_snippet_by_id",
"Fetch and display a snippet by its ID",
{
id: z.string().describe("ID of the snippet to fetch"),
},
async ({ id }) => fetchSnippetById(id)
);
}

View File

@@ -1,44 +1,52 @@
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";
import { toPubkeys, formatUser } from "../lib/converters/index.js";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.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
* @param query Search query to find a user
* @returns Formatted user data or error message
*/
export async function findUser(query: string) {
export async function findUser(query: string): Promise<{
content: Array<{ type: "text"; text: string }>;
}> {
try {
const pubkeys = queryUser(query);
// Convert the query to pubkeys
const pubkeys = toPubkeys(query);
if (pubkeys.length === 0) {
return {
content: [
{
type: "text" as const,
text: "No users found matching the query.",
type: "text",
text: `No users found matching: ${query}`,
},
],
};
}
// Format the found users for display
// Format user data for each pubkey
const formattedUsers = pubkeys.map(formatUser).join("\n\n---\n\n");
return {
content: [
{
type: "text" as const,
text: `Found ${pubkeys.length} users:\n\n${formattedUsers}`,
type: "text",
text: formattedUsers,
},
],
};
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new Error(`Failed to find user: ${errorMessage}`);
return {
content: [
{
type: "text",
text: `Error finding user: ${errorMessage}`,
},
],
};
}
}
@@ -49,29 +57,7 @@ export function addFindUserCommand(server: McpServer) {
{
query: z.string().describe("The search query to find a user"),
},
async ({ query }) => {
return findUser(query);
}
async ({ query }) => 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");
}

88
logic/list_snippets.ts Normal file
View File

@@ -0,0 +1,88 @@
import { z } from "zod";
import type { CodeSnippet, FindSnippetsParams } from "../lib/types/index.js";
import { getSnippets } from "../lib/nostr/snippets.js";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { toMetadataString } from "../lib/converters/index.js";
/**
* List snippets with metadata
* @param params Parameters to filter snippets
* @returns List of snippet metadata
*/
export async function listSnippets(
params: FindSnippetsParams = {}
): Promise<{ content: Array<{ type: "text", text: string }> }> {
try {
const result = await getSnippets(params);
let list = result.snippets
.map(toMetadataString)
.join("\n\n------------------\n\n");
const extra = result.otherSnippets
.map(toMetadataString)
.join("\n\n------------------\n\n");
// Include partial matches if they exist
if (result.otherSnippets.length > 0) {
list += "\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";
list += extra;
}
if (list.length === 0) {
return {
content: [
{
type: "text",
text: "No snippets found",
},
],
};
}
return {
content: [
{
type: "text",
text: list,
},
],
};
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : String(error);
throw new Error(`Failed to list snippets: ${errorMessage}`);
}
}
export function addListSnippetsCommand(server: McpServer) {
server.tool(
"list_snippets",
"List code snippets metadata (without code content) with filtering by language and tags. Use this to get a large list of available code snippets.",
{
since: z
.number()
.optional()
.describe("Fetch snippets newer than this timestamp"),
until: z
.number()
.optional()
.describe("Fetch snippets older than this timestamp"),
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' ], we use OR matches"
),
},
async (args) => listSnippets(args)
);
}

View File

@@ -2,15 +2,16 @@ 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 type { 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 { readConfig } from "../config.js";
import { existsSync } from "node:fs";
import * as Bun from "bun";
import { createMetadataString, parseMetadataFromString } from "../lib/converters/index.js";
function log(message: string, ...args: any[]) {
function log(message: string): void {
// append to ~/.nmcp-nostr.log
const logFilePath = join(homedir(), ".nmcp-nostr.log");
const logMessage = `${new Date().toISOString()} - ${message}\n`;
@@ -134,25 +135,12 @@ export async function publishCodeSnippet(
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);
const fileContent = createMetadataString(title, description, language, tags, code);
// Write the content to the temp file
writeFileSync(tempFilePath, fileContent);
@@ -163,7 +151,7 @@ export async function publishCodeSnippet(
// 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);
log(`spawned editor process to edit ${tempFilePath}`);
// Wait for the editor to close
await process.exited;
@@ -178,21 +166,21 @@ export async function publishCodeSnippet(
if (existsSync(tempFilePath)) {
const updatedContent = readFileSync(tempFilePath, "utf-8");
try {
log("updatedContent: " + updatedContent);
const parsed = parseMetadata(updatedContent);
log(`updatedContent: ${updatedContent}`);
const parsed = parseMetadataFromString(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);
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);
log(`tempFilePath does not exist ${tempFilePath}`);
}
const eventTags = [