node-server #10
27
CHANGELOG.md
27
CHANGELOG.md
@@ -73,6 +73,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Comprehensive input schema definitions
|
- Comprehensive input schema definitions
|
||||||
- Security audit of parameter handling
|
- Security audit of parameter handling
|
||||||
|
|
||||||
|
## [1.2.0] - 2026-04-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Binary file support in `obsidian_read_note`**: Binary files are now read directly from the filesystem, bypassing the Obsidian CLI which does not support binary output
|
||||||
|
- Uses `obsidian vault info=path` to resolve the vault root filesystem path
|
||||||
|
- Uses `obsidian file file=<name>` to resolve wikilink-style names to vault-relative paths
|
||||||
|
- Images (PNG, JPG, JPEG, GIF, WEBP, SVG) returned as MCP image content `{ type: "image", data, mimeType }`
|
||||||
|
- Other binary files (PDF, ZIP, DOCX, XLSX, PPTX, etc.) returned as MCP embedded resource `{ type: "resource", resource: { uri, mimeType, blob } }`
|
||||||
|
- Unknown extensions fall through to the CLI with a runtime binary detection fallback
|
||||||
|
|
||||||
|
## [1.1.9] - 2026-04-30
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Images returned as MCP image content type**: Updated binary file handling so image files (PNG, JPG, JPEG, GIF, WEBP, SVG) are returned as `{ type: "image", data: "<base64>", mimeType: "..." }` per the MCP 2025-11-25 spec for image content in tool results
|
||||||
|
- Non-image binary files (PDF, ZIP, DOCX, etc.) continue to use the embedded resource format `{ type: "resource", resource: { uri, mimeType, blob } }`
|
||||||
|
|
||||||
|
## [1.1.8] - 2026-04-30
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Binary files returned as MCP embedded resource**: Updated issue #9 fix to use the proper MCP `EmbeddedResource` format instead of a `BASE64:` text prefix
|
||||||
|
- Binary files are now returned as `{ type: "resource", resource: { uri, mimeType, blob } }`
|
||||||
|
- `uri` is constructed as `obsidian://<vault>/<path>`
|
||||||
|
- `mimeType` is detected from the file extension (PDF, ZIP, images, Office formats, audio/video; defaults to `application/octet-stream`)
|
||||||
|
- `blob` contains the base64-encoded raw bytes
|
||||||
|
|
||||||
## [1.1.7] - 2026-04-30
|
## [1.1.7] - 2026-04-30
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@@ -160,6 +185,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
|
|
||||||
|
- **1.1.8** - Bug fix release: Binary files returned as MCP embedded resource (fixes #9)
|
||||||
- **1.1.7** - Bug fix release: Binary files returned as base64 in `obsidian_read_note` (fixes #9)
|
- **1.1.7** - Bug fix release: Binary files returned as base64 in `obsidian_read_note` (fixes #9)
|
||||||
- **1.1.6** - Bug fix release: Clarify `name` vs `path` semantics in `obsidian_create_note` (fixes #8)
|
- **1.1.6** - Bug fix release: Clarify `name` vs `path` semantics in `obsidian_create_note` (fixes #8)
|
||||||
- **1.1.5** - Bug fix release: Preserve `<` and `>` in note content for Mermaid/HTML (fixes #7)
|
- **1.1.5** - Bug fix release: Preserve `<` and `>` in note content for Mermaid/HTML (fixes #7)
|
||||||
@@ -173,6 +199,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Search & Discovery (12 tools)
|
- Search & Discovery (12 tools)
|
||||||
- Task & Property Management (8 tools)
|
- Task & Property Management (8 tools)
|
||||||
|
|
||||||
|
[1.1.8]: https://git.mortons.site/Peter.Morton/obsidian-mcp/releases/tag/v1.1.8
|
||||||
[1.1.7]: https://git.mortons.site/Peter.Morton/obsidian-mcp/releases/tag/v1.1.7
|
[1.1.7]: https://git.mortons.site/Peter.Morton/obsidian-mcp/releases/tag/v1.1.7
|
||||||
[1.1.6]: https://git.mortons.site/Peter.Morton/obsidian-mcp/releases/tag/v1.1.6
|
[1.1.6]: https://git.mortons.site/Peter.Morton/obsidian-mcp/releases/tag/v1.1.6
|
||||||
[1.1.5]: https://git.mortons.site/Peter.Morton/obsidian-mcp/releases/tag/v1.1.5
|
[1.1.5]: https://git.mortons.site/Peter.Morton/obsidian-mcp/releases/tag/v1.1.5
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": "0.3",
|
"manifest_version": "0.3",
|
||||||
"name": "obsidian-mcp",
|
"name": "obsidian-mcp",
|
||||||
"version": "1.1.7",
|
"version": "1.2.0",
|
||||||
"display_name": "Obsidian CLI Bundle",
|
"display_name": "Obsidian CLI Bundle",
|
||||||
"description": "MCP Bundle for Obsidian CLI - Enable AI assistants to manage Obsidian vaults through conversational interface",
|
"description": "MCP Bundle for Obsidian CLI - Enable AI assistants to manage Obsidian vaults through conversational interface",
|
||||||
"long_description": "This MCP bundle provides a comprehensive set of tools for AI assistants to interact with and manage Obsidian vaults. It includes capabilities for creating, reading, updating, and deleting notes, managing links and tags, handling tasks, and more. With this bundle, AI assistants can seamlessly integrate with Obsidian to help users organize their knowledge and workflows.",
|
"long_description": "This MCP bundle provides a comprehensive set of tools for AI assistants to interact with and manage Obsidian vaults. It includes capabilities for creating, reading, updating, and deleting notes, managing links and tags, handling tasks, and more. With this bundle, AI assistants can seamlessly integrate with Obsidian to help users organize their knowledge and workflows.",
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-mcp",
|
"name": "obsidian-mcp",
|
||||||
"version": "1.1.7",
|
"version": "1.2.0",
|
||||||
"description": "MCP Bundle for Obsidian CLI - Enable AI assistants to manage Obsidian vaults through Model Context Protocol",
|
"description": "MCP Bundle for Obsidian CLI - Enable AI assistants to manage Obsidian vaults through Model Context Protocol",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
|
"server": "node dist/http-server.js",
|
||||||
"validate-manifest": "mcpb validate manifest.json",
|
"validate-manifest": "mcpb validate manifest.json",
|
||||||
"pack": "npm run build && mcpb pack",
|
"pack": "npm run build && mcpb pack",
|
||||||
|
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"dev": "tsc --watch",
|
"dev": "tsc --watch",
|
||||||
"validate-tools": "node scripts/validate-tools.js"
|
"validate-tools": "node scripts/validate-tools.js"
|
||||||
|
|||||||
114
src/http-server.ts
Normal file
114
src/http-server.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* HTTP Entry Point for Obsidian MCP Server
|
||||||
|
* Exposes the MCP implementation over Streamable HTTP transport
|
||||||
|
* per the MCP 2025-11-25 specification.
|
||||||
|
*
|
||||||
|
* Endpoint: POST/GET /mcp
|
||||||
|
* Port: MCP_PORT env var (default: 3000)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import http, { IncomingMessage } from 'node:http';
|
||||||
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||||
|
import { ObsidianMCPServer } from './server.js';
|
||||||
|
import { registerAllTools } from './tools/index.js';
|
||||||
|
import { logger } from './utils/logger.js';
|
||||||
|
|
||||||
|
const PORT = parseInt(process.env.MCP_PORT ?? '3000', 10);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read and parse the JSON body from an incoming HTTP request.
|
||||||
|
*/
|
||||||
|
function readBody(req: IncomingMessage): Promise<unknown> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
req.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||||
|
req.on('end', () => {
|
||||||
|
const raw = Buffer.concat(chunks).toString('utf8');
|
||||||
|
if (!raw) {
|
||||||
|
resolve(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(raw));
|
||||||
|
} catch {
|
||||||
|
reject(new Error('Invalid JSON body'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const vaultName = process.env.OBSIDIAN_VAULT;
|
||||||
|
if (!vaultName) {
|
||||||
|
logger.error('OBSIDIAN_VAULT environment variable not set');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Starting Obsidian MCP HTTP server', { vault: vaultName, port: PORT });
|
||||||
|
|
||||||
|
// Create and configure the MCP server
|
||||||
|
const mcpServer = new ObsidianMCPServer();
|
||||||
|
await registerAllTools(mcpServer);
|
||||||
|
|
||||||
|
// Stateless transport: no session management, each request is self-contained.
|
||||||
|
// Use sessionIdGenerator: () => randomUUID() for stateful/multi-client mode.
|
||||||
|
const transport = new StreamableHTTPServerTransport({
|
||||||
|
sessionIdGenerator: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
await mcpServer.connect(transport);
|
||||||
|
|
||||||
|
const httpServer = http.createServer(async (req, res) => {
|
||||||
|
// Only serve the /mcp endpoint
|
||||||
|
if (req.url !== '/mcp') {
|
||||||
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'Not Found. Use POST /mcp' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let body: unknown;
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
body = await readBody(req);
|
||||||
|
}
|
||||||
|
await transport.handleRequest(req, res, body);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error handling MCP request', {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'Internal server error' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
httpServer.listen(PORT, () => {
|
||||||
|
logger.info(`MCP HTTP server ready`, { url: `http://localhost:${PORT}/mcp` });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
const shutdown = async (signal: string) => {
|
||||||
|
logger.info(`Received ${signal}, shutting down`);
|
||||||
|
httpServer.close(async () => {
|
||||||
|
try {
|
||||||
|
await mcpServer.close();
|
||||||
|
logger.info('Server closed');
|
||||||
|
process.exit(0);
|
||||||
|
} catch {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
||||||
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
logger.error('Fatal error', {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||||
import {
|
import {
|
||||||
CallToolRequestSchema,
|
CallToolRequestSchema,
|
||||||
ListToolsRequestSchema,
|
ListToolsRequestSchema,
|
||||||
@@ -119,12 +120,12 @@ export class ObsidianMCPServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connect to stdio transport
|
* Connect using the provided transport, or stdio if none is given
|
||||||
*/
|
*/
|
||||||
async connect(): Promise<void> {
|
async connect(transport?: Transport): Promise<void> {
|
||||||
const transport = new StdioServerTransport();
|
const t = transport ?? new StdioServerTransport();
|
||||||
await this.server.connect(transport);
|
await this.server.connect(t);
|
||||||
logger.info('MCP server connected to stdio transport');
|
logger.info('MCP server connected', { transport: transport ? 'custom' : 'stdio' });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
* User Story 1 (P1 - MVP): Enable basic note-taking workflows
|
* User Story 1 (P1 - MVP): Enable basic note-taking workflows
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { join } from 'node:path';
|
||||||
import { ObsidianMCPServer, createToolHandler } from '../server.js';
|
import { ObsidianMCPServer, createToolHandler } from '../server.js';
|
||||||
import { executeObsidianCommand, executeObsidianCommandBinary } from '../cli/executor.js';
|
import { executeObsidianCommand } from '../cli/executor.js';
|
||||||
import { formatForMCP } from '../cli/parser.js';
|
import { formatForMCP } from '../cli/parser.js';
|
||||||
import { handleCLIResult } from '../utils/error-handler.js';
|
import { handleCLIResult } from '../utils/error-handler.js';
|
||||||
import { logger } from '../utils/logger.js';
|
import { logger } from '../utils/logger.js';
|
||||||
@@ -40,6 +42,80 @@ function isBinaryContent(buf: Buffer): boolean {
|
|||||||
return nonPrintable / sample.length > 0.1;
|
return nonPrintable / sample.length > 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Map common file extensions to MIME types */
|
||||||
|
const MIME_TYPES: Record<string, string> = {
|
||||||
|
pdf: 'application/pdf',
|
||||||
|
zip: 'application/zip',
|
||||||
|
gz: 'application/gzip',
|
||||||
|
tar: 'application/x-tar',
|
||||||
|
png: 'image/png',
|
||||||
|
jpg: 'image/jpeg',
|
||||||
|
jpeg: 'image/jpeg',
|
||||||
|
gif: 'image/gif',
|
||||||
|
webp: 'image/webp',
|
||||||
|
svg: 'image/svg+xml',
|
||||||
|
mp3: 'audio/mpeg',
|
||||||
|
mp4: 'video/mp4',
|
||||||
|
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getMimeType(filePath: string): string {
|
||||||
|
const ext = filePath.split('.').pop()?.toLowerCase() ?? '';
|
||||||
|
return MIME_TYPES[ext] ?? 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isImageMimeType(mimeType: string): boolean {
|
||||||
|
return mimeType.startsWith('image/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when the MIME type is a known binary format.
|
||||||
|
* application/octet-stream (unknown extension) returns false so we fall
|
||||||
|
* through to the CLI and let isBinaryContent() decide at runtime.
|
||||||
|
*/
|
||||||
|
function isBinaryMimeType(mimeType: string): boolean {
|
||||||
|
return !mimeType.startsWith('text/') && mimeType !== 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the vault's root filesystem path via `obsidian vault info=path`.
|
||||||
|
*/
|
||||||
|
async function getVaultRootPath(): Promise<string> {
|
||||||
|
const result = await executeObsidianCommand('vault', ['info=path']);
|
||||||
|
if (result.exitCode !== 0 || !result.stdout) {
|
||||||
|
throw new Error(`Failed to get vault filesystem path: ${result.stderr || 'no output'}`);
|
||||||
|
}
|
||||||
|
return result.stdout.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the vault-relative path for a note.
|
||||||
|
* - When `path` is provided it is already vault-relative, so return it directly.
|
||||||
|
* - When `file` (wikilink-style name) is provided, run `obsidian file file=<name>`
|
||||||
|
* and parse the `path` field from its tab-separated output.
|
||||||
|
*/
|
||||||
|
async function resolveVaultRelativePath(file?: string, path?: string): Promise<string> {
|
||||||
|
if (path) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
if (file) {
|
||||||
|
const result = await executeObsidianCommand('file', [formatParam('file', file)]);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
throw new Error(`File "${file}" not found in vault: ${result.stderr}`);
|
||||||
|
}
|
||||||
|
for (const line of result.stdout.split('\n')) {
|
||||||
|
const [key, value] = line.split('\t');
|
||||||
|
if (key?.trim() === 'path' && value?.trim()) {
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`Could not resolve vault path for file "${file}"`);
|
||||||
|
}
|
||||||
|
throw new Error('Either file or path must be provided');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register all file operation tools
|
* Register all file operation tools
|
||||||
*/
|
*/
|
||||||
@@ -151,7 +227,7 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
|
|||||||
// T031: Read note tool
|
// T031: Read note tool
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
'obsidian_read_note',
|
'obsidian_read_note',
|
||||||
'Read the content of a note from the Obsidian vault. Specify either the note name (file) or full path (path). For large files (e.g. PDFs), use max_chars and offset to read in chunks and avoid exceeding context limits. Binary files (ZIP, images, compiled files, etc.) are automatically detected and returned as a base64-encoded string prefixed with "BASE64:" — the client must base64-decode the value to recover the original binary content.',
|
'Read the content of a note from the Obsidian vault. Specify either the note name (file) or full path (path). For large text files, use max_chars and offset to read in chunks and avoid exceeding context limits. Binary files are read natively from the filesystem (bypassing the CLI): images (PNG, JPG, GIF, WEBP, SVG) are returned as MCP image content (type: "image") with base64-encoded data and mimeType; all other binary files (ZIP, PDF, DOCX, etc.) are returned as MCP embedded resource content (type: "resource") with uri, mimeType, and base64-encoded blob.',
|
||||||
{
|
{
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
@@ -174,7 +250,7 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
createToolHandler(
|
createToolHandler(
|
||||||
'Read the content of a note. Binary files are returned as a base64-encoded string prefixed with "BASE64:" — decode it to recover the original binary content.',
|
'Read the content of a note. Images (PNG, JPG, GIF, WEBP, SVG) are returned as MCP image content (type: "image") with base64-encoded data. Other binary files are returned as MCP embedded resource content (type: "resource") with uri, mimeType, and base64-encoded blob.',
|
||||||
{
|
{
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
@@ -200,22 +276,62 @@ export async function registerFileOperationTools(server: ObsidianMCPServer): Pro
|
|||||||
const validated = readNoteSchema.parse(args) as any;
|
const validated = readNoteSchema.parse(args) as any;
|
||||||
const sanitized = sanitizeParameters(validated) as any;
|
const sanitized = sanitizeParameters(validated) as any;
|
||||||
|
|
||||||
|
const identifier = (sanitized.file || sanitized.path) as string;
|
||||||
|
const mimeType = getMimeType(identifier);
|
||||||
|
|
||||||
|
// For known binary MIME types, bypass the CLI entirely and read via native FS.
|
||||||
|
// The Obsidian CLI does not support binary file output.
|
||||||
|
if (isBinaryMimeType(mimeType)) {
|
||||||
|
logger.debug('Binary file detected by extension, using native FS', { identifier, mimeType });
|
||||||
|
const vaultRoot = await getVaultRootPath();
|
||||||
|
const relPath = await resolveVaultRelativePath(sanitized.file, sanitized.path);
|
||||||
|
const fullPath = join(vaultRoot, relPath);
|
||||||
|
const buf = await readFile(fullPath);
|
||||||
|
const base64 = buf.toString('base64');
|
||||||
|
const vaultName = process.env.OBSIDIAN_VAULT ?? 'vault';
|
||||||
|
|
||||||
|
if (isImageMimeType(mimeType)) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'image' as const, data: base64, mimeType }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const uri = `obsidian://${encodeURIComponent(vaultName)}/${encodeURIComponent(relPath)}`;
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'resource' as const,
|
||||||
|
resource: { uri, mimeType, blob: base64 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text file — use the CLI
|
||||||
const cmdArgs: string[] = [];
|
const cmdArgs: string[] = [];
|
||||||
if (sanitized.file) cmdArgs.push(formatParam('file', sanitized.file as string));
|
if (sanitized.file) cmdArgs.push(formatParam('file', sanitized.file as string));
|
||||||
if (sanitized.path) cmdArgs.push(formatParam('path', sanitized.path as string));
|
if (sanitized.path) cmdArgs.push(formatParam('path', sanitized.path as string));
|
||||||
|
|
||||||
// Use binary-safe executor so stdout is collected as a raw Buffer,
|
const result = await executeObsidianCommand('read', cmdArgs);
|
||||||
// preventing UTF-8 decoding from corrupting binary file content.
|
handleCLIResult(result, { operation: 'read_note', identifier });
|
||||||
const result = await executeObsidianCommandBinary('read', cmdArgs);
|
|
||||||
handleCLIResult(result, { operation: 'read_note', identifier: sanitized.file || sanitized.path });
|
|
||||||
|
|
||||||
// Detect binary content from the raw buffer and return as base64
|
// Fallback: CLI returned binary bytes for an unknown-extension file
|
||||||
if (result.stdoutBuffer && isBinaryContent(result.stdoutBuffer)) {
|
const rawBuf = Buffer.from(result.stdout, 'utf8');
|
||||||
|
if (isBinaryContent(rawBuf)) {
|
||||||
|
logger.debug('Binary content detected at runtime, using native FS fallback', { identifier });
|
||||||
|
const vaultRoot = await getVaultRootPath();
|
||||||
|
const relPath = await resolveVaultRelativePath(sanitized.file, sanitized.path);
|
||||||
|
const fullPath = join(vaultRoot, relPath);
|
||||||
|
const buf = await readFile(fullPath);
|
||||||
|
const base64 = buf.toString('base64');
|
||||||
|
const vaultName = process.env.OBSIDIAN_VAULT ?? 'vault';
|
||||||
|
const fallbackMime = getMimeType(relPath);
|
||||||
|
const uri = `obsidian://${encodeURIComponent(vaultName)}/${encodeURIComponent(relPath)}`;
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'resource' as const,
|
||||||
text: `BASE64:${result.stdoutBuffer.toString('base64')}`,
|
resource: { uri, mimeType: fallbackMime, blob: base64 },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,10 +16,12 @@ export interface ToolInput {
|
|||||||
* Successful tool output
|
* Successful tool output
|
||||||
*/
|
*/
|
||||||
export interface ToolOutput {
|
export interface ToolOutput {
|
||||||
content: Array<{
|
content: Array<
|
||||||
type: 'text';
|
| { type: 'text'; text: string }
|
||||||
text: string;
|
| { type: 'image'; data: string; mimeType: string }
|
||||||
}>;
|
| { type: 'audio'; data: string; mimeType: string }
|
||||||
|
| { type: 'resource'; resource: { uri: string; mimeType?: string; blob: string } }
|
||||||
|
>;
|
||||||
isError?: false;
|
isError?: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user