Skip to main content
Glama

MCP-NOSTR

by pablof7z
publish-code-snippet.ts9.2 kB
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 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 } from "../config.js"; import { existsSync } from "node:fs"; import { spawn } from "child_process"; import { createMetadataString, parseMetadataFromString } from "../lib/converters/index.js"; function log(message: string): void { // append to ~/.ntenex-tools.log const logFilePath = join(homedir(), ".ntenex-tools.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 { // 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 = createMetadataString(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 using Node.js child_process await new Promise<void>((resolve, reject) => { const command = editorCommand[0] || "code"; const args = [...editorCommand.slice(1), tempFilePath]; const child = spawn(command, args, { stdio: "inherit" }); log(`spawned editor process to edit ${tempFilePath}`); child.on("exit", (code: number | null) => { if (code === 0) { resolve(); } else { reject(new Error(`Editor process exited with code ${code}`)); } }); child.on("error", (err: Error) => reject(err)); }); // 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 = 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}`); 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); } ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pablof7z/mcp-code'

If you have feedback or need assistance with the MCP directory API, please join our Discord server