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:
144
src/server/mcp-server.ts
Normal file
144
src/server/mcp-server.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { App } from 'obsidian';
|
||||
import express, { Express } from 'express';
|
||||
import { Server } from 'http';
|
||||
import {
|
||||
JSONRPCRequest,
|
||||
JSONRPCResponse,
|
||||
ErrorCodes,
|
||||
InitializeResult,
|
||||
ListToolsResult,
|
||||
CallToolResult
|
||||
} from '../types/mcp-types';
|
||||
import { MCPServerSettings } from '../types/settings-types';
|
||||
import { ToolRegistry } from '../tools';
|
||||
import { setupMiddleware } from './middleware';
|
||||
import { setupRoutes } from './routes';
|
||||
|
||||
export class MCPServer {
|
||||
private app: Express;
|
||||
private server: Server | null = null;
|
||||
private obsidianApp: App;
|
||||
private settings: MCPServerSettings;
|
||||
private toolRegistry: ToolRegistry;
|
||||
|
||||
constructor(obsidianApp: App, settings: MCPServerSettings) {
|
||||
this.obsidianApp = obsidianApp;
|
||||
this.settings = settings;
|
||||
this.app = express();
|
||||
this.toolRegistry = new ToolRegistry(obsidianApp);
|
||||
|
||||
setupMiddleware(this.app, this.settings, this.createErrorResponse.bind(this));
|
||||
setupRoutes(this.app, this.handleRequest.bind(this), this.createErrorResponse.bind(this));
|
||||
}
|
||||
|
||||
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 as 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> {
|
||||
return {
|
||||
tools: this.toolRegistry.getToolDefinitions()
|
||||
};
|
||||
}
|
||||
|
||||
private async handleCallTool(params: any): Promise<CallToolResult> {
|
||||
const { name, arguments: args } = params;
|
||||
return await this.toolRegistry.callTool(name, args);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
54
src/server/middleware.ts
Normal file
54
src/server/middleware.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Express, Request, Response } from 'express';
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { MCPServerSettings } from '../types/settings-types';
|
||||
import { ErrorCodes } from '../types/mcp-types';
|
||||
|
||||
export function setupMiddleware(app: Express, settings: MCPServerSettings, createErrorResponse: (id: any, code: number, message: string) => any): void {
|
||||
// Parse JSON bodies
|
||||
app.use(express.json());
|
||||
|
||||
// CORS configuration
|
||||
if (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 (settings.allowedOrigins.includes('*') ||
|
||||
settings.allowedOrigins.includes(origin)) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
callback(new Error('Not allowed by CORS'));
|
||||
}
|
||||
},
|
||||
credentials: true
|
||||
};
|
||||
app.use(cors(corsOptions));
|
||||
}
|
||||
|
||||
// Authentication middleware
|
||||
if (settings.enableAuth && settings.apiKey) {
|
||||
app.use((req: Request, res: Response, next: any) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
const apiKey = authHeader?.replace('Bearer ', '');
|
||||
|
||||
if (apiKey !== settings.apiKey) {
|
||||
return res.status(401).json(createErrorResponse(null, ErrorCodes.InvalidRequest, 'Unauthorized'));
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
// Origin validation for security (DNS rebinding protection)
|
||||
app.use((req: Request, res: Response, next: any) => {
|
||||
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(createErrorResponse(null, ErrorCodes.InvalidRequest, 'Only localhost connections allowed'));
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
25
src/server/routes.ts
Normal file
25
src/server/routes.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Express, Request, Response } from 'express';
|
||||
import { JSONRPCRequest, JSONRPCResponse, ErrorCodes } from '../types/mcp-types';
|
||||
|
||||
export function setupRoutes(
|
||||
app: Express,
|
||||
handleRequest: (request: JSONRPCRequest) => Promise<JSONRPCResponse>,
|
||||
createErrorResponse: (id: any, code: number, message: string) => JSONRPCResponse
|
||||
): void {
|
||||
// Main MCP endpoint
|
||||
app.post('/mcp', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const request = req.body as JSONRPCRequest;
|
||||
const response = await handleRequest(request);
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
console.error('MCP request error:', error);
|
||||
res.status(500).json(createErrorResponse(null, ErrorCodes.InternalError, 'Internal server error'));
|
||||
}
|
||||
});
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (_req: Request, res: Response) => {
|
||||
res.json({ status: 'ok', timestamp: Date.now() });
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user