From 1f9cdcd922851247e43aad3be9d7707413f0f927 Mon Sep 17 00:00:00 2001 From: Clawd Date: Thu, 5 Mar 2026 23:01:34 +0000 Subject: [PATCH] Add .gitignore, clean up node_modules from repo --- .gitignore | 4 + dist/index.d.ts | 8 - dist/index.js | 523 ------------------------------------------------ 3 files changed, 4 insertions(+), 531 deletions(-) create mode 100644 .gitignore delete mode 100644 dist/index.d.ts delete mode 100644 dist/index.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd8fe26 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log +.env diff --git a/dist/index.d.ts b/dist/index.d.ts deleted file mode 100644 index 52c720f..0000000 --- a/dist/index.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env node -/** - * Second Brain MCP Server - * - * MCP server to interact with the Second Brain RAG knowledge management system. - * Provides tools for adding logs, uploading documents, searching, and chatting. - */ -export {}; diff --git a/dist/index.js b/dist/index.js deleted file mode 100644 index 76cdba3..0000000 --- a/dist/index.js +++ /dev/null @@ -1,523 +0,0 @@ -#!/usr/bin/env node -/** - * Second Brain MCP Server - * - * MCP server to interact with the Second Brain RAG knowledge management system. - * Provides tools for adding logs, uploading documents, searching, and chatting. - */ -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; -import * as fs from "fs"; -import * as path from "path"; -// Configuration - can be overridden via environment variables -const API_BASE_URL = process.env.SECOND_BRAIN_API_URL || "https://2brain.coer.nl/api"; -const API_USERNAME = process.env.SECOND_BRAIN_USERNAME || ""; -const API_PASSWORD = process.env.SECOND_BRAIN_PASSWORD || ""; -// Build auth header if credentials provided -function getAuthHeader() { - if (API_USERNAME && API_PASSWORD) { - const credentials = Buffer.from(`${API_USERNAME}:${API_PASSWORD}`).toString("base64"); - return { Authorization: `Basic ${credentials}` }; - } - return {}; -} -// Helper to make API requests -async function apiRequest(endpoint, options = {}) { - const url = `${API_BASE_URL}${endpoint}`; - const headers = { - ...getAuthHeader(), - ...options.headers, - }; - const response = await fetch(url, { - ...options, - headers, - }); - return response; -} -// Define available tools -const TOOLS = [ - { - name: "add_log", - description: "Add a quick log entry or note to the Second Brain. Entries are timestamped and stored in daily log files.", - inputSchema: { - type: "object", - properties: { - content: { - type: "string", - description: "The content of the log entry (markdown supported)", - }, - title: { - type: "string", - description: "Optional title for the log entry", - }, - tags: { - type: "array", - items: { type: "string" }, - description: "Optional tags for categorization (without # prefix)", - }, - }, - required: ["content"], - }, - }, - { - name: "upload_document", - description: "Upload a document file to the Second Brain vault. Supports .md, .txt, .pdf, .json, .yaml, .csv files.", - inputSchema: { - type: "object", - properties: { - file_path: { - type: "string", - description: "Path to the file to upload", - }, - folder: { - type: "string", - description: "Target folder in vault (default: uploads)", - }, - tags: { - type: "string", - description: "Comma-separated tags for the document", - }, - }, - required: ["file_path"], - }, - }, - { - name: "add_document", - description: "Create a new markdown document in the Second Brain vault directly from content.", - inputSchema: { - type: "object", - properties: { - content: { - type: "string", - description: "The markdown content of the document", - }, - filename: { - type: "string", - description: "Filename for the document (with .md extension)", - }, - folder: { - type: "string", - description: "Target folder in vault (default: documents)", - }, - tags: { - type: "string", - description: "Comma-separated tags for the document", - }, - }, - required: ["content", "filename"], - }, - }, - { - name: "search", - description: "Search the Second Brain vault using semantic search. Returns relevant document chunks.", - inputSchema: { - type: "object", - properties: { - query: { - type: "string", - description: "Search query", - }, - top_k: { - type: "number", - description: "Number of results to return (default: 5)", - }, - }, - required: ["query"], - }, - }, - { - name: "chat", - description: "Chat with the Second Brain AI. Uses RAG to answer questions based on your knowledge base.", - inputSchema: { - type: "object", - properties: { - message: { - type: "string", - description: "Your question or message", - }, - }, - required: ["message"], - }, - }, - { - name: "list_logs", - description: "List recent log entries from the Second Brain.", - inputSchema: { - type: "object", - properties: { - limit: { - type: "number", - description: "Number of logs to return (default: 10)", - }, - }, - }, - }, - { - name: "get_log", - description: "Get the contents of a specific daily log file.", - inputSchema: { - type: "object", - properties: { - date: { - type: "string", - description: "Date in YYYY-MM-DD format", - }, - }, - required: ["date"], - }, - }, - { - name: "list_documents", - description: "List documents in the Second Brain vault.", - inputSchema: { - type: "object", - properties: { - folder: { - type: "string", - description: "Filter by folder", - }, - limit: { - type: "number", - description: "Number of documents to return (default: 20)", - }, - }, - }, - }, - { - name: "get_stats", - description: "Get statistics about the Second Brain knowledge base.", - inputSchema: { - type: "object", - properties: {}, - }, - }, - { - name: "reindex", - description: "Trigger a full reindex of the vault. Use after bulk changes.", - inputSchema: { - type: "object", - properties: {}, - }, - }, -]; -// Tool implementations -async function handleAddLog(args) { - const response = await apiRequest("/v1/capture/log", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - content: args.content, - title: args.title, - tags: args.tags, - }), - }); - const data = await response.json(); - if (!response.ok) { - throw new Error(`Failed to add log: ${data.detail || response.statusText}`); - } - return `✅ Log entry added!\n- File: ${data.file_path}\n- Timestamp: ${data.timestamp}`; -} -async function handleUploadDocument(args) { - const filePath = path.resolve(args.file_path); - if (!fs.existsSync(filePath)) { - throw new Error(`File not found: ${filePath}`); - } - const filename = path.basename(filePath); - const fileContent = fs.readFileSync(filePath); - const blob = new Blob([fileContent]); - const formData = new FormData(); - formData.append("file", blob, filename); - if (args.folder) - formData.append("folder", args.folder); - if (args.tags) - formData.append("tags", args.tags); - const response = await apiRequest("/v1/capture/upload", { - method: "POST", - body: formData, - }); - const data = await response.json(); - if (!response.ok) { - throw new Error(`Failed to upload: ${data.detail || response.statusText}`); - } - return `✅ Document uploaded!\n- Path: ${data.file_path}\n- Size: ${data.size_bytes} bytes`; -} -async function handleAddDocument(args) { - // Create a temporary file and upload it - const tempDir = process.env.TMPDIR || "/tmp"; - const tempPath = path.join(tempDir, args.filename); - let content = args.content; - // Add frontmatter if tags provided - if (args.tags) { - const tagList = args.tags.split(",").map((t) => t.trim()); - const frontmatter = `---\ntags: [${tagList.join(", ")}]\ncreated: ${new Date().toISOString()}\n---\n\n`; - content = frontmatter + content; - } - fs.writeFileSync(tempPath, content, "utf-8"); - try { - const result = await handleUploadDocument({ - file_path: tempPath, - folder: args.folder || "documents", - }); - return result; - } - finally { - fs.unlinkSync(tempPath); - } -} -async function handleSearch(args) { - const params = new URLSearchParams({ - q: args.query, - top_k: String(args.top_k || 5), - }); - const response = await apiRequest(`/v1/search?${params}`); - const data = await response.json(); - if (!response.ok) { - throw new Error(`Search failed: ${data.detail || response.statusText}`); - } - if (!data.results || data.results.length === 0) { - return "No results found."; - } - let output = `Found ${data.results.length} results:\n\n`; - for (const result of data.results) { - output += `### ${result.title || result.path}\n`; - output += `Score: ${(result.score * 100).toFixed(1)}%\n`; - output += `${result.content?.substring(0, 200)}...\n\n`; - } - return output; -} -async function handleChat(args) { - const response = await apiRequest("/v1/chat", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ message: args.message }), - }); - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(`Chat failed: ${errorData.detail || response.statusText}`); - } - // Handle SSE stream - const reader = response.body?.getReader(); - if (!reader) { - throw new Error("No response body"); - } - let fullResponse = ""; - let sources = []; - const decoder = new TextDecoder(); - while (true) { - const { done, value } = await reader.read(); - if (done) - break; - const text = decoder.decode(value, { stream: true }); - const lines = text.split("\n"); - for (const line of lines) { - if (line.startsWith("data: ")) { - try { - const data = JSON.parse(line.substring(6)); - if (data.type === "token") { - fullResponse += data.token; - } - else if (data.type === "sources" && data.sources) { - sources = data.sources.map((s) => `- ${s.title || s.path}`); - } - } - catch { - // Ignore parse errors - } - } - } - } - let output = fullResponse; - if (sources.length > 0) { - output += "\n\n**Sources:**\n" + sources.join("\n"); - } - return output; -} -async function handleListLogs(args) { - const params = new URLSearchParams({ - limit: String(args.limit || 10), - }); - const response = await apiRequest(`/v1/capture/logs?${params}`); - const data = await response.json(); - if (!response.ok) { - throw new Error(`Failed to list logs: ${data.detail || response.statusText}`); - } - if (!data.logs || data.logs.length === 0) { - return "No logs found."; - } - let output = `📝 Recent logs (${data.total}):\n\n`; - for (const log of data.logs) { - output += `- **${log.date}** (${log.size_bytes} bytes)\n`; - } - return output; -} -async function handleGetLog(args) { - const response = await apiRequest(`/v1/capture/log/${args.date}`); - const data = await response.json(); - if (!response.ok) { - throw new Error(`Failed to get log: ${data.detail || response.statusText}`); - } - return `# Log for ${args.date}\n\n${data.content}`; -} -async function handleListDocuments(args) { - const params = new URLSearchParams({ - limit: String(args.limit || 20), - }); - if (args.folder) - params.append("folder", args.folder); - const response = await apiRequest(`/v1/documents?${params}`); - const data = await response.json(); - if (!response.ok) { - throw new Error(`Failed to list documents: ${data.detail || response.statusText}`); - } - if (!data.documents || data.documents.length === 0) { - return "No documents found."; - } - let output = `📚 Documents (${data.total}):\n\n`; - for (const doc of data.documents) { - output += `- **${doc.title || doc.path}**`; - if (doc.tags?.length) - output += ` [${doc.tags.join(", ")}]`; - output += "\n"; - } - return output; -} -async function handleGetStats() { - const response = await apiRequest("/v1/stats"); - const data = await response.json(); - if (!response.ok) { - throw new Error(`Failed to get stats: ${data.detail || response.statusText}`); - } - return `📊 Second Brain Stats: -- Documents: ${data.document_count || 0} -- Chunks: ${data.chunk_count || 0} -- Vault size: ${data.vault_size_mb?.toFixed(2) || 0} MB -- Last indexed: ${data.last_indexed || "Never"}`; -} -async function handleReindex() { - const response = await apiRequest("/v1/index/reindex", { - method: "POST", - }); - const data = await response.json(); - if (!response.ok) { - throw new Error(`Reindex failed: ${data.detail || response.statusText}`); - } - return `🔄 Reindex triggered!\n- Status: ${data.status || "started"}`; -} -// Create and configure the MCP server -const server = new Server({ - name: "second-brain-mcp", - version: "1.0.0", -}, { - capabilities: { - tools: {}, - resources: {}, - }, -}); -// Handle tool listing -server.setRequestHandler(ListToolsRequestSchema, async () => { - return { tools: TOOLS }; -}); -// Handle tool calls -server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - try { - let result; - switch (name) { - case "add_log": - result = await handleAddLog(args); - break; - case "upload_document": - result = await handleUploadDocument(args); - break; - case "add_document": - result = await handleAddDocument(args); - break; - case "search": - result = await handleSearch(args); - break; - case "chat": - result = await handleChat(args); - break; - case "list_logs": - result = await handleListLogs(args); - break; - case "get_log": - result = await handleGetLog(args); - break; - case "list_documents": - result = await handleListDocuments(args); - break; - case "get_stats": - result = await handleGetStats(); - break; - case "reindex": - result = await handleReindex(); - break; - default: - throw new Error(`Unknown tool: ${name}`); - } - return { - content: [{ type: "text", text: result }], - }; - } - catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { - content: [{ type: "text", text: `Error: ${message}` }], - isError: true, - }; - } -}); -// Handle resource listing (expose recent logs as resources) -server.setRequestHandler(ListResourcesRequestSchema, async () => { - try { - const response = await apiRequest("/v1/capture/logs?limit=5"); - const data = await response.json(); - if (!response.ok || !data.logs) { - return { resources: [] }; - } - return { - resources: data.logs.map((log) => ({ - uri: `secondbrain://logs/${log.date}`, - name: `Log: ${log.date}`, - mimeType: "text/markdown", - })), - }; - } - catch { - return { resources: [] }; - } -}); -// Handle resource reading -server.setRequestHandler(ReadResourceRequestSchema, async (request) => { - const uri = request.params.uri; - if (uri.startsWith("secondbrain://logs/")) { - const date = uri.replace("secondbrain://logs/", ""); - const response = await apiRequest(`/v1/capture/log/${date}`); - const data = await response.json(); - if (!response.ok) { - throw new Error(`Log not found: ${date}`); - } - return { - contents: [ - { - uri, - mimeType: "text/markdown", - text: data.content, - }, - ], - }; - } - throw new Error(`Unknown resource: ${uri}`); -}); -// Start the server -async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("Second Brain MCP Server running on stdio"); -} -main().catch((error) => { - console.error("Fatal error:", error); - process.exit(1); -});