Workspace initialization fails on first run with a "lock timeout" error because withLock() attempts to create a lock directory without ensuring its parent directory exists. This affects all versions since MCP v2 (commit ba1ae92, Nov 11 2025).
Affected file: src/lib/workspace/locks.ts
This issue was discovered while investigating "lock timeout" errors during script execution. The error message is misleading, as it suggests lock contention when the actual problem is a missing directory causing ENOENT errors misinterpreted as "lock already exists."
Root Cause
The lock mechanism depends on a directory created by the code it protects:
withLock() creates lock at: envPaths("datocms-mcp").data/workspace-init.lock.lock
- Parent directory:
envPaths("datocms-mcp").data (e.g., ~/Library/Application Support/datocms-mcp-nodejs/)
withLock() calls fs.mkdir(lockDir) without { recursive: true }
- When parent doesn't exist,
mkdir throws ENOENT
- Error is caught and misinterpreted as "lock already exists" (line 28)
- Infinite retry until timeout (~2 minutes)
The workspace root at src/lib/workspace/index.ts:79 creates this same directory with fs.mkdir(this.rootPath, { recursive: true }), but this runs inside the lock-protected function. The lock's parent directory is created by the code the lock protects.
Details
1. No code path creates parent before lock
The lock's parent directory is never created before lock acquisition. Here are all mkdir calls in the code:
// src/lib/workspace/locks.ts:19
await fs.mkdir(lockDir); // No recursive option, no parent creation
// src/lib/workspace/index.ts:79 (Inside withLock)
await fs.mkdir(this.rootPath, { recursive: true }); // Runs after lock acquisition
// src/lib/workspace/index.ts:80 (Inside withLock)
await fs.mkdir(this.scriptsPath, { recursive: true });
// src/lib/workspace/temp.ts:29
await fs.mkdir(scriptsDir, { recursive: true }); // Creates different path, not lock parent
2. Bug introduced in MCP v2
The locks.ts file was introduced in commit ba1ae92 (Nov 11, 2025).
To Reproduce
-
Ensure workspace directory doesn't exist:
# macOS
rm -rf ~/Library/Application\ Support/datocms-mcp-nodejs/
# Linux
rm -rf ~/.local/share/datocms-mcp-nodejs/
# Windows
# Remove %LOCALAPPDATA%\datocms-mcp-nodejs\
-
Run any MCP operation that requires workspace initialization:
{
"tool": "DatoCMS-create_script",
"arguments": {
"name": "script://test.ts",
"content": "import { type Client } from '@datocms/cma-client-node';\nexport default async function(client: Client) { console.log('test'); }",
"execute": true
}
}
-
Error after ~2 minutes:
Simple Fix
Adding parent directory creation before lock acquisition resolves the immediate issue. This simple fix works as an example, but doesn't address the problem from an architectural standpoint, including the ENOENT error being misinterpreted as the lock already existing:
// src/lib/workspace/locks.ts
export async function withLock<T>(
name: string,
fn: () => Promise<T>,
opts?: { timeoutMs?: number },
): Promise<T> {
const lockDir = path.join(envPaths("datocms-mcp").data, `${name}.lock`);
const timeoutMs = opts?.timeoutMs ?? 2 * 60 * 1000;
const start = Date.now();
// FIX: Ensure parent directory exists before attempting lock acquisition
await fs.mkdir(path.dirname(lockDir), { recursive: true });
while (true) {
try {
await fs.mkdir(lockDir);
// ... rest of function
Test Case
test("should initialize workspace even if parent directory doesn't exist", async () => {
const testRoot = path.join(os.tmpdir(), `test-workspace-${Date.now()}`);
// Ensure parent doesn't exist
await fs.rm(testRoot, { recursive: true, force: true });
const wm = new Workspace({ rootPath: testRoot });
await wm.ensure();
// Verify workspace was created
expect(await fs.access(testRoot).then(() => true, () => false)).toBe(true);
});
Workspace initialization fails on first run with a "lock timeout" error because
withLock()attempts to create a lock directory without ensuring its parent directory exists. This affects all versions since MCP v2 (commit ba1ae92, Nov 11 2025).Affected file:
src/lib/workspace/locks.tsThis issue was discovered while investigating "lock timeout" errors during script execution. The error message is misleading, as it suggests lock contention when the actual problem is a missing directory causing
ENOENTerrors misinterpreted as "lock already exists."Root Cause
The lock mechanism depends on a directory created by the code it protects:
withLock()creates lock at:envPaths("datocms-mcp").data/workspace-init.lock.lockenvPaths("datocms-mcp").data(e.g.,~/Library/Application Support/datocms-mcp-nodejs/)withLock()callsfs.mkdir(lockDir)without{ recursive: true }mkdirthrowsENOENTThe workspace root at
src/lib/workspace/index.ts:79creates this same directory withfs.mkdir(this.rootPath, { recursive: true }), but this runs inside the lock-protected function. The lock's parent directory is created by the code the lock protects.Details
1. No code path creates parent before lock
The lock's parent directory is never created before lock acquisition. Here are all
mkdircalls in the code:2. Bug introduced in MCP v2
The
locks.tsfile was introduced in commit ba1ae92 (Nov 11, 2025).To Reproduce
Ensure workspace directory doesn't exist:
Run any MCP operation that requires workspace initialization:
{ "tool": "DatoCMS-create_script", "arguments": { "name": "script://test.ts", "content": "import { type Client } from '@datocms/cma-client-node';\nexport default async function(client: Client) { console.log('test'); }", "execute": true } }Error after ~2 minutes:
Simple Fix
Adding parent directory creation before lock acquisition resolves the immediate issue. This simple fix works as an example, but doesn't address the problem from an architectural standpoint, including the
ENOENTerror being misinterpreted as the lock already existing:Test Case