feat: Phase 3 - Discovery Endpoints (stat and exists tools)

- Add stat tool for detailed file/folder metadata
- Add exists tool for fast path existence checking
- Add StatResult and ExistsResult type definitions
- Implement stat() and exists() methods in VaultTools
- Register both tools in ToolRegistry with complete schemas
- Update version to 2.1.0
- Update ROADMAP.md to mark Phase 3 complete
- Update CHANGELOG.md with Phase 3 release notes
- Add IMPLEMENTATION_NOTES_PHASE3.md documentation
- Clean up old phase documentation files
This commit is contained in:
2025-10-16 22:59:38 -04:00
parent 9d07ec64e2
commit 83ac6bedfa
12 changed files with 614 additions and 572 deletions

View File

@@ -116,6 +116,34 @@ export class ToolRegistry {
}
}
}
},
{
name: "stat",
description: "Get detailed metadata for a file or folder at a specific path. Returns existence status, kind (file or directory), and full metadata including size, dates, etc. Use this to check if a path exists and get its properties. More detailed than exists() but slightly slower. Returns structured JSON with path, exists boolean, kind, and metadata object.",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Vault-relative path to check (e.g., 'folder/note.md' or 'projects'). Can be a file or folder. Paths are case-sensitive on macOS/Linux. Do not use leading or trailing slashes."
}
},
required: ["path"]
}
},
{
name: "exists",
description: "Quickly check if a file or folder exists at a specific path. Returns existence status and kind (file or directory) without fetching full metadata. Faster than stat() when you only need to verify existence. Use this before operations that require a path to exist. Returns structured JSON with path, exists boolean, and optional kind.",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Vault-relative path to check (e.g., 'folder/note.md' or 'projects'). Can be a file or folder. Paths are case-sensitive on macOS/Linux. Do not use leading or trailing slashes."
}
},
required: ["path"]
}
}
];
}
@@ -137,6 +165,10 @@ export class ToolRegistry {
return await this.vaultTools.getVaultInfo();
case "list_notes":
return await this.vaultTools.listNotes(args.path);
case "stat":
return await this.vaultTools.stat(args.path);
case "exists":
return await this.vaultTools.exists(args.path);
default:
return {
content: [{ type: "text", text: `Unknown tool: ${name}` }],

View File

@@ -1,5 +1,5 @@
import { App, TFile, TFolder } from 'obsidian';
import { CallToolResult, FileMetadata, DirectoryMetadata, VaultInfo, SearchResult, SearchMatch } from '../types/mcp-types';
import { CallToolResult, FileMetadata, DirectoryMetadata, VaultInfo, SearchResult, SearchMatch, StatResult, ExistsResult } from '../types/mcp-types';
import { PathUtils } from '../utils/path-utils';
import { ErrorMessages } from '../utils/error-messages';
@@ -239,4 +239,119 @@ export class VaultTools {
modified: modified
};
}
// Phase 3: Discovery Endpoints
async stat(path: string): Promise<CallToolResult> {
// Validate path
if (!PathUtils.isValidVaultPath(path)) {
return {
content: [{ type: "text", text: ErrorMessages.invalidPath(path) }],
isError: true
};
}
// Normalize the path
const normalizedPath = PathUtils.normalizePath(path);
// Check if it's a file
const file = PathUtils.resolveFile(this.app, normalizedPath);
if (file) {
const result: StatResult = {
path: normalizedPath,
exists: true,
kind: "file",
metadata: this.createFileMetadata(file)
};
return {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}]
};
}
// Check if it's a folder
const folder = PathUtils.resolveFolder(this.app, normalizedPath);
if (folder) {
const result: StatResult = {
path: normalizedPath,
exists: true,
kind: "directory",
metadata: this.createDirectoryMetadata(folder)
};
return {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}]
};
}
// Path doesn't exist
const result: StatResult = {
path: normalizedPath,
exists: false
};
return {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}]
};
}
async exists(path: string): Promise<CallToolResult> {
// Validate path
if (!PathUtils.isValidVaultPath(path)) {
return {
content: [{ type: "text", text: ErrorMessages.invalidPath(path) }],
isError: true
};
}
// Normalize the path
const normalizedPath = PathUtils.normalizePath(path);
// Check if it's a file
if (PathUtils.fileExists(this.app, normalizedPath)) {
const result: ExistsResult = {
path: normalizedPath,
exists: true,
kind: "file"
};
return {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}]
};
}
// Check if it's a folder
if (PathUtils.folderExists(this.app, normalizedPath)) {
const result: ExistsResult = {
path: normalizedPath,
exists: true,
kind: "directory"
};
return {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}]
};
}
// Path doesn't exist
const result: ExistsResult = {
path: normalizedPath,
exists: false
};
return {
content: [{
type: "text",
text: JSON.stringify(result, null, 2)
}]
};
}
}

View File

@@ -107,3 +107,17 @@ export interface SearchResult {
filesSearched: number;
filesWithMatches: number;
}
// Phase 3: Discovery Endpoint Types
export interface StatResult {
path: string;
exists: boolean;
kind?: ItemKind;
metadata?: FileMetadata | DirectoryMetadata;
}
export interface ExistsResult {
path: string;
exists: boolean;
kind?: ItemKind;
}