refactor: extract metadata validation to shared utility

- Add validateMcpMetadata() utility function in tools/utils.js
- Replace duplicated validation code in update-task.js and update-subtask.js
- Reduces code duplication and ensures consistent validation

Addresses CodeRabbit nitpick about duplicated metadata validation.
This commit is contained in:
Cedric Hurst
2026-01-02 17:40:42 -06:00
committed by Ralph Khreish
parent bcee5b75dd
commit 7949faa352
3 changed files with 87 additions and 61 deletions

View File

@@ -7,7 +7,8 @@ import { TaskIdSchemaForMcp } from '@tm/core';
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
withNormalizedProjectRoot,
validateMcpMetadata
} from '@tm/mcp';
import { z } from 'zod';
import { resolveTag } from '../../../scripts/modules/utils.js';
@@ -77,35 +78,14 @@ export function registerUpdateSubtaskTool(server) {
}
// Validate metadata if provided
let parsedMetadata = null;
if (args.metadata) {
// Check if metadata updates are allowed
const allowMetadataUpdates =
process.env.TASK_MASTER_ALLOW_METADATA_UPDATES === 'true';
if (!allowMetadataUpdates) {
return createErrorResponse(
'Metadata updates are disabled. Set TASK_MASTER_ALLOW_METADATA_UPDATES=true in your MCP server environment to enable metadata modifications.'
);
}
// Parse and validate JSON
try {
parsedMetadata = JSON.parse(args.metadata);
if (
typeof parsedMetadata !== 'object' ||
parsedMetadata === null ||
Array.isArray(parsedMetadata)
) {
return createErrorResponse(
'Invalid metadata: must be a JSON object (not null or array)'
);
}
} catch {
return createErrorResponse(
`Invalid metadata JSON: ${args.metadata}. Provide a valid JSON object string.`
);
}
const validationResult = validateMcpMetadata(
args.metadata,
createErrorResponse
);
if (validationResult.error) {
return validationResult.error;
}
const parsedMetadata = validationResult.parsedMetadata;
// Validate that at least prompt or metadata is provided
if (!args.prompt && !parsedMetadata) {
return createErrorResponse(

View File

@@ -6,7 +6,8 @@
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
withNormalizedProjectRoot,
validateMcpMetadata
} from '@tm/mcp';
import { z } from 'zod';
import { resolveTag } from '../../../scripts/modules/utils.js';
@@ -85,44 +86,23 @@ export function registerUpdateTaskTool(server) {
);
}
// 3. Validate metadata if provided
let parsedMetadata = null;
if (args.metadata) {
// Check if metadata updates are allowed
const allowMetadataUpdates =
process.env.TASK_MASTER_ALLOW_METADATA_UPDATES === 'true';
if (!allowMetadataUpdates) {
return createErrorResponse(
'Metadata updates are disabled. Set TASK_MASTER_ALLOW_METADATA_UPDATES=true in your MCP server environment to enable metadata modifications.'
);
}
// Parse and validate JSON
try {
parsedMetadata = JSON.parse(args.metadata);
if (
typeof parsedMetadata !== 'object' ||
parsedMetadata === null ||
Array.isArray(parsedMetadata)
) {
return createErrorResponse(
'Invalid metadata: must be a JSON object (not null or array)'
);
}
} catch {
return createErrorResponse(
`Invalid metadata JSON: ${args.metadata}. Provide a valid JSON object string.`
);
}
// Validate metadata if provided
const validationResult = validateMcpMetadata(
args.metadata,
createErrorResponse
);
if (validationResult.error) {
return validationResult.error;
}
// 4. Validate that at least prompt or metadata is provided
const parsedMetadata = validationResult.parsedMetadata;
// Validate that at least prompt or metadata is provided
if (!args.prompt && !parsedMetadata) {
return createErrorResponse(
'Either prompt or metadata must be provided for update-task'
);
}
// 5. Call Direct Function - Include projectRoot and metadata
// Call Direct Function - Include projectRoot and metadata
const result = await updateTaskByIdDirect(
{
tasksJsonPath: tasksJsonPath,

View File

@@ -829,6 +829,72 @@ function checkProgressCapability(reportProgress, log) {
return reportProgress;
}
/**
* Validates and parses metadata string for MCP tools.
* Checks environment flag, validates JSON format, and ensures metadata is a plain object.
*
* @param {string|null|undefined} metadataString - JSON string to parse and validate
* @param {Function} createErrorResponse - Function to create error response
* @returns {{parsedMetadata: Object|null, error?: Object}} Object with parsed metadata or error
*
* @example Success case:
* const result = validateMcpMetadata('{"key":"value"}', createErrorResponse);
* if (result.error) return result.error;
* const metadata = result.parsedMetadata; // { key: "value" }
*
* @example Disabled case:
* // When TASK_MASTER_ALLOW_METADATA_UPDATES !== 'true'
* const result = validateMcpMetadata('{"key":"value"}', createErrorResponse);
* // Returns: { error: <error response> }
*
* @example Invalid JSON:
* const result = validateMcpMetadata('{invalid}', createErrorResponse);
* // Returns: { error: <error response> }
*/
export function validateMcpMetadata(metadataString, createErrorResponse) {
// Return null if no metadata provided
if (!metadataString) {
return { parsedMetadata: null };
}
// Check if metadata updates are allowed via environment variable
const allowMetadataUpdates =
process.env.TASK_MASTER_ALLOW_METADATA_UPDATES === 'true';
if (!allowMetadataUpdates) {
return {
error: createErrorResponse(
'Metadata updates are disabled. Set TASK_MASTER_ALLOW_METADATA_UPDATES=true in your MCP server environment to enable metadata modifications.'
)
};
}
// Parse and validate JSON
try {
const parsedMetadata = JSON.parse(metadataString);
// Ensure it's a plain object (not null, not array)
if (
typeof parsedMetadata !== 'object' ||
parsedMetadata === null ||
Array.isArray(parsedMetadata)
) {
return {
error: createErrorResponse(
'Invalid metadata: must be a JSON object (not null or array)'
)
};
}
return { parsedMetadata };
} catch {
return {
error: createErrorResponse(
`Invalid metadata JSON: ${metadataString}. Provide a valid JSON object string.`
)
};
}
}
// Ensure all functions are exported
export {
getProjectRoot,