Files
obsidian-mcp/src/validation/schemas.ts
Peter.Morton c577c07877 refactor: update search tool to match Obsidian CLI spec
Removed obsidian_search_with_context tool (not in CLI spec)
Updated obsidian_search to use exact CLI parameter names:
- query (required) - Search query text
- path (optional) - Limit search to folder path
- limit (optional) - Max number of files to return
- total (optional) - Return match count instead of file list
- case (optional) - Case sensitive search
- format (optional) - Output format: text or json (default: text)

Changed parameter names to match CLI:
- folder → path
- caseSensitive → case
- Added: total flag for match counts
- Removed: contextLines (not in CLI)

Files updated:
- src/tools/search.ts: Simplified to single search tool
- src/validation/schemas.ts: Updated searchSchema parameters
- manifest.json: Removed search_with_context, updated description
- tasks.md: Marked T048 as REMOVED

Total tools: 20 (was 21)
- User Story 1: 9 tools
- User Story 2: 11 tools (was 12)

Build:  0 errors
Validation:  Manifest passes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-22 12:26:17 -05:00

168 lines
4.6 KiB
TypeScript

/**
* Zod Validation Schemas
* Constitutional Principle III: Local Execution Security
* Constitutional Principle IV: Defensive Programming
*/
import { z } from 'zod';
/**
* Common validation patterns
*/
// Vault name validation (alphanumeric, spaces, hyphens, underscores)
export const vaultNameSchema = z
.string()
.min(1, 'Vault name cannot be empty')
.max(255, 'Vault name too long')
.regex(/^[a-zA-Z0-9\s_-]+$/, 'Vault name contains invalid characters');
// Note name validation (allow most characters, but not path separators)
export const noteNameSchema = z
.string()
.min(1, 'Note name cannot be empty')
.max(255, 'Note name too long')
.regex(/^[^/\\]+$/, 'Note name cannot contain path separators');
// File path validation (allow subdirectories)
export const filePathSchema = z
.string()
.min(1, 'File path cannot be empty')
.max(1024, 'File path too long')
.regex(/^[^<>:"|?*]+$/, 'File path contains invalid characters');
// Content validation (allow any string, but limit size)
export const contentSchema = z
.string()
.max(1048576, 'Content too large (max 1MB)'); // 1MB limit
// Tag validation (must start with #)
export const tagSchema = z
.string()
.min(2, 'Tag too short')
.regex(/^#[a-zA-Z0-9_/-]+$/, 'Invalid tag format (must start with # and contain only alphanumeric, _, /, -)');
// Property key validation
export const propertyKeySchema = z
.string()
.min(1, 'Property key cannot be empty')
.max(100, 'Property key too long')
.regex(/^[a-zA-Z0-9_-]+$/, 'Property key contains invalid characters');
// Boolean flag validation
export const booleanFlagSchema = z
.union([z.boolean(), z.string()])
.transform((val) => {
if (typeof val === 'boolean') return val;
return val.toLowerCase() === 'true' || val === '1';
});
// Optional string that can be undefined or empty
export const optionalStringSchema = z.string().optional();
// Date string validation (ISO 8601)
export const dateSchema = z
.string()
.regex(/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?)?$/, 'Invalid date format (use ISO 8601)');
/**
* Common parameter schemas
*/
// File identifier (either file name or full path)
export const fileIdentifierSchema = z.object({
file: z.string().optional(),
path: z.string().optional(),
}).refine(
(data) => data.file || data.path,
{ message: 'Either file or path must be provided' }
);
// Pagination parameters
export const paginationSchema = z.object({
limit: z.number().int().positive().max(1000).optional(),
offset: z.number().int().nonnegative().optional(),
});
// Format options
export const formatSchema = z.object({
format: z.enum(['json', 'tsv', 'csv', 'text']).optional().default('text'),
});
/**
* Tool-specific schemas
*/
// Create note parameters
export const createNoteSchema = z.object({
name: noteNameSchema,
path: filePathSchema.optional(),
content: contentSchema.optional(),
template: z.string().optional(),
overwrite: booleanFlagSchema.optional(),
open: booleanFlagSchema.optional(),
});
// Read note parameters
export const readNoteSchema = z.union([
z.object({ file: noteNameSchema }),
z.object({ path: filePathSchema }),
]).refine(
(data) => ('file' in data && data.file) || ('path' in data && data.path),
{ message: 'Either file or path must be provided' }
);
// Append/Prepend parameters
export const appendPrependSchema = z.intersection(
fileIdentifierSchema,
z.object({
content: contentSchema,
inline: booleanFlagSchema.optional(),
})
);
// Delete note parameters
export const deleteNoteSchema = z.intersection(
fileIdentifierSchema,
z.object({
permanent: booleanFlagSchema.optional(),
})
);
// Move/Rename parameters
export const moveRenameSchema = z.intersection(
fileIdentifierSchema,
z.object({
newPath: filePathSchema.optional(),
newName: noteNameSchema.optional(),
})
).refine(
(data) => data.newPath || data.newName,
{ message: 'Either newPath or newName must be provided' }
);
// Search parameters
export const searchSchema = z.object({
query: z.string().min(1, 'Search query cannot be empty'),
path: optionalStringSchema, // Folder path to limit search
limit: z.number().int().positive().max(1000).optional(),
total: booleanFlagSchema.optional(), // Return match count instead of files
case: booleanFlagSchema.optional(), // Case sensitive search
format: z.enum(['text', 'json']).optional().default('text'),
});
// Tag search parameters
export const tagSearchSchema = z.object({
tag: tagSchema,
...formatSchema.shape,
});
// Property parameters
export const propertySchema = z.intersection(
fileIdentifierSchema,
z.object({
key: propertyKeySchema,
value: z.unknown().optional(),
})
);