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
|
||||
.DS_Store
|
||||
mcp-code
|
||||
|
||||
@@ -1,43 +1,32 @@
|
||||
import { Command } from 'commander';
|
||||
import { ndk } from '../ndk.js';
|
||||
import { knownUsers } from '../users.js';
|
||||
import { identifierToPubkeys } from '../lib/nostr/utils.js';
|
||||
import { Command } from "commander";
|
||||
import { toPubkeys, formatUser } from "../lib/converters/index.js";
|
||||
|
||||
export function registerFindUserCommand(program: Command): void {
|
||||
program
|
||||
.command('find-user')
|
||||
.description('Find a user by identifier')
|
||||
.argument('<query>', 'User identifier to search for')
|
||||
.action(async (query: string) => {
|
||||
try {
|
||||
const pubkeys = identifierToPubkeys(query);
|
||||
|
||||
if (pubkeys.length > 0) {
|
||||
const result = pubkeys.map(formatUser).join('\n\n---\n\n');
|
||||
console.log(result);
|
||||
} else {
|
||||
console.log("No user found matching the query.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error executing find-user command:', error);
|
||||
process.exit(1);
|
||||
// Create a command for finding a user
|
||||
const findUserCommand = new Command("find-user")
|
||||
.description("Find a user by name, npub, or other profile information")
|
||||
.argument("<query>", "The search query to find a user")
|
||||
.action(async (query: string) => {
|
||||
try {
|
||||
// Find matching pubkeys
|
||||
const pubkeys = toPubkeys(query);
|
||||
|
||||
if (pubkeys.length === 0) {
|
||||
console.log(`No users found matching query: ${query}`);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to format user profiles
|
||||
function formatUser(pubkey: string) {
|
||||
const profile = knownUsers[pubkey]?.profile;
|
||||
const user = ndk.getUser({ pubkey });
|
||||
const keys: Record<string, string> = {
|
||||
Npub: user.npub,
|
||||
};
|
||||
// Format and display each matching user
|
||||
console.log(`Found ${pubkeys.length} matching users:`);
|
||||
for (let i = 0; i < pubkeys.length; i++) {
|
||||
if (i > 0) console.log("\n---\n");
|
||||
const pubkey = pubkeys[i];
|
||||
if (pubkey) {
|
||||
console.log(formatUser(pubkey));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
}
|
||||
});
|
||||
|
||||
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");
|
||||
}
|
||||
export default findUserCommand;
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Command } from 'commander';
|
||||
import { registerFindUserCommand } from './find-user.js';
|
||||
import { registerFindSnippetsCommand } from './find-snippets.js';
|
||||
import { registerWotCommand } from './wot.js';
|
||||
import { registerListUsernamesCommand } from './list-usernames.js';
|
||||
@@ -17,7 +16,6 @@ program
|
||||
|
||||
// Register all commands
|
||||
registerMcpCommand(program);
|
||||
registerFindUserCommand(program);
|
||||
registerFindSnippetsCommand(program);
|
||||
registerWotCommand(program);
|
||||
registerListUsernamesCommand(program);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Command } from 'commander';
|
||||
import type { Command } from 'commander';
|
||||
import { readConfig } from "../config.js";
|
||||
import { addCreatePubkeyCommand } from "../logic/create-pubkey.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 { addPublishCodeSnippetCommand } from "../logic/publish-code-snippet.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 { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
|
||||
@@ -20,6 +22,8 @@ const commandMap: Record<string, CommandFunction> = {
|
||||
"find-user": addFindUserCommand,
|
||||
"find-snippets": addFindSnippetsCommand,
|
||||
"list-usernames": addListUsernamesCommand,
|
||||
"list-snippets": addListSnippetsCommand,
|
||||
"fetch-snippet-by-id": addFetchSnippetByIdCommand,
|
||||
};
|
||||
|
||||
// Global server instance
|
||||
@@ -70,5 +74,7 @@ export function registerMcpCommands(server: McpServer) {
|
||||
addFindUserCommand(server);
|
||||
addFindSnippetsCommand(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 { ndk } from "../../ndk.js";
|
||||
import { knownUsers } from "../../users.js";
|
||||
import type { CodeSnippet, FindSnippetsParams } from "../types/index.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
|
||||
@@ -87,50 +87,11 @@ export async function getSnippets(params: FindSnippetsParams = {}): Promise<{
|
||||
}
|
||||
|
||||
// Convert events to snippets
|
||||
const snippets = selectedEvents.map(eventToSnippet);
|
||||
const otherSnippets = notSelectedEvents.map(eventToSnippet);
|
||||
const snippets = selectedEvents.map(toSnippet);
|
||||
const otherSnippets = notSelectedEvents.map(toSnippet);
|
||||
|
||||
return { snippets, otherSnippets };
|
||||
}
|
||||
|
||||
/**
|
||||
* Format 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");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
// Re-export formatters for backward compatibility
|
||||
export { formatSnippets, formatPartialMatches };
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { NDKEvent, NDKSigner } from "@nostr-dev-kit/ndk";
|
||||
import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||
import { ndk } from "../../ndk.js";
|
||||
import { queryUser } from "../../users.js";
|
||||
import type { CodeSnippet } from "../types/index.js";
|
||||
import { getUser } from "../../config.js";
|
||||
import { toPubkeys, toSnippet } from "../converters/index.js";
|
||||
|
||||
export const SNIPPET_KIND = 1337;
|
||||
|
||||
@@ -32,45 +31,5 @@ export async function getSigner(username?: string): Promise<NDKSigner> {
|
||||
return new NDKPrivateKeySigner(userData.nsec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an identifier (pubkey, npub, or name) to pubkeys
|
||||
* @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,
|
||||
};
|
||||
}
|
||||
// Re-export converter functions for backward compatibility
|
||||
export { toPubkeys as identifierToPubkeys, toSnippet as eventToSnippet };
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface FindSnippetsParams {
|
||||
export type CodeSnippet = {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
code: string;
|
||||
language: 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 { 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
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 { 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 = [
|
||||
|
||||
Reference in New Issue
Block a user