@uarlouski/testrail-mcp-server 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+ import { TestRailClient } from "../client/testrail.js";
3
+ import { ToolDefinition, withErrorHandling } from "../types/custom.js";
4
+
5
+ const parameters = {
6
+ section_id: z.string().describe("The ID of the section where the case should be created"),
7
+ title: z.string().describe("The title of the test case (required)"),
8
+ fields: z.record(z.string(), z.any()).optional().describe("Optional object containing additional fields. Use get_case_fields to see available fields. Example: {\"priority_id\": 2, \"template_id\": 1, \"custom_automation_priority\": 1}"),
9
+ };
10
+
11
+ export const createCaseTool: ToolDefinition<typeof parameters, TestRailClient> = {
12
+ name: "create_case",
13
+ description: "Create a new test case in TestRail. IMPORTANT: Before creating a case, first call get_templates to discover available templates for the project, then use get_case_fields to see which fields are available for the chosen template. Requires section_id and title.",
14
+ parameters,
15
+ handler: withErrorHandling<typeof parameters, TestRailClient>(async ({ section_id, title, fields }, client) => {
16
+ const caseData = {
17
+ title,
18
+ ...fields,
19
+ };
20
+
21
+ const createdCase = await client.createCase(section_id, caseData);
22
+
23
+ return {
24
+ content: [
25
+ {
26
+ type: "text" as const,
27
+ text: JSON.stringify({
28
+ success: true,
29
+ case_id: createdCase.id,
30
+ message: `Test case C${createdCase.id} created successfully`,
31
+ }, null, 2),
32
+ },
33
+ ],
34
+ };
35
+ })
36
+ };
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+ import { processCustomFields } from "../utils/mapper.js";
3
+ import { TestRailClient } from "../client/testrail.js";
4
+ import { TestCaseResponse, ToolDefinition, withErrorHandling } from "../types/custom.js";
5
+
6
+ const parameters = {
7
+ case_id: z.string().describe("The ID of the test case (e.g. '123' or 'C123')"),
8
+ };
9
+
10
+ export const getCaseTool: ToolDefinition<typeof parameters, TestRailClient> = {
11
+ name: "get_case",
12
+ description: "Get a test case from TestRail by ID",
13
+ parameters,
14
+ handler: withErrorHandling<typeof parameters, TestRailClient>(async ({ case_id }, client) => {
15
+ const id = case_id.toUpperCase().startsWith("C") ? case_id.substring(1) : case_id;
16
+ const testCase = await client.getCase(id);
17
+
18
+ const [section, caseTypes, priorities, caseFields] = await Promise.all([
19
+ client.getSection(testCase.section_id.toString()),
20
+ client.getCaseTypes(),
21
+ client.getPriorities(),
22
+ client.getCaseFields()
23
+ ]);
24
+
25
+ const caseType = caseTypes.find(t => t.id === testCase.type_id);
26
+ const priority = priorities.find(p => p.id === testCase.priority_id);
27
+
28
+ const customFields = processCustomFields(testCase, caseFields);
29
+
30
+ const response: TestCaseResponse = {
31
+ id: testCase.id,
32
+ title: testCase.title,
33
+ section: section.name,
34
+ type: caseType ? caseType.name : "Unknown",
35
+ priority: priority ? priority.name : "Unknown",
36
+ labels: testCase.labels || [],
37
+ references: testCase.refs,
38
+ updated_on: testCase.updated_on,
39
+ ...customFields
40
+ };
41
+
42
+ return {
43
+ content: [
44
+ {
45
+ type: "text" as const,
46
+ text: JSON.stringify(response, null, 2),
47
+ },
48
+ ],
49
+ };
50
+ })
51
+ };
@@ -0,0 +1,97 @@
1
+ import { TestRailClient } from "../client/testrail.js";
2
+ import { ToolDefinition, withErrorHandling } from "../types/custom.js";
3
+ import { CaseField } from "../types/testrail.js";
4
+ import { parseDropdownOptions } from "../utils/mapper.js";
5
+
6
+ const parameters = {};
7
+
8
+ interface FieldSchema {
9
+ system_name: string;
10
+ label: string;
11
+ type: string;
12
+ is_required: boolean;
13
+ template_ids?: number[];
14
+ options?: string[];
15
+ }
16
+
17
+ interface CaseFieldsResponse {
18
+ fields: FieldSchema[];
19
+ }
20
+
21
+ const FIELD_TYPE_NAMES: Record<number, string> = {
22
+ 1: "String",
23
+ 2: "Integer",
24
+ 3: "Text",
25
+ 4: "URL",
26
+ 5: "Checkbox",
27
+ 6: "Dropdown",
28
+ 7: "User",
29
+ 8: "Date",
30
+ 9: "Milestone",
31
+ 10: "Steps",
32
+ 11: "Step Results",
33
+ 12: "Multi-select",
34
+ 13: "Scenarios",
35
+ };
36
+
37
+ function getFieldTypeName(typeId: number): string {
38
+ return FIELD_TYPE_NAMES[typeId] || `Unknown (${typeId})`;
39
+ }
40
+
41
+ function isFieldRequired(field: CaseField): boolean {
42
+ return field.configs.some(config => config.options?.is_required === true);
43
+ }
44
+
45
+ function mapToFieldSchema(field: CaseField): FieldSchema {
46
+ const schema: FieldSchema = {
47
+ system_name: field.system_name,
48
+ label: field.label,
49
+ type: getFieldTypeName(field.type_id),
50
+ is_required: isFieldRequired(field),
51
+ };
52
+
53
+ if (!field.include_all && field.template_ids.length > 0) {
54
+ schema.template_ids = field.template_ids;
55
+ }
56
+
57
+ if (field.type_id === 6 || field.type_id === 12) {
58
+ schema.options = Array.from(parseDropdownOptions(field).values());
59
+ }
60
+
61
+ return schema;
62
+ }
63
+
64
+ const SYSTEM_FIELDS: FieldSchema[] = [
65
+ { system_name: "title", label: "Title", type: "String", is_required: true },
66
+ { system_name: "section_id", label: "Section", type: "Integer", is_required: true },
67
+ { system_name: "template_id", label: "Template", type: "Integer", is_required: false },
68
+ { system_name: "type_id", label: "Type", type: "Integer", is_required: false },
69
+ { system_name: "priority_id", label: "Priority", type: "Integer", is_required: false },
70
+ { system_name: "estimate", label: "Estimate", type: "String", is_required: false },
71
+ { system_name: "milestone_id", label: "Milestone", type: "Integer", is_required: false },
72
+ { system_name: "refs", label: "References", type: "String", is_required: false },
73
+ ];
74
+
75
+ export const getCaseFieldsTool: ToolDefinition<typeof parameters, TestRailClient> = {
76
+ name: "get_case_fields",
77
+ description: "Get the field schema for test cases. Returns all available fields with their types, descriptions, and options (for dropdown fields). Use this to understand what fields can be set when creating or updating test cases.",
78
+ parameters,
79
+ handler: withErrorHandling<typeof parameters, TestRailClient>(async (_args, client) => {
80
+ const caseFields = await client.getCaseFields();
81
+
82
+ const customFieldSchemas = caseFields.filter(field => field.is_active).map(mapToFieldSchema);
83
+
84
+ const response: CaseFieldsResponse = {
85
+ fields: [...SYSTEM_FIELDS, ...customFieldSchemas],
86
+ };
87
+
88
+ return {
89
+ content: [
90
+ {
91
+ type: "text" as const,
92
+ text: JSON.stringify(response, null, 2),
93
+ },
94
+ ],
95
+ };
96
+ })
97
+ };
@@ -0,0 +1,67 @@
1
+ import { z } from "zod";
2
+ import { TestRailClient } from "../client/testrail.js";
3
+ import { ToolDefinition, withErrorHandling } from "../types/custom.js";
4
+ import { Case } from "../types/testrail.js";
5
+
6
+ const parameters = {
7
+ project_id: z.string().describe("The ID of the project"),
8
+ section_id: z.string().optional().describe("Optional section ID to filter cases by section"),
9
+ filter: z.record(z.string(), z.string()).optional().describe("Optional API filter parameters. Supported filters: priority_id (comma-separated IDs), type_id (comma-separated IDs), created_by (comma-separated user IDs), updated_by, milestone_id, refs, created_after, created_before, updated_after, updated_before. Example: {\"priority_id\": \"1,2\", \"type_id\": \"3\"}"),
10
+ where: z.record(z.string(), z.any()).optional().describe("Optional client-side filter for any field including custom fields. Cases are filtered after fetching. Supports exact match for values. Example: {\"custom_automation_status\": 1, \"priority_id\": 2}"),
11
+ fields: z.array(z.string()).optional().describe("Optional list of additional fields to include in the response. Use get_case_fields to see available field names. Example: [\"priority_id\", \"type_id\", \"custom_automation_status\"]"),
12
+ };
13
+
14
+ interface CasesResponse {
15
+ cases: Record<string, any>[];
16
+ }
17
+
18
+ export const getCasesTool: ToolDefinition<typeof parameters, TestRailClient> = {
19
+ name: "get_cases",
20
+ description: "Get all test cases for a project. Filter by section, API params (priority, type), or any field including custom fields via 'where'. Returns case IDs, titles, and any additional requested fields.",
21
+ parameters,
22
+ handler: withErrorHandling<typeof parameters, TestRailClient>(async ({ project_id, section_id, filter, where, fields }, client) => {
23
+ let cases: Case[] = await client.getCases(project_id, section_id, filter);
24
+
25
+ // Client-side filtering for custom fields and other unsupported API filters
26
+ if (where) {
27
+ cases = cases.filter(c => {
28
+ for (const [key, value] of Object.entries(where)) {
29
+ const caseValue = (c as Record<string, any>)[key];
30
+ if (caseValue !== value) {
31
+ return false;
32
+ }
33
+ }
34
+ return true;
35
+ });
36
+ }
37
+
38
+ const response: CasesResponse = {
39
+ cases: cases.map(c => {
40
+ const result: Record<string, any> = {
41
+ id: c.id,
42
+ title: c.title,
43
+ suite_id: c.suite_id,
44
+ };
45
+
46
+ if (fields) {
47
+ for (const field of fields) {
48
+ if (field in c) {
49
+ result[field] = c[field];
50
+ }
51
+ }
52
+ }
53
+
54
+ return result;
55
+ }),
56
+ };
57
+
58
+ return {
59
+ content: [
60
+ {
61
+ type: "text" as const,
62
+ text: JSON.stringify(response, null, 2),
63
+ },
64
+ ],
65
+ };
66
+ })
67
+ };
@@ -0,0 +1,40 @@
1
+ import { TestRailClient } from "../client/testrail.js";
2
+ import { ToolDefinition, withErrorHandling } from "../types/custom.js";
3
+
4
+ const parameters = {};
5
+
6
+ interface ProjectSchema {
7
+ id: number;
8
+ name: string;
9
+ }
10
+
11
+ interface ProjectsResponse {
12
+ projects: ProjectSchema[];
13
+ }
14
+
15
+ export const getProjectsTool: ToolDefinition<typeof parameters, TestRailClient> = {
16
+ name: "get_projects",
17
+ description: "Get all available projects in TestRail. Returns project IDs and names. Use this to find the project_id needed for get_sections and get_templates.",
18
+ parameters,
19
+ handler: withErrorHandling<typeof parameters, TestRailClient>(async (_args, client) => {
20
+ const projects = await client.getProjects();
21
+
22
+ const response: ProjectsResponse = {
23
+ projects: projects
24
+ .filter(p => !p.is_completed)
25
+ .map(p => ({
26
+ id: p.id,
27
+ name: p.name,
28
+ })),
29
+ };
30
+
31
+ return {
32
+ content: [
33
+ {
34
+ type: "text" as const,
35
+ text: JSON.stringify(response, null, 2),
36
+ },
37
+ ],
38
+ };
39
+ })
40
+ };
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ import { TestRailClient } from "../client/testrail.js";
3
+ import { ToolDefinition, withErrorHandling } from "../types/custom.js";
4
+
5
+ const parameters = {
6
+ project_id: z.string().describe("The ID of the project"),
7
+ };
8
+
9
+ interface SectionSchema {
10
+ id: number;
11
+ name: string;
12
+ }
13
+
14
+ interface SectionsResponse {
15
+ sections: SectionSchema[];
16
+ }
17
+
18
+ export const getSectionsTool: ToolDefinition<typeof parameters, TestRailClient> = {
19
+ name: "get_sections",
20
+ description: "Get all sections for a project. Returns section IDs and names. Use this to find the section_id needed for create_case.",
21
+ parameters,
22
+ handler: withErrorHandling<typeof parameters, TestRailClient>(async ({ project_id }, client) => {
23
+ const sections = await client.getSections(project_id);
24
+
25
+ const response: SectionsResponse = {
26
+ sections: sections.map(s => ({
27
+ id: s.id,
28
+ name: s.name,
29
+ })),
30
+ };
31
+
32
+ return {
33
+ content: [
34
+ {
35
+ type: "text" as const,
36
+ text: JSON.stringify(response, null, 2),
37
+ },
38
+ ],
39
+ };
40
+ })
41
+ };
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ import { TestRailClient } from "../client/testrail.js";
3
+ import { ToolDefinition, withErrorHandling } from "../types/custom.js";
4
+
5
+ const parameters = {
6
+ project_id: z.string().describe("The ID of the project"),
7
+ };
8
+
9
+ interface TemplateSchema {
10
+ id: number;
11
+ name: string;
12
+ }
13
+
14
+ interface TemplatesResponse {
15
+ templates: TemplateSchema[];
16
+ }
17
+
18
+ export const getTemplatesTool: ToolDefinition<typeof parameters, TestRailClient> = {
19
+ name: "get_templates",
20
+ description: "Get available test case templates for a project. Returns template IDs and names. Use template IDs with get_case_fields to understand which fields apply to each template.",
21
+ parameters,
22
+ handler: withErrorHandling<typeof parameters, TestRailClient>(async ({ project_id }, client) => {
23
+ const templates = await client.getTemplates(project_id);
24
+
25
+ const response: TemplatesResponse = {
26
+ templates: templates.map(t => ({
27
+ id: t.id,
28
+ name: t.name,
29
+ })),
30
+ };
31
+
32
+ return {
33
+ content: [
34
+ {
35
+ type: "text" as const,
36
+ text: JSON.stringify(response, null, 2),
37
+ },
38
+ ],
39
+ };
40
+ })
41
+ };
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+ import { TestRailClient } from "../client/testrail.js";
3
+ import { ToolDefinition, withErrorHandling } from "../types/custom.js";
4
+
5
+ const parameters = {
6
+ case_id: z.string().describe("The ID of the test case to update (e.g. '123' or 'C123')"),
7
+ fields: z.record(z.string(), z.any()).describe("Object containing fields to update. Use get_case_fields to see available fields. Example: {\"title\": \"New title\", \"priority_id\": 2, \"custom_automation_priority\": 1}"),
8
+ };
9
+
10
+ export const updateCaseTool: ToolDefinition<typeof parameters, TestRailClient> = {
11
+ name: "update_case",
12
+ description: "Update a test case in TestRail. Supports partial updates - only specify the fields you want to change. Use get_case_fields to see available fields and their types.",
13
+ parameters,
14
+ handler: withErrorHandling<typeof parameters, TestRailClient>(async ({ case_id, fields }, client) => {
15
+ const id = case_id.toUpperCase().startsWith("C") ? case_id.substring(1) : case_id;
16
+
17
+ const updatedCase = await client.updateCase(id, fields);
18
+
19
+ return {
20
+ content: [
21
+ {
22
+ type: "text" as const,
23
+ text: JSON.stringify({
24
+ success: true,
25
+ case_id: updatedCase.id,
26
+ message: `Test case C${updatedCase.id} updated successfully`,
27
+ }, null, 2),
28
+ },
29
+ ],
30
+ };
31
+ })
32
+ };
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+ import { TestRailClient } from "../client/testrail.js";
3
+ import { ToolDefinition, withErrorHandling } from "../types/custom.js";
4
+
5
+ const parameters = {
6
+ case_ids: z.array(z.number()).describe("Array of case IDs to update (e.g. [123, 456, 789])"),
7
+ fields: z.record(z.string(), z.any()).describe("Object containing fields to update for ALL specified cases. Use get_case_fields to see available fields. Example: {\"priority_id\": 2, \"type_id\": 1}"),
8
+ };
9
+
10
+ export const updateCasesTool: ToolDefinition<typeof parameters, TestRailClient> = {
11
+ name: "update_cases",
12
+ description: "Bulk update multiple test cases with the same field values. More efficient than calling update_case multiple times. All cases will receive the same field values.",
13
+ parameters,
14
+ handler: withErrorHandling<typeof parameters, TestRailClient>(async ({ case_ids, fields }, client) => {
15
+ const updatedCases = await client.updateCases(case_ids, fields);
16
+
17
+ return {
18
+ content: [
19
+ {
20
+ type: "text" as const,
21
+ text: JSON.stringify({
22
+ success: true,
23
+ updated_count: updatedCases.length,
24
+ case_ids: updatedCases.map(c => c.id),
25
+ message: `Successfully updated ${updatedCases.length} test cases`,
26
+ }, null, 2),
27
+ },
28
+ ],
29
+ };
30
+ })
31
+ };
@@ -0,0 +1,34 @@
1
+ import { z } from "zod";
2
+
3
+ export interface TestCaseResponse {
4
+ id: number;
5
+ title: string;
6
+ section: string;
7
+ type: string;
8
+ priority: string;
9
+ labels: string[];
10
+ references: string | null;
11
+ [key: string]: any;
12
+ }
13
+
14
+ export interface ToolDefinition<T extends z.ZodRawShape, Context = any> {
15
+ name: string;
16
+ description: string;
17
+ parameters: T;
18
+ handler: (args: z.infer<z.ZodObject<T>>, context: Context) => Promise<any>;
19
+ }
20
+
21
+ export function withErrorHandling<T extends z.ZodRawShape, Context = any>(
22
+ handler: (args: z.infer<z.ZodObject<T>>, context: Context) => Promise<any>
23
+ ): (args: z.infer<z.ZodObject<T>>, context: Context) => Promise<any> {
24
+ return async (args, context) => {
25
+ try {
26
+ return await handler(args, context);
27
+ } catch (error: any) {
28
+ return {
29
+ content: [{ type: "text", text: `Error: ${error.message}` }],
30
+ isError: true,
31
+ };
32
+ }
33
+ };
34
+ }
@@ -0,0 +1,87 @@
1
+ export interface Case {
2
+ id: number;
3
+ title: string;
4
+ section_id: number;
5
+ template_id: number;
6
+ type_id: number;
7
+ priority_id: number;
8
+ milestone_id: number | null;
9
+ refs: string | null;
10
+ created_by: number;
11
+ created_on: number;
12
+ updated_by: number;
13
+ updated_on: number;
14
+ estimate: string | null;
15
+ estimate_forecast: string | null;
16
+ suite_id: number;
17
+ display_order: number;
18
+ is_deleted: number;
19
+ labels: any[];
20
+ [key: string]: any;
21
+ }
22
+
23
+ export interface Section {
24
+ id: number;
25
+ name: string;
26
+ description: string | null;
27
+ parent_id: number | null;
28
+ depth: number;
29
+ display_order: number;
30
+ suite_id: number;
31
+ }
32
+
33
+ export interface Priority {
34
+ id: number;
35
+ is_default: boolean;
36
+ name: string;
37
+ priority: number;
38
+ short_name: string;
39
+ }
40
+
41
+ export interface CaseType {
42
+ id: number;
43
+ is_default: boolean;
44
+ name: string;
45
+ }
46
+
47
+ export interface CaseField {
48
+ id: number;
49
+ name: string;
50
+ system_name: string;
51
+ label: string;
52
+ type_id: number;
53
+ template_ids: number[];
54
+ is_active: boolean;
55
+ display_order: number;
56
+ description: string | null;
57
+ include_all: boolean;
58
+ configs: CaseFieldConfig[];
59
+ }
60
+
61
+ export interface CaseFieldConfig {
62
+ context: {
63
+ is_global: boolean;
64
+ project_ids: number[] | null;
65
+ };
66
+ options: {
67
+ default_value?: string;
68
+ format?: string;
69
+ is_required?: boolean;
70
+ rows?: string;
71
+ items?: string;
72
+ };
73
+ }
74
+
75
+ export interface Template {
76
+ id: number;
77
+ name: string;
78
+ is_default: boolean;
79
+ }
80
+
81
+ export interface Project {
82
+ id: number;
83
+ name: string;
84
+ announcement: string | null;
85
+ is_completed: boolean;
86
+ suite_mode: number;
87
+ }
@@ -0,0 +1,90 @@
1
+ import { Case, CaseField } from "../types/testrail.js";
2
+ import { sanitizeValue } from "./sanitizer.js";
3
+
4
+ /**
5
+ * Processes custom fields from a test case, converting system names to human-readable labels
6
+ * and mapping dropdown values to their display text.
7
+ */
8
+ export function processCustomFields(
9
+ testCase: Case,
10
+ caseFields: CaseField[]
11
+ ): Record<string, any> {
12
+ if (!testCase) {
13
+ throw new Error("Test case is undefined or null");
14
+ }
15
+
16
+ const templateId = testCase.template_id;
17
+ const result: Record<string, any> = {};
18
+
19
+ const applicableFields = caseFields.filter(field =>
20
+ field.include_all || field.template_ids.includes(templateId)
21
+ );
22
+
23
+ const fieldNameMap = new Map<string, string>();
24
+ const dropdownOptionsMap = new Map<string, Map<string, string>>();
25
+
26
+ for (const field of applicableFields) {
27
+ fieldNameMap.set(field.system_name, toSnakeCase(field.label));
28
+
29
+ const optionsMap = parseDropdownOptions(field);
30
+ if (optionsMap.size > 0) {
31
+ dropdownOptionsMap.set(field.system_name, optionsMap);
32
+ }
33
+ }
34
+
35
+ for (const [key, value] of Object.entries(testCase)) {
36
+ if (!key.startsWith("custom_")) continue;
37
+ if (value === null || value === undefined) continue;
38
+
39
+ if (!fieldNameMap.has(key)) {
40
+ console.error(`No field mapping found for: ${key}`);
41
+ result[key] = value;
42
+ continue;
43
+ }
44
+
45
+ const outputKey = fieldNameMap.get(key)!;
46
+ let finalValue = value;
47
+
48
+ if (dropdownOptionsMap.has(key)) {
49
+ const options = dropdownOptionsMap.get(key)!;
50
+ const optionKey = String(value);
51
+ finalValue = options.get(optionKey) || value;
52
+ }
53
+
54
+ result[outputKey] = sanitizeValue(finalValue);
55
+ }
56
+
57
+ return result;
58
+ }
59
+
60
+ /**
61
+ * Parse dropdown options from field configs.
62
+ * Format: "1, First\n2, Second\n3, Third"
63
+ */
64
+ export function parseDropdownOptions(field: CaseField): Map<string, string> {
65
+ const options = new Map<string, string>();
66
+
67
+ for (const config of field.configs) {
68
+ const items = config.options?.items;
69
+ if (!items) continue;
70
+
71
+ const lines = items.split('\n');
72
+ for (const line of lines) {
73
+ const commaIndex = line.indexOf(',');
74
+ if (commaIndex > 0) {
75
+ const id = line.substring(0, commaIndex).trim();
76
+ const label = line.substring(commaIndex + 1).trim();
77
+ options.set(id, label);
78
+ }
79
+ }
80
+ }
81
+
82
+ return options;
83
+ }
84
+
85
+ function toSnakeCase(str: string): string {
86
+ return str
87
+ .toLowerCase()
88
+ .replace(/[^a-z0-9]+/g, '_')
89
+ .replace(/^_|_$/g, '');
90
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Recursively ыtrips inline style attributes from HTML tags contained in string values
3
+ * in an object or array. Useful for cleaning up TestRail markdown content before sending
4
+ * to LLMs.
5
+ */
6
+ export function sanitizeValue(value: any): any {
7
+ if (typeof value === 'string') {
8
+ return value.replace(/\s*style\s*=\s*["'][^"']*["']/gi, '');
9
+ }
10
+ if (Array.isArray(value)) {
11
+ return value.map(sanitizeValue);
12
+ }
13
+ if (value !== null && typeof value === 'object') {
14
+ const result: Record<string, any> = {};
15
+ for (const [key, val] of Object.entries(value)) {
16
+ result[key] = sanitizeValue(val);
17
+ }
18
+ return result;
19
+ }
20
+ return value;
21
+ }