file_reserve
Reserve file paths or glob patterns to prevent agent conflicts. Reservations expire after a configurable TTL; use check_only to check without reserving.
Instructions
Reserve files or glob patterns to prevent conflicts between agents. Use check_only=true to check for conflicts without reserving. Reservations expire after TTL.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| patterns | Yes | File paths or glob patterns to reserve (e.g. ['src/auth/**', 'package.json']) | |
| agent | Yes | Agent reserving the files | |
| ttl_minutes | No | Reservation TTL in minutes (default 15) | |
| check_only | No | If true, only check for conflicts without creating reservations |
Implementation Reference
- src/tools/file-reservation.ts:29-117 (handler)The async handler function for the 'file_reserve' tool. It checks for conflicts, optionally checks only (check_only mode), cleans expired reservations, and inserts new reservation records into the file_reservations table.
async ({ patterns, agent, ttl_minutes, check_only }) => { const db = getDb(); const now = new Date(); const expiresAt = new Date( now.getTime() + ttl_minutes * 60 * 1000 ).toISOString(); // Clean expired reservations first db.prepare( `DELETE FROM file_reservations WHERE expires_at < datetime('now')` ).run(); // Check for conflicts const existing = db .prepare(`SELECT * FROM file_reservations WHERE agent != ?`) .all(agent) as Array<{ pattern: string; agent: string; expires_at: string }>; const conflicts = existing.filter((r) => patterns.some( (p) => patternsOverlap(p, r.pattern) ) ); if (conflicts.length > 0) { return { content: [ { type: "text" as const, text: JSON.stringify({ reserved: false, has_conflicts: true, conflicts: conflicts.map((c) => ({ pattern: c.pattern, held_by: c.agent, expires_at: c.expires_at, })), }), }, ], }; } // Check-only mode: return clean result without reserving if (check_only) { return { content: [ { type: "text" as const, text: JSON.stringify({ reserved: false, has_conflicts: false, conflicts: [], }), }, ], }; } const insert = db.prepare( `INSERT INTO file_reservations (id, pattern, agent, expires_at) VALUES (?, ?, ?, ?)` ); const ids: string[] = []; const tx = db.transaction(() => { for (const pattern of patterns) { const id = generateId("res"); insert.run(id, pattern, agent, expiresAt); ids.push(id); } }); tx(); return { content: [ { type: "text" as const, text: JSON.stringify({ reserved: true, has_conflicts: false, reservation_ids: ids, agent, patterns, expires_at: expiresAt, }), }, ], }; } ); - src/tools/file-reservation.ts:13-28 (schema)Zod schema for the 'file_reserve' tool inputs: patterns (array of strings), agent (string with regex validation), ttl_minutes (number, 1-1440, default 15), check_only (boolean, default false).
{ patterns: z .array(z.string()) .describe("File paths or glob patterns to reserve (e.g. ['src/auth/**', 'package.json'])"), agent: z.string().max(256).regex(/^[a-zA-Z0-9_.-]+$/).describe("Agent reserving the files"), ttl_minutes: z .number() .min(1) .max(1440) .default(DEFAULT_TTL_MINUTES) .describe("Reservation TTL in minutes (default 15)"), check_only: z .boolean() .default(false) .describe("If true, only check for conflicts without creating reservations"), }, - src/server.ts:20-21 (registration)Registration of the file tools (including file_reserve) in the MCP server via registerFileTools(server).
registerFileTools(server); - src/tools/file-reservation.ts:8-173 (registration)The registerFileTools function that registers both 'file_reserve' and 'file_release' tools on the MCP server.
export function registerFileTools(server: McpServer): void { // ── Reserve Files ────────────────────────────────── server.tool( "file_reserve", "Reserve files or glob patterns to prevent conflicts between agents. Use check_only=true to check for conflicts without reserving. Reservations expire after TTL.", { patterns: z .array(z.string()) .describe("File paths or glob patterns to reserve (e.g. ['src/auth/**', 'package.json'])"), agent: z.string().max(256).regex(/^[a-zA-Z0-9_.-]+$/).describe("Agent reserving the files"), ttl_minutes: z .number() .min(1) .max(1440) .default(DEFAULT_TTL_MINUTES) .describe("Reservation TTL in minutes (default 15)"), check_only: z .boolean() .default(false) .describe("If true, only check for conflicts without creating reservations"), }, async ({ patterns, agent, ttl_minutes, check_only }) => { const db = getDb(); const now = new Date(); const expiresAt = new Date( now.getTime() + ttl_minutes * 60 * 1000 ).toISOString(); // Clean expired reservations first db.prepare( `DELETE FROM file_reservations WHERE expires_at < datetime('now')` ).run(); // Check for conflicts const existing = db .prepare(`SELECT * FROM file_reservations WHERE agent != ?`) .all(agent) as Array<{ pattern: string; agent: string; expires_at: string }>; const conflicts = existing.filter((r) => patterns.some( (p) => patternsOverlap(p, r.pattern) ) ); if (conflicts.length > 0) { return { content: [ { type: "text" as const, text: JSON.stringify({ reserved: false, has_conflicts: true, conflicts: conflicts.map((c) => ({ pattern: c.pattern, held_by: c.agent, expires_at: c.expires_at, })), }), }, ], }; } // Check-only mode: return clean result without reserving if (check_only) { return { content: [ { type: "text" as const, text: JSON.stringify({ reserved: false, has_conflicts: false, conflicts: [], }), }, ], }; } const insert = db.prepare( `INSERT INTO file_reservations (id, pattern, agent, expires_at) VALUES (?, ?, ?, ?)` ); const ids: string[] = []; const tx = db.transaction(() => { for (const pattern of patterns) { const id = generateId("res"); insert.run(id, pattern, agent, expiresAt); ids.push(id); } }); tx(); return { content: [ { type: "text" as const, text: JSON.stringify({ reserved: true, has_conflicts: false, reservation_ids: ids, agent, patterns, expires_at: expiresAt, }), }, ], }; } ); // ── Release File Reservations ────────────────────── server.tool( "file_release", "Release file reservations held by an agent.", { agent: z.string().max(256).regex(/^[a-zA-Z0-9_.-]+$/).describe("Agent releasing reservations"), patterns: z .array(z.string()) .optional() .describe("Specific patterns to release (omit to release all)"), }, async ({ agent, patterns }) => { const db = getDb(); if (patterns && patterns.length > 0) { const placeholders = patterns.map(() => "?").join(","); const result = db .prepare( `DELETE FROM file_reservations WHERE agent = ? AND pattern IN (${placeholders})` ) .run(agent, ...patterns); return { content: [ { type: "text" as const, text: JSON.stringify({ released: true, count: result.changes, agent, patterns, }), }, ], }; } const result = db .prepare(`DELETE FROM file_reservations WHERE agent = ?`) .run(agent); return { content: [ { type: "text" as const, text: JSON.stringify({ released: true, count: result.changes, agent, }), }, ], }; } ); } - The patternsOverlap helper function used to determine if two file patterns overlap (exact match, parent directory, or wildcard containment).
function patternsOverlap(a: string, b: string): boolean { if (a === b) return true; // Normalize: remove trailing slashes const na = a.replace(/\/+$/, ""); const nb = b.replace(/\/+$/, ""); if (na === nb) return true; // Check if one is a parent directory of the other const aBase = na.replace(/\/\*\*$/, "").replace(/\/\*$/, ""); const bBase = nb.replace(/\/\*\*$/, "").replace(/\/\*$/, ""); // If either has wildcards, check prefix containment if (na.includes("*") || nb.includes("*")) { return aBase.startsWith(bBase) || bBase.startsWith(aBase); } // Exact file paths: check if one is under the other's directory return na.startsWith(nb + "/") || nb.startsWith(na + "/"); } - src/types/index.ts:111-118 (schema)TypeScript interface FileReservation defining the shape of a reservation record (id, pattern, agent, expires_at, created_at).
// ── File Reservation Types ────────────────────────────── export interface FileReservation { id: string; pattern: string; agent: string; expires_at: string; created_at: string; } - src/database/index.ts:72-83 (helper)SQL schema for the file_reservations table including the index on agent column.
CREATE TABLE IF NOT EXISTS file_reservations ( id TEXT PRIMARY KEY, pattern TEXT NOT NULL, agent TEXT NOT NULL, expires_at TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_tasks_board ON tasks(board_id); CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); CREATE INDEX IF NOT EXISTS idx_contracts_project ON contracts(project); CREATE INDEX IF NOT EXISTS idx_reservations_agent ON file_reservations(agent);