mirror of
https://github.com/aljazceru/mcp-code.git
synced 2025-12-17 04:35:19 +01:00
refactor and add list_snippets
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -32,3 +32,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|||||||
|
|
||||||
# Finder (MacOS) folder config
|
# Finder (MacOS) folder config
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
mcp-code
|
||||||
|
|||||||
@@ -1,43 +1,32 @@
|
|||||||
import { Command } from 'commander';
|
import { Command } from "commander";
|
||||||
import { ndk } from '../ndk.js';
|
import { toPubkeys, formatUser } from "../lib/converters/index.js";
|
||||||
import { knownUsers } from '../users.js';
|
|
||||||
import { identifierToPubkeys } from '../lib/nostr/utils.js';
|
|
||||||
|
|
||||||
export function registerFindUserCommand(program: Command): void {
|
// Create a command for finding a user
|
||||||
program
|
const findUserCommand = new Command("find-user")
|
||||||
.command('find-user')
|
.description("Find a user by name, npub, or other profile information")
|
||||||
.description('Find a user by identifier')
|
.argument("<query>", "The search query to find a user")
|
||||||
.argument('<query>', 'User identifier to search for')
|
.action(async (query: string) => {
|
||||||
.action(async (query: string) => {
|
try {
|
||||||
try {
|
// Find matching pubkeys
|
||||||
const pubkeys = identifierToPubkeys(query);
|
const pubkeys = toPubkeys(query);
|
||||||
|
|
||||||
if (pubkeys.length > 0) {
|
if (pubkeys.length === 0) {
|
||||||
const result = pubkeys.map(formatUser).join('\n\n---\n\n');
|
console.log(`No users found matching query: ${query}`);
|
||||||
console.log(result);
|
return;
|
||||||
} else {
|
|
||||||
console.log("No user found matching the query.");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error executing find-user command:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to format user profiles
|
// Format and display each matching user
|
||||||
function formatUser(pubkey: string) {
|
console.log(`Found ${pubkeys.length} matching users:`);
|
||||||
const profile = knownUsers[pubkey]?.profile;
|
for (let i = 0; i < pubkeys.length; i++) {
|
||||||
const user = ndk.getUser({ pubkey });
|
if (i > 0) console.log("\n---\n");
|
||||||
const keys: Record<string, string> = {
|
const pubkey = pubkeys[i];
|
||||||
Npub: user.npub,
|
if (pubkey) {
|
||||||
};
|
console.log(formatUser(pubkey));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (profile?.name) keys.Name = profile.name;
|
export default findUserCommand;
|
||||||
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");
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Command } from 'commander';
|
import { Command } from 'commander';
|
||||||
import { registerFindUserCommand } from './find-user.js';
|
|
||||||
import { registerFindSnippetsCommand } from './find-snippets.js';
|
import { registerFindSnippetsCommand } from './find-snippets.js';
|
||||||
import { registerWotCommand } from './wot.js';
|
import { registerWotCommand } from './wot.js';
|
||||||
import { registerListUsernamesCommand } from './list-usernames.js';
|
import { registerListUsernamesCommand } from './list-usernames.js';
|
||||||
@@ -17,7 +16,6 @@ program
|
|||||||
|
|
||||||
// Register all commands
|
// Register all commands
|
||||||
registerMcpCommand(program);
|
registerMcpCommand(program);
|
||||||
registerFindUserCommand(program);
|
|
||||||
registerFindSnippetsCommand(program);
|
registerFindSnippetsCommand(program);
|
||||||
registerWotCommand(program);
|
registerWotCommand(program);
|
||||||
registerListUsernamesCommand(program);
|
registerListUsernamesCommand(program);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Command } from 'commander';
|
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 { addFindSnippetsCommand } from "../logic/find_snippets.js";
|
import { addFindSnippetsCommand } from "../logic/find_snippets.js";
|
||||||
@@ -6,6 +6,8 @@ import { addFindUserCommand } from "../logic/find_user.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 { addFetchSnippetByIdCommand } from "../logic/fetch_snippet_by_id.js";
|
||||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
|
||||||
@@ -20,6 +22,8 @@ const commandMap: Record<string, CommandFunction> = {
|
|||||||
"find-user": addFindUserCommand,
|
"find-user": addFindUserCommand,
|
||||||
"find-snippets": addFindSnippetsCommand,
|
"find-snippets": addFindSnippetsCommand,
|
||||||
"list-usernames": addListUsernamesCommand,
|
"list-usernames": addListUsernamesCommand,
|
||||||
|
"list-snippets": addListSnippetsCommand,
|
||||||
|
"fetch-snippet-by-id": addFetchSnippetByIdCommand,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Global server instance
|
// Global server instance
|
||||||
@@ -70,5 +74,7 @@ export function registerMcpCommands(server: McpServer) {
|
|||||||
addFindUserCommand(server);
|
addFindUserCommand(server);
|
||||||
addFindSnippetsCommand(server);
|
addFindSnippetsCommand(server);
|
||||||
addListUsernamesCommand(server);
|
addListUsernamesCommand(server);
|
||||||
|
addListSnippetsCommand(server);
|
||||||
|
addFetchSnippetByIdCommand(server);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
lib/cache/snippets.ts
vendored
Normal file
7
lib/cache/snippets.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { CodeSnippet } from "../types/index.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-memory cache to store snippets by ID for efficient retrieval
|
||||||
|
* Used by list_snippets and fetch_snippet_by_id commands
|
||||||
|
*/
|
||||||
|
export const snippetsCache: Record<string, CodeSnippet> = {};
|
||||||
3
lib/converters/index.ts
Normal file
3
lib/converters/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Export all converters
|
||||||
|
export * from './snippets.js';
|
||||||
|
export * from './users.js';
|
||||||
195
lib/converters/snippets.ts
Normal file
195
lib/converters/snippets.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import type { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||||
|
import { knownUsers } from "../../users.js";
|
||||||
|
import type { CodeSnippet } from "../types/index.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an NDKEvent into a CodeSnippet
|
||||||
|
* @param event NDKEvent of kind 1337
|
||||||
|
* @returns CodeSnippet object
|
||||||
|
*/
|
||||||
|
export function toSnippet(event: NDKEvent): CodeSnippet {
|
||||||
|
const title = event.tagValue("title") ?? event.tagValue("name");
|
||||||
|
const description = event.tagValue("description") ?? "";
|
||||||
|
const language = event.tagValue("l");
|
||||||
|
const tags = event.tags
|
||||||
|
.filter((tag) => tag[0] === "t" && tag[1] !== undefined)
|
||||||
|
.map((tag) => tag[1] as string);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: event.id,
|
||||||
|
title: title || "Untitled",
|
||||||
|
description,
|
||||||
|
code: event.content,
|
||||||
|
language: language || "text",
|
||||||
|
pubkey: event.pubkey,
|
||||||
|
createdAt: event.created_at || 0,
|
||||||
|
tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a CodeSnippet to a formatted metadata string (without code)
|
||||||
|
* @param snippet CodeSnippet object
|
||||||
|
* @returns Formatted metadata string
|
||||||
|
*/
|
||||||
|
export function toMetadataString(snippet: CodeSnippet): string {
|
||||||
|
const { title, description, language, tags, id, pubkey, createdAt } = snippet;
|
||||||
|
const profile = knownUsers[pubkey]?.profile;
|
||||||
|
|
||||||
|
const returns = [
|
||||||
|
`ID: ${id}`,
|
||||||
|
`Title: ${title}`,
|
||||||
|
`Description: ${description}`,
|
||||||
|
`Language: ${language}`,
|
||||||
|
`Tags: ${tags.join(", ")}`,
|
||||||
|
`Created: ${new Date(createdAt * 1000).toISOString()}`,
|
||||||
|
`Pubkey: ${pubkey}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (profile?.name) returns.push(`Author: ${profile.name}`);
|
||||||
|
|
||||||
|
return returns.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a single snippet for display
|
||||||
|
* @param snippet The snippet to format
|
||||||
|
* @returns Formatted string representation
|
||||||
|
*/
|
||||||
|
export function formatSnippet(snippet: CodeSnippet): string {
|
||||||
|
return formatSnippets([snippet]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats snippets for display
|
||||||
|
* @param snippets Array of code snippets
|
||||||
|
* @returns Formatted string representation
|
||||||
|
*/
|
||||||
|
export function formatSnippets(snippets: CodeSnippet[]): string {
|
||||||
|
return snippets
|
||||||
|
.map((snippet) => {
|
||||||
|
const author = knownUsers[snippet.pubkey];
|
||||||
|
const keys: Record<string, string> = {
|
||||||
|
Title: snippet.title,
|
||||||
|
Language: snippet.language,
|
||||||
|
Tags: snippet.tags.join(", "),
|
||||||
|
Code: snippet.code,
|
||||||
|
};
|
||||||
|
if (author?.profile?.name) keys.Author = author.profile.name;
|
||||||
|
return Object.entries(keys)
|
||||||
|
.map(([key, value]) => `${key}: ${value}`)
|
||||||
|
.join("\n");
|
||||||
|
})
|
||||||
|
.join("\n\n---\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats partial match snippets for display
|
||||||
|
* @param snippets Array of code snippets
|
||||||
|
* @returns Formatted string representation
|
||||||
|
*/
|
||||||
|
export function formatPartialMatches(snippets: CodeSnippet[]): string {
|
||||||
|
if (snippets.length === 0) return "";
|
||||||
|
|
||||||
|
let text =
|
||||||
|
"\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
|
||||||
|
.map((snippet) => {
|
||||||
|
return ` * ${snippet.title}:\n Tags: ${snippet.tags.join(", ")}`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses metadata from a file content string
|
||||||
|
* @param fileContent String containing metadata and code sections
|
||||||
|
* @returns Object with metadata and code
|
||||||
|
*/
|
||||||
|
export function parseMetadataFromString(fileContent: string): {
|
||||||
|
metadata: { title: string; description: string; language: string; tags: string[] };
|
||||||
|
code: string
|
||||||
|
} {
|
||||||
|
// Match the metadata and code sections
|
||||||
|
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?.[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?.[1] ? languageMatch[1].trim() : "";
|
||||||
|
|
||||||
|
const tagsMatch = metadataSection.match(/^Tags:\s*(.+)$/m);
|
||||||
|
const tagsString = tagsMatch?.[1] ? tagsMatch[1].trim() : "";
|
||||||
|
const tags = tagsString.split(',').map(tag => tag.trim()).filter(Boolean);
|
||||||
|
|
||||||
|
return {
|
||||||
|
metadata: {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
language,
|
||||||
|
tags
|
||||||
|
},
|
||||||
|
code: codeSection
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a string with metadata and code sections
|
||||||
|
*/
|
||||||
|
export function createMetadataString(
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
44
lib/converters/users.ts
Normal file
44
lib/converters/users.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { ndk } from "../../ndk.js";
|
||||||
|
import { knownUsers } from "../../users.js";
|
||||||
|
import { queryUser } from "../../users.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an identifier (pubkey, npub, or name) to pubkeys
|
||||||
|
* @param identifier The identifier to convert
|
||||||
|
* @returns Array of pubkeys
|
||||||
|
*/
|
||||||
|
export function toPubkeys(identifier: string): string[] {
|
||||||
|
// If it's an npub, convert directly
|
||||||
|
if (identifier.startsWith("npub")) {
|
||||||
|
return [ndk.getUser({ npub: identifier }).pubkey];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a hex pubkey, return as is
|
||||||
|
if (identifier.length === 64 && /^[0-9a-f]+$/i.test(identifier)) {
|
||||||
|
return [identifier];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, search by profile name or other attributes
|
||||||
|
return queryUser(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format user profile data for display
|
||||||
|
* @param pubkey User public key
|
||||||
|
* @returns Formatted string representation
|
||||||
|
*/
|
||||||
|
export 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");
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk";
|
import type { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk";
|
||||||
import { ndk } from "../../ndk.js";
|
import { ndk } from "../../ndk.js";
|
||||||
import { knownUsers } from "../../users.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";
|
||||||
import { SNIPPET_KIND, eventToSnippet, identifierToPubkeys } from "./utils.js";
|
import { SNIPPET_KIND, identifierToPubkeys } from "./utils.js";
|
||||||
|
import { formatPartialMatches, formatSnippets, toSnippet } from "../converters/index.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get code snippets from Nostr events of kind 1337
|
* Get code snippets from Nostr events of kind 1337
|
||||||
@@ -87,50 +87,11 @@ export async function getSnippets(params: FindSnippetsParams = {}): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert events to snippets
|
// Convert events to snippets
|
||||||
const snippets = selectedEvents.map(eventToSnippet);
|
const snippets = selectedEvents.map(toSnippet);
|
||||||
const otherSnippets = notSelectedEvents.map(eventToSnippet);
|
const otherSnippets = notSelectedEvents.map(toSnippet);
|
||||||
|
|
||||||
return { snippets, otherSnippets };
|
return { snippets, otherSnippets };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Re-export formatters for backward compatibility
|
||||||
* Format snippets for display
|
export { formatSnippets, formatPartialMatches };
|
||||||
* @param snippets Array of code snippets
|
|
||||||
* @returns Formatted string representation
|
|
||||||
*/
|
|
||||||
export function formatSnippets(snippets: CodeSnippet[]): string {
|
|
||||||
return snippets
|
|
||||||
.map((snippet) => {
|
|
||||||
const author = knownUsers[snippet.pubkey];
|
|
||||||
const keys: Record<string, string> = {
|
|
||||||
Title: snippet.title,
|
|
||||||
Language: snippet.language,
|
|
||||||
Tags: snippet.tags.join(", "),
|
|
||||||
Code: snippet.code,
|
|
||||||
};
|
|
||||||
if (author?.profile?.name) keys.Author = author.profile.name;
|
|
||||||
return Object.entries(keys)
|
|
||||||
.map(([key, value]) => `${key}: ${value}`)
|
|
||||||
.join("\n");
|
|
||||||
})
|
|
||||||
.join("\n\n---\n\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format partial match snippets for display
|
|
||||||
* @param snippets Array of code snippets
|
|
||||||
* @returns Formatted string representation
|
|
||||||
*/
|
|
||||||
export function formatPartialMatches(snippets: CodeSnippet[]): string {
|
|
||||||
if (snippets.length === 0) return "";
|
|
||||||
|
|
||||||
let text =
|
|
||||||
"\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
|
|
||||||
.map((snippet) => {
|
|
||||||
return ` * ${snippet.title}:\n Tags: ${snippet.tags.join(", ")}`;
|
|
||||||
})
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import type { NDKEvent, NDKSigner } from "@nostr-dev-kit/ndk";
|
import type { NDKEvent, NDKSigner } from "@nostr-dev-kit/ndk";
|
||||||
import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||||
import { ndk } from "../../ndk.js";
|
import { ndk } from "../../ndk.js";
|
||||||
import { queryUser } from "../../users.js";
|
|
||||||
import type { CodeSnippet } from "../types/index.js";
|
|
||||||
import { getUser } from "../../config.js";
|
import { getUser } from "../../config.js";
|
||||||
|
import { toPubkeys, toSnippet } from "../converters/index.js";
|
||||||
|
|
||||||
export const SNIPPET_KIND = 1337;
|
export const SNIPPET_KIND = 1337;
|
||||||
|
|
||||||
@@ -32,45 +31,5 @@ export async function getSigner(username?: string): Promise<NDKSigner> {
|
|||||||
return new NDKPrivateKeySigner(userData.nsec);
|
return new NDKPrivateKeySigner(userData.nsec);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Re-export converter functions for backward compatibility
|
||||||
* Converts an identifier (pubkey, npub, or name) to pubkeys
|
export { toPubkeys as identifierToPubkeys, toSnippet as eventToSnippet };
|
||||||
* @param identifier The identifier to convert
|
|
||||||
* @returns Array of pubkeys
|
|
||||||
*/
|
|
||||||
export function identifierToPubkeys(identifier: string): string[] {
|
|
||||||
// If it's an npub, convert directly
|
|
||||||
if (identifier.startsWith("npub")) {
|
|
||||||
return [ndk.getUser({ npub: identifier }).pubkey];
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's a hex pubkey, return as is
|
|
||||||
if (identifier.length === 64 && /^[0-9a-f]+$/i.test(identifier)) {
|
|
||||||
return [identifier];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, search by profile name or other attributes
|
|
||||||
return queryUser(identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts an NDKEvent into a CodeSnippet
|
|
||||||
* @param event NDKEvent of kind 1337
|
|
||||||
* @returns CodeSnippet object
|
|
||||||
*/
|
|
||||||
export function eventToSnippet(event: NDKEvent): CodeSnippet {
|
|
||||||
const title = event.tagValue("title") ?? event.tagValue("name");
|
|
||||||
const language = event.tagValue("l");
|
|
||||||
const tags = event.tags
|
|
||||||
.filter((tag) => tag[0] === "t" && tag[1] !== undefined)
|
|
||||||
.map((tag) => tag[1] as string);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: event.id,
|
|
||||||
title: title || "Untitled",
|
|
||||||
code: event.content,
|
|
||||||
language: language || "text",
|
|
||||||
pubkey: event.pubkey,
|
|
||||||
createdAt: event.created_at || 0,
|
|
||||||
tags,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface FindSnippetsParams {
|
|||||||
export type CodeSnippet = {
|
export type CodeSnippet = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
description: string;
|
||||||
code: string;
|
code: string;
|
||||||
language: string;
|
language: string;
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
|
|||||||
78
logic/fetch_snippet_by_id.ts
Normal file
78
logic/fetch_snippet_by_id.ts
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,44 +1,52 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ndk } from "../ndk.js";
|
import { toPubkeys, formatUser } from "../lib/converters/index.js";
|
||||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import type { 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
|
* Find a user by name, npub, or other profile information
|
||||||
* @param query The search query to find a user
|
* @param query Search query to find a user
|
||||||
* @returns Results with formatted user information
|
* @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 {
|
try {
|
||||||
const pubkeys = queryUser(query);
|
// Convert the query to pubkeys
|
||||||
|
const pubkeys = toPubkeys(query);
|
||||||
|
|
||||||
if (pubkeys.length === 0) {
|
if (pubkeys.length === 0) {
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text",
|
||||||
text: "No users found matching the query.",
|
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");
|
const formattedUsers = pubkeys.map(formatUser).join("\n\n---\n\n");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text" as const,
|
type: "text",
|
||||||
text: `Found ${pubkeys.length} users:\n\n${formattedUsers}`,
|
text: formattedUsers,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : String(error);
|
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"),
|
query: z.string().describe("The search query to find a user"),
|
||||||
},
|
},
|
||||||
async ({ query }) => {
|
async ({ query }) => findUser(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");
|
|
||||||
}
|
|
||||||
|
|||||||
88
logic/list_snippets.ts
Normal file
88
logic/list_snippets.ts
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,15 +2,16 @@ import { NDKEvent } from "@nostr-dev-kit/ndk";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { SNIPPET_KIND, getSigner } from "../lib/nostr/utils.js";
|
import { SNIPPET_KIND, getSigner } from "../lib/nostr/utils.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";
|
||||||
import { writeFileSync, readFileSync } from "node:fs";
|
import { writeFileSync, readFileSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { homedir, tmpdir } from "node:os";
|
import { homedir, tmpdir } from "node:os";
|
||||||
import { readConfig, getUser } from "../config.js";
|
import { readConfig } from "../config.js";
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import * as Bun from "bun";
|
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
|
// append to ~/.nmcp-nostr.log
|
||||||
const logFilePath = join(homedir(), ".nmcp-nostr.log");
|
const logFilePath = join(homedir(), ".nmcp-nostr.log");
|
||||||
const logMessage = `${new Date().toISOString()} - ${message}\n`;
|
const logMessage = `${new Date().toISOString()} - ${message}\n`;
|
||||||
@@ -134,25 +135,12 @@ export async function publishCodeSnippet(
|
|||||||
username?: string
|
username?: string
|
||||||
): Promise<{ content: Array<{ type: "text", text: string }> }> {
|
): Promise<{ content: Array<{ type: "text", text: string }> }> {
|
||||||
try {
|
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
|
// 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 config = readConfig();
|
||||||
const tempFilePath = join(tmpdir(), `snippet-${Date.now()}.${language}`);
|
const tempFilePath = join(tmpdir(), `snippet-${Date.now()}.${language}`);
|
||||||
|
|
||||||
// Create file content with metadata section for editing
|
// 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
|
// Write the content to the temp file
|
||||||
writeFileSync(tempFilePath, fileContent);
|
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
|
// Spawn the editor process - first arg is the command array including both the command and its arguments
|
||||||
const process = Bun.spawn([...editorCommand, tempFilePath]);
|
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
|
// Wait for the editor to close
|
||||||
await process.exited;
|
await process.exited;
|
||||||
@@ -178,21 +166,21 @@ export async function publishCodeSnippet(
|
|||||||
if (existsSync(tempFilePath)) {
|
if (existsSync(tempFilePath)) {
|
||||||
const updatedContent = readFileSync(tempFilePath, "utf-8");
|
const updatedContent = readFileSync(tempFilePath, "utf-8");
|
||||||
try {
|
try {
|
||||||
log("updatedContent: " + updatedContent);
|
log(`updatedContent: ${updatedContent}`);
|
||||||
const parsed = parseMetadata(updatedContent);
|
const parsed = parseMetadataFromString(updatedContent);
|
||||||
updatedTitle = parsed.metadata.title || title;
|
updatedTitle = parsed.metadata.title || title;
|
||||||
updatedDescription = parsed.metadata.description || description;
|
updatedDescription = parsed.metadata.description || description;
|
||||||
updatedLanguage = parsed.metadata.language || language;
|
updatedLanguage = parsed.metadata.language || language;
|
||||||
updatedTags = parsed.metadata.tags.length >= 5 ? parsed.metadata.tags : tags;
|
updatedTags = parsed.metadata.tags.length >= 5 ? parsed.metadata.tags : tags;
|
||||||
updatedCode = parsed.code;
|
updatedCode = parsed.code;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log("error " + error);
|
log(`error ${error}`);
|
||||||
console.error("Error parsing metadata:", error);
|
console.error("Error parsing metadata:", error);
|
||||||
// Fallback to using the file content as just code if metadata parsing fails
|
// Fallback to using the file content as just code if metadata parsing fails
|
||||||
updatedCode = updatedContent;
|
updatedCode = updatedContent;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log("tempFilePath does not exist", tempFilePath);
|
log(`tempFilePath does not exist ${tempFilePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventTags = [
|
const eventTags = [
|
||||||
|
|||||||
Reference in New Issue
Block a user