Initial MCP server for Second Brain

main
Clawd 3 weeks ago
commit 9b8559b2ce

@ -0,0 +1,103 @@
# Second Brain MCP Server
MCP server to interact with your Second Brain knowledge management system from any MCP-compatible client (Claude Desktop, VS Code, etc.).
## Features
- **add_log** - Add quick notes/log entries with timestamps
- **add_document** - Create new markdown documents directly
- **upload_document** - Upload files (.md, .txt, .pdf, .json, .yaml, .csv)
- **search** - Semantic search across your knowledge base
- **chat** - Chat with AI using RAG on your documents
- **list_logs** / **get_log** - Browse daily logs
- **list_documents** - Browse vault contents
- **get_stats** - Knowledge base statistics
- **reindex** - Trigger vault reindexing
## Installation
```bash
# Clone or copy this folder to your local machine
cd second-brain-mcp
# Install dependencies
npm install
# Build
npm run build
```
## Configuration
Set environment variables:
```bash
# Required: Your Second Brain API URL
export SECOND_BRAIN_API_URL="https://2brain.coer.nl/api"
# Optional: Basic auth credentials (if using access list)
export SECOND_BRAIN_USERNAME="your-username"
export SECOND_BRAIN_PASSWORD="your-password"
```
## Usage with Claude Desktop
Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on Mac or `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
```json
{
"mcpServers": {
"second-brain": {
"command": "node",
"args": ["/path/to/second-brain-mcp/dist/index.js"],
"env": {
"SECOND_BRAIN_API_URL": "https://2brain.coer.nl/api",
"SECOND_BRAIN_USERNAME": "your-username",
"SECOND_BRAIN_PASSWORD": "your-password"
}
}
}
}
```
## Usage with VS Code (Copilot)
Add to your VS Code settings:
```json
{
"github.copilot.chat.mcpServers": {
"second-brain": {
"command": "node",
"args": ["/path/to/second-brain-mcp/dist/index.js"],
"env": {
"SECOND_BRAIN_API_URL": "https://2brain.coer.nl/api"
}
}
}
}
```
## Example Usage
Once configured, you can use natural language:
- "Add a note about the meeting with John today"
- "Search my knowledge base for homelab documentation"
- "What do I know about Proxmox configuration?"
- "Upload this requirements.txt file to my second brain"
- "Show me my recent logs"
## Development
```bash
# Run in development mode
npm run dev
# Build for production
npm run build
```
## License
MIT

8
dist/index.d.ts vendored

@ -0,0 +1,8 @@
#!/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 {};

523
dist/index.js vendored

@ -0,0 +1,523 @@
#!/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);
});

1717
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,23 @@
{
"name": "second-brain-mcp",
"version": "1.0.0",
"description": "MCP server to interact with Second Brain knowledge management system",
"main": "dist/index.js",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
},
"keywords": ["mcp", "second-brain", "rag", "knowledge-management"],
"author": "Mark",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsx": "^4.7.0",
"typescript": "^5.3.0"
}
}

@ -0,0 +1,654 @@
#!/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,
Tool,
} 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(): Record<string, string> {
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: string,
options: RequestInit = {}
): Promise<Response> {
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: Tool[] = [
{
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: {
content: string;
title?: string;
tags?: string[];
}): Promise<string> {
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: {
file_path: string;
folder?: string;
tags?: string;
}): Promise<string> {
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: {
content: string;
filename: string;
folder?: string;
tags?: string;
}): Promise<string> {
// 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: {
query: string;
top_k?: number;
}): Promise<string> {
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: { message: string }): Promise<string> {
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: string[] = [];
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: { title: string; path: string }) =>
`- ${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: { limit?: number }): Promise<string> {
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: { date: string }): Promise<string> {
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: {
folder?: string;
limit?: number;
}): Promise<string> {
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(): Promise<string> {
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(): Promise<string> {
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: string;
switch (name) {
case "add_log":
result = await handleAddLog(args as Parameters<typeof handleAddLog>[0]);
break;
case "upload_document":
result = await handleUploadDocument(
args as Parameters<typeof handleUploadDocument>[0]
);
break;
case "add_document":
result = await handleAddDocument(
args as Parameters<typeof handleAddDocument>[0]
);
break;
case "search":
result = await handleSearch(args as Parameters<typeof handleSearch>[0]);
break;
case "chat":
result = await handleChat(args as Parameters<typeof handleChat>[0]);
break;
case "list_logs":
result = await handleListLogs(
args as Parameters<typeof handleListLogs>[0]
);
break;
case "get_log":
result = await handleGetLog(args as Parameters<typeof handleGetLog>[0]);
break;
case "list_documents":
result = await handleListDocuments(
args as Parameters<typeof handleListDocuments>[0]
);
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: { date: string; file_path: string }) => ({
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);
});

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Loading…
Cancel
Save

Powered by TurnKey Linux.