Release v1.0.0 - Initial Release
🎉 Initial release of Obsidian MCP Server plugin
Core Features:
- MCP server implementation with HTTP transport
- JSON-RPC 2.0 message handling
- Protocol version 2024-11-05 support
MCP Tools:
- read_note, create_note, update_note, delete_note
- search_notes, list_notes, get_vault_info
Server Features:
- Configurable HTTP server (default port: 3000)
- Health check and MCP endpoints
- Auto-start option
Security:
- Origin header validation (DNS rebinding protection)
- Optional Bearer token authentication
- CORS configuration
UI:
- Settings panel with full configuration
- Status bar indicator and ribbon icon
- Start/Stop/Restart commands
Documentation:
- Comprehensive README with examples
- Quick Start Guide and Implementation Summary
- Test client script
This commit is contained in:
275
old-structure/main.ts
Normal file
275
old-structure/main.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import { App, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
|
||||
import { MCPServer, MCPServerSettings } from './mcp-server';
|
||||
|
||||
interface MCPPluginSettings extends MCPServerSettings {
|
||||
autoStart: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: MCPPluginSettings = {
|
||||
port: 3000,
|
||||
enableCORS: true,
|
||||
allowedOrigins: ['*'],
|
||||
apiKey: '',
|
||||
enableAuth: false,
|
||||
autoStart: false
|
||||
}
|
||||
|
||||
export default class MCPServerPlugin extends Plugin {
|
||||
settings: MCPPluginSettings;
|
||||
mcpServer: MCPServer | null = null;
|
||||
statusBarItem: HTMLElement | null = null;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
// Add status bar item
|
||||
this.statusBarItem = this.addStatusBarItem();
|
||||
this.updateStatusBar();
|
||||
|
||||
// Add ribbon icon to toggle server
|
||||
this.addRibbonIcon('server', 'Toggle MCP Server', async () => {
|
||||
if (this.mcpServer?.isRunning()) {
|
||||
await this.stopServer();
|
||||
} else {
|
||||
await this.startServer();
|
||||
}
|
||||
});
|
||||
|
||||
// Add commands
|
||||
this.addCommand({
|
||||
id: 'start-mcp-server',
|
||||
name: 'Start MCP Server',
|
||||
callback: async () => {
|
||||
await this.startServer();
|
||||
}
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'stop-mcp-server',
|
||||
name: 'Stop MCP Server',
|
||||
callback: async () => {
|
||||
await this.stopServer();
|
||||
}
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'restart-mcp-server',
|
||||
name: 'Restart MCP Server',
|
||||
callback: async () => {
|
||||
await this.stopServer();
|
||||
await this.startServer();
|
||||
}
|
||||
});
|
||||
|
||||
// Add settings tab
|
||||
this.addSettingTab(new MCPServerSettingTab(this.app, this));
|
||||
|
||||
// Auto-start if enabled
|
||||
if (this.settings.autoStart) {
|
||||
await this.startServer();
|
||||
}
|
||||
}
|
||||
|
||||
async onunload() {
|
||||
await this.stopServer();
|
||||
}
|
||||
|
||||
async startServer() {
|
||||
if (this.mcpServer?.isRunning()) {
|
||||
new Notice('MCP Server is already running');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.mcpServer = new MCPServer(this.app, this.settings);
|
||||
await this.mcpServer.start();
|
||||
new Notice(`MCP Server started on port ${this.settings.port}`);
|
||||
this.updateStatusBar();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
new Notice(`Failed to start MCP Server: ${message}`);
|
||||
console.error('MCP Server start error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async stopServer() {
|
||||
if (!this.mcpServer?.isRunning()) {
|
||||
new Notice('MCP Server is not running');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.mcpServer.stop();
|
||||
new Notice('MCP Server stopped');
|
||||
this.updateStatusBar();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
new Notice(`Failed to stop MCP Server: ${message}`);
|
||||
console.error('MCP Server stop error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
updateStatusBar() {
|
||||
if (this.statusBarItem) {
|
||||
const isRunning = this.mcpServer?.isRunning() ?? false;
|
||||
this.statusBarItem.setText(
|
||||
isRunning
|
||||
? `MCP: Running (${this.settings.port})`
|
||||
: 'MCP: Stopped'
|
||||
);
|
||||
this.statusBarItem.addClass('mcp-status-bar');
|
||||
}
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
// Update server settings if it's running
|
||||
if (this.mcpServer) {
|
||||
this.mcpServer.updateSettings(this.settings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MCPServerSettingTab extends PluginSettingTab {
|
||||
plugin: MCPServerPlugin;
|
||||
|
||||
constructor(app: App, plugin: MCPServerPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const {containerEl} = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl('h2', {text: 'MCP Server Settings'});
|
||||
|
||||
// Auto-start setting
|
||||
new Setting(containerEl)
|
||||
.setName('Auto-start server')
|
||||
.setDesc('Automatically start the MCP server when Obsidian launches')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoStart)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoStart = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// Port setting
|
||||
new Setting(containerEl)
|
||||
.setName('Port')
|
||||
.setDesc('Port number for the HTTP server (requires restart)')
|
||||
.addText(text => text
|
||||
.setPlaceholder('3000')
|
||||
.setValue(String(this.plugin.settings.port))
|
||||
.onChange(async (value) => {
|
||||
const port = parseInt(value);
|
||||
if (!isNaN(port) && port > 0 && port < 65536) {
|
||||
this.plugin.settings.port = port;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
}));
|
||||
|
||||
// CORS setting
|
||||
new Setting(containerEl)
|
||||
.setName('Enable CORS')
|
||||
.setDesc('Enable Cross-Origin Resource Sharing')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableCORS)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.enableCORS = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// Allowed origins
|
||||
new Setting(containerEl)
|
||||
.setName('Allowed origins')
|
||||
.setDesc('Comma-separated list of allowed origins (* for all)')
|
||||
.addText(text => text
|
||||
.setPlaceholder('*')
|
||||
.setValue(this.plugin.settings.allowedOrigins.join(', '))
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.allowedOrigins = value
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 0);
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// Authentication
|
||||
new Setting(containerEl)
|
||||
.setName('Enable authentication')
|
||||
.setDesc('Require API key for requests')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableAuth)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.enableAuth = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// API Key
|
||||
new Setting(containerEl)
|
||||
.setName('API Key')
|
||||
.setDesc('API key for authentication (Bearer token)')
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter API key')
|
||||
.setValue(this.plugin.settings.apiKey || '')
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.apiKey = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// Server status
|
||||
containerEl.createEl('h3', {text: 'Server Status'});
|
||||
|
||||
const statusEl = containerEl.createEl('div', {cls: 'mcp-server-status'});
|
||||
const isRunning = this.plugin.mcpServer?.isRunning() ?? false;
|
||||
|
||||
statusEl.createEl('p', {
|
||||
text: isRunning
|
||||
? `✅ Server is running on http://127.0.0.1:${this.plugin.settings.port}/mcp`
|
||||
: '⭕ Server is stopped'
|
||||
});
|
||||
|
||||
// Control buttons
|
||||
const buttonContainer = containerEl.createEl('div', {cls: 'mcp-button-container'});
|
||||
|
||||
if (isRunning) {
|
||||
buttonContainer.createEl('button', {text: 'Stop Server'})
|
||||
.addEventListener('click', async () => {
|
||||
await this.plugin.stopServer();
|
||||
this.display(); // Refresh display
|
||||
});
|
||||
|
||||
buttonContainer.createEl('button', {text: 'Restart Server'})
|
||||
.addEventListener('click', async () => {
|
||||
await this.plugin.stopServer();
|
||||
await this.plugin.startServer();
|
||||
this.display(); // Refresh display
|
||||
});
|
||||
} else {
|
||||
buttonContainer.createEl('button', {text: 'Start Server'})
|
||||
.addEventListener('click', async () => {
|
||||
await this.plugin.startServer();
|
||||
this.display(); // Refresh display
|
||||
});
|
||||
}
|
||||
|
||||
// Connection info
|
||||
if (isRunning) {
|
||||
containerEl.createEl('h3', {text: 'Connection Information'});
|
||||
|
||||
const infoEl = containerEl.createEl('div', {cls: 'mcp-connection-info'});
|
||||
infoEl.createEl('p', {text: 'MCP Endpoint:'});
|
||||
infoEl.createEl('code', {text: `http://127.0.0.1:${this.plugin.settings.port}/mcp`});
|
||||
|
||||
infoEl.createEl('p', {text: 'Health Check:'});
|
||||
infoEl.createEl('code', {text: `http://127.0.0.1:${this.plugin.settings.port}/health`});
|
||||
}
|
||||
}
|
||||
}
|
||||
485
old-structure/mcp-server.ts
Normal file
485
old-structure/mcp-server.ts
Normal file
@@ -0,0 +1,485 @@
|
||||
import { App, TFile, TFolder } from 'obsidian';
|
||||
import express, { Express, Request, Response } from 'express';
|
||||
import cors from 'cors';
|
||||
import { Server } from 'http';
|
||||
import {
|
||||
JSONRPCRequest,
|
||||
JSONRPCResponse,
|
||||
JSONRPCError,
|
||||
InitializeResult,
|
||||
ListToolsResult,
|
||||
CallToolResult,
|
||||
Tool,
|
||||
ErrorCodes,
|
||||
ContentBlock
|
||||
} from './mcp-types';
|
||||
|
||||
export interface MCPServerSettings {
|
||||
port: number;
|
||||
enableCORS: boolean;
|
||||
allowedOrigins: string[];
|
||||
apiKey?: string;
|
||||
enableAuth: boolean;
|
||||
}
|
||||
|
||||
export class MCPServer {
|
||||
private app: Express;
|
||||
private server: Server | null = null;
|
||||
private obsidianApp: App;
|
||||
private settings: MCPServerSettings;
|
||||
|
||||
constructor(obsidianApp: App, settings: MCPServerSettings) {
|
||||
this.obsidianApp = obsidianApp;
|
||||
this.settings = settings;
|
||||
this.app = express();
|
||||
this.setupMiddleware();
|
||||
this.setupRoutes();
|
||||
}
|
||||
|
||||
private setupMiddleware(): void {
|
||||
// Parse JSON bodies
|
||||
this.app.use(express.json());
|
||||
|
||||
// CORS configuration
|
||||
if (this.settings.enableCORS) {
|
||||
const corsOptions = {
|
||||
origin: (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => {
|
||||
// Allow requests with no origin (like mobile apps or curl requests)
|
||||
if (!origin) return callback(null, true);
|
||||
|
||||
if (this.settings.allowedOrigins.includes('*') ||
|
||||
this.settings.allowedOrigins.includes(origin)) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
callback(new Error('Not allowed by CORS'));
|
||||
}
|
||||
},
|
||||
credentials: true
|
||||
};
|
||||
this.app.use(cors(corsOptions));
|
||||
}
|
||||
|
||||
// Authentication middleware
|
||||
if (this.settings.enableAuth && this.settings.apiKey) {
|
||||
this.app.use((req: Request, res: Response, next: any) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
const apiKey = authHeader?.replace('Bearer ', '');
|
||||
|
||||
if (apiKey !== this.settings.apiKey) {
|
||||
return res.status(401).json(this.createErrorResponse(null, ErrorCodes.InvalidRequest, 'Unauthorized'));
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
// Origin validation for security (DNS rebinding protection)
|
||||
this.app.use((req: Request, res: Response, next: any) => {
|
||||
const origin = req.headers.origin;
|
||||
const host = req.headers.host;
|
||||
|
||||
// Only allow localhost connections
|
||||
if (host && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
|
||||
return res.status(403).json(this.createErrorResponse(null, ErrorCodes.InvalidRequest, 'Only localhost connections allowed'));
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
private setupRoutes(): void {
|
||||
// Main MCP endpoint
|
||||
this.app.post('/mcp', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const request = req.body as JSONRPCRequest;
|
||||
const response = await this.handleRequest(request);
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
console.error('MCP request error:', error);
|
||||
res.status(500).json(this.createErrorResponse(null, ErrorCodes.InternalError, 'Internal server error'));
|
||||
}
|
||||
});
|
||||
|
||||
// Health check endpoint
|
||||
this.app.get('/health', (_req: Request, res: Response) => {
|
||||
res.json({ status: 'ok', timestamp: Date.now() });
|
||||
});
|
||||
}
|
||||
|
||||
private async handleRequest(request: JSONRPCRequest): Promise<JSONRPCResponse> {
|
||||
try {
|
||||
switch (request.method) {
|
||||
case 'initialize':
|
||||
return this.createSuccessResponse(request.id, await this.handleInitialize(request.params));
|
||||
case 'tools/list':
|
||||
return this.createSuccessResponse(request.id, await this.handleListTools());
|
||||
case 'tools/call':
|
||||
return this.createSuccessResponse(request.id, await this.handleCallTool(request.params));
|
||||
case 'ping':
|
||||
return this.createSuccessResponse(request.id, {});
|
||||
default:
|
||||
return this.createErrorResponse(request.id, ErrorCodes.MethodNotFound, `Method not found: ${request.method}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling request:', error);
|
||||
return this.createErrorResponse(request.id, ErrorCodes.InternalError, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleInitialize(params: any): Promise<InitializeResult> {
|
||||
return {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: {
|
||||
tools: {}
|
||||
},
|
||||
serverInfo: {
|
||||
name: "obsidian-mcp-server",
|
||||
version: "1.0.0"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async handleListTools(): Promise<ListToolsResult> {
|
||||
const tools: Tool[] = [
|
||||
{
|
||||
name: "read_note",
|
||||
description: "Read the content of a note from the Obsidian vault",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
description: "Path to the note within the vault (e.g., 'folder/note.md')"
|
||||
}
|
||||
},
|
||||
required: ["path"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "create_note",
|
||||
description: "Create a new note in the Obsidian vault",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
description: "Path for the new note (e.g., 'folder/note.md')"
|
||||
},
|
||||
content: {
|
||||
type: "string",
|
||||
description: "Content of the note"
|
||||
}
|
||||
},
|
||||
required: ["path", "content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "update_note",
|
||||
description: "Update an existing note in the Obsidian vault",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
description: "Path to the note to update"
|
||||
},
|
||||
content: {
|
||||
type: "string",
|
||||
description: "New content for the note"
|
||||
}
|
||||
},
|
||||
required: ["path", "content"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "delete_note",
|
||||
description: "Delete a note from the Obsidian vault",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
description: "Path to the note to delete"
|
||||
}
|
||||
},
|
||||
required: ["path"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "search_notes",
|
||||
description: "Search for notes in the Obsidian vault",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "Search query string"
|
||||
}
|
||||
},
|
||||
required: ["query"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "get_vault_info",
|
||||
description: "Get information about the Obsidian vault",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "list_notes",
|
||||
description: "List all notes in the vault or in a specific folder",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
folder: {
|
||||
type: "string",
|
||||
description: "Optional folder path to list notes from"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return { tools };
|
||||
}
|
||||
|
||||
private async handleCallTool(params: any): Promise<CallToolResult> {
|
||||
const { name, arguments: args } = params;
|
||||
|
||||
try {
|
||||
switch (name) {
|
||||
case "read_note":
|
||||
return await this.readNote(args.path);
|
||||
case "create_note":
|
||||
return await this.createNote(args.path, args.content);
|
||||
case "update_note":
|
||||
return await this.updateNote(args.path, args.content);
|
||||
case "delete_note":
|
||||
return await this.deleteNote(args.path);
|
||||
case "search_notes":
|
||||
return await this.searchNotes(args.query);
|
||||
case "get_vault_info":
|
||||
return await this.getVaultInfo();
|
||||
case "list_notes":
|
||||
return await this.listNotes(args.folder);
|
||||
default:
|
||||
return {
|
||||
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Error: ${error.message}` }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Tool implementations
|
||||
|
||||
private async readNote(path: string): Promise<CallToolResult> {
|
||||
const file = this.obsidianApp.vault.getAbstractFileByPath(path);
|
||||
|
||||
if (!file || !(file instanceof TFile)) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Note not found: ${path}` }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
const content = await this.obsidianApp.vault.read(file);
|
||||
return {
|
||||
content: [{ type: "text", text: content }]
|
||||
};
|
||||
}
|
||||
|
||||
private async createNote(path: string, content: string): Promise<CallToolResult> {
|
||||
try {
|
||||
const file = await this.obsidianApp.vault.create(path, content);
|
||||
return {
|
||||
content: [{ type: "text", text: `Note created successfully: ${file.path}` }]
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Failed to create note: ${error.message}` }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async updateNote(path: string, content: string): Promise<CallToolResult> {
|
||||
const file = this.obsidianApp.vault.getAbstractFileByPath(path);
|
||||
|
||||
if (!file || !(file instanceof TFile)) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Note not found: ${path}` }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
await this.obsidianApp.vault.modify(file, content);
|
||||
return {
|
||||
content: [{ type: "text", text: `Note updated successfully: ${path}` }]
|
||||
};
|
||||
}
|
||||
|
||||
private async deleteNote(path: string): Promise<CallToolResult> {
|
||||
const file = this.obsidianApp.vault.getAbstractFileByPath(path);
|
||||
|
||||
if (!file || !(file instanceof TFile)) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Note not found: ${path}` }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
await this.obsidianApp.vault.delete(file);
|
||||
return {
|
||||
content: [{ type: "text", text: `Note deleted successfully: ${path}` }]
|
||||
};
|
||||
}
|
||||
|
||||
private async searchNotes(query: string): Promise<CallToolResult> {
|
||||
const files = this.obsidianApp.vault.getMarkdownFiles();
|
||||
const results: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const content = await this.obsidianApp.vault.read(file);
|
||||
if (content.toLowerCase().includes(query.toLowerCase()) ||
|
||||
file.basename.toLowerCase().includes(query.toLowerCase())) {
|
||||
results.push(file.path);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: results.length > 0
|
||||
? `Found ${results.length} notes:\n${results.join('\n')}`
|
||||
: 'No notes found matching the query'
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
private async getVaultInfo(): Promise<CallToolResult> {
|
||||
const files = this.obsidianApp.vault.getFiles();
|
||||
const markdownFiles = this.obsidianApp.vault.getMarkdownFiles();
|
||||
|
||||
const info = {
|
||||
name: this.obsidianApp.vault.getName(),
|
||||
totalFiles: files.length,
|
||||
markdownFiles: markdownFiles.length,
|
||||
rootPath: (this.obsidianApp.vault.adapter as any).basePath || 'Unknown'
|
||||
};
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(info, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
private async listNotes(folder?: string): Promise<CallToolResult> {
|
||||
let files: TFile[];
|
||||
|
||||
if (folder) {
|
||||
const folderObj = this.obsidianApp.vault.getAbstractFileByPath(folder);
|
||||
if (!folderObj || !(folderObj instanceof TFolder)) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Folder not found: ${folder}` }],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
files = [];
|
||||
this.obsidianApp.vault.getMarkdownFiles().forEach((file: TFile) => {
|
||||
if (file.path.startsWith(folder + '/')) {
|
||||
files.push(file);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
files = this.obsidianApp.vault.getMarkdownFiles();
|
||||
}
|
||||
|
||||
const noteList = files.map(f => f.path).join('\n');
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Found ${files.length} notes:\n${noteList}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
private createSuccessResponse(id: string | number | undefined, result: any): JSONRPCResponse {
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id: id ?? null,
|
||||
result
|
||||
};
|
||||
}
|
||||
|
||||
private createErrorResponse(id: string | number | undefined | null, code: number, message: string, data?: any): JSONRPCResponse {
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id: id ?? null,
|
||||
error: {
|
||||
code,
|
||||
message,
|
||||
data
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Server lifecycle
|
||||
|
||||
public async start(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
this.server = this.app.listen(this.settings.port, '127.0.0.1', () => {
|
||||
console.log(`MCP Server listening on http://127.0.0.1:${this.settings.port}/mcp`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
this.server.on('error', (error: any) => {
|
||||
if (error.code === 'EADDRINUSE') {
|
||||
reject(new Error(`Port ${this.settings.port} is already in use`));
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.server) {
|
||||
this.server.close((err?: Error) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
console.log('MCP Server stopped');
|
||||
this.server = null;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public isRunning(): boolean {
|
||||
return this.server !== null;
|
||||
}
|
||||
|
||||
public updateSettings(settings: MCPServerSettings): void {
|
||||
this.settings = settings;
|
||||
}
|
||||
}
|
||||
122
old-structure/mcp-types.ts
Normal file
122
old-structure/mcp-types.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
// MCP Protocol Types based on JSON-RPC 2.0
|
||||
|
||||
export interface JSONRPCRequest {
|
||||
jsonrpc: "2.0";
|
||||
id?: string | number;
|
||||
method: string;
|
||||
params?: any;
|
||||
}
|
||||
|
||||
export interface JSONRPCResponse {
|
||||
jsonrpc: "2.0";
|
||||
id: string | number | null;
|
||||
result?: any;
|
||||
error?: JSONRPCError;
|
||||
}
|
||||
|
||||
export interface JSONRPCError {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export interface JSONRPCNotification {
|
||||
jsonrpc: "2.0";
|
||||
method: string;
|
||||
params?: any;
|
||||
}
|
||||
|
||||
// MCP Protocol Messages
|
||||
|
||||
export interface InitializeRequest {
|
||||
method: "initialize";
|
||||
params: {
|
||||
protocolVersion: string;
|
||||
capabilities: ClientCapabilities;
|
||||
clientInfo: {
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface InitializeResult {
|
||||
protocolVersion: string;
|
||||
capabilities: ServerCapabilities;
|
||||
serverInfo: {
|
||||
name: string;
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ClientCapabilities {
|
||||
roots?: {
|
||||
listChanged?: boolean;
|
||||
};
|
||||
sampling?: {};
|
||||
experimental?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ServerCapabilities {
|
||||
tools?: {};
|
||||
resources?: {
|
||||
subscribe?: boolean;
|
||||
listChanged?: boolean;
|
||||
};
|
||||
prompts?: {
|
||||
listChanged?: boolean;
|
||||
};
|
||||
logging?: {};
|
||||
experimental?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ListToolsRequest {
|
||||
method: "tools/list";
|
||||
params?: {
|
||||
cursor?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
name: string;
|
||||
description?: string;
|
||||
inputSchema: {
|
||||
type: "object";
|
||||
properties?: Record<string, any>;
|
||||
required?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ListToolsResult {
|
||||
tools: Tool[];
|
||||
nextCursor?: string;
|
||||
}
|
||||
|
||||
export interface CallToolRequest {
|
||||
method: "tools/call";
|
||||
params: {
|
||||
name: string;
|
||||
arguments?: Record<string, any>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CallToolResult {
|
||||
content: ContentBlock[];
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
export interface ContentBlock {
|
||||
type: "text" | "image" | "resource";
|
||||
text?: string;
|
||||
data?: string;
|
||||
mimeType?: string;
|
||||
}
|
||||
|
||||
// Error codes
|
||||
export const ErrorCodes = {
|
||||
ParseError: -32700,
|
||||
InvalidRequest: -32600,
|
||||
MethodNotFound: -32601,
|
||||
InvalidParams: -32602,
|
||||
InternalError: -32603,
|
||||
};
|
||||
Reference in New Issue
Block a user