Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/tools/customFields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp'
import axios from 'axios'
import { QASPHERE_API_KEY, QASPHERE_TENANT_URL } from '../config.js'
import type { CustomFieldsResponse } from '../types.js'
import { projectCodeSchema } from '../schemas.js'

export const registerTools = (server: McpServer) => {
server.tool(
'list_custom_fields',
"List all custom fields available for a project. This endpoint is useful when creating or updating test cases that include custom field values. Custom fields allow you to extend test cases with additional metadata specific to your organization's needs.",
{
projectCode: projectCodeSchema,
},
async ({ projectCode }: { projectCode: string }) => {
try {
const response = await axios.get<CustomFieldsResponse>(
`${QASPHERE_TENANT_URL}/api/public/v0/project/${projectCode}/custom-field`,
{
headers: {
Authorization: `ApiKey ${QASPHERE_API_KEY}`,
'Content-Type': 'application/json',
},
}
)

return {
content: [
{
type: 'text',
text: JSON.stringify(response.data.customFields),
},
],
}
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
switch (error.response?.status) {
case 404:
throw new Error(`Project with code '${projectCode}' not found.`)
case 401:
throw new Error('Invalid or missing API key')
case 403:
throw new Error('Insufficient permissions or suspended tenant')
default:
throw new Error(
`Failed to fetch custom fields: ${error.response?.data?.message || error.message}`
)
}
}
throw error
}
}
)
}
2 changes: 2 additions & 0 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { registerTools as registerProjectsTools } from './projects.js'
import { registerTools as registerTestCasesTools } from './tcases.js'
import { registerTools as registerTestFoldersTools } from './folders.js'
import { registerTools as registerTestTagsTools } from './tags.js'
import { registerTools as registerCustomFieldsTools } from './customFields.js'

export const registerTools = (server: McpServer) => {
registerProjectsTools(server)
registerTestCasesTools(server)
registerTestFoldersTools(server)
registerTestTagsTools(server)
registerCustomFieldsTools(server)
}
29 changes: 29 additions & 0 deletions src/tools/tcases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ export const registerTools = (server: McpServer) => {
)
.optional()
.describe('Additional links relevant to the test case'),
customFields: tcaseCustomFieldParamSchema,
parameterValues: z
.array(
z.object({
Expand Down Expand Up @@ -430,6 +431,7 @@ export const registerTools = (server: McpServer) => {
)
.optional()
.describe('Additional links relevant to the test case'),
customFields: tcaseCustomFieldParamSchema,
parameterValues: z
.array(
z.object({
Expand Down Expand Up @@ -503,6 +505,33 @@ export const registerTools = (server: McpServer) => {
)
}

const tcaseCustomFieldParamSchema = z
.record(
z.string(),
z
.object({
value: z
.string()
.optional()
.describe(
"The actual value for the field. For text fields: any string value. For dropdown fields: must match one of the option value strings from the field's options array. Omit if 'isDefault' is true."
),
isDefault: z
.boolean()
.optional()
.describe(
"Boolean indicating whether to use the field's default value (if true, the value field should be omitted)"
),
})
.refine((data) => data.value !== undefined || data.isDefault !== undefined, {
message: "For each custom field provided, either 'value' or 'isDefault' must be specified.",
})
)
.optional()
.describe(
'Custom field values. Use the systemName property from custom fields as the key. Only enabled fields should be used. Use list_custom_fields tool to get the custom fields.'
)

const testCaseMarkerSchema = z
.string()
.regex(
Expand Down
41 changes: 31 additions & 10 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ export interface CreateTestCaseLink {
url: string // URL of the link (1-255 characters)
}

export interface CreateTestCaseCustomField {
isDefault: boolean // Whether to set the default value
value: string // Custom field value to be set
export interface TestCaseCustomFieldValue {
isDefault?: boolean // Whether to set the default value (if true, the value field should be omitted)
value?: string // Custom field value to be set. For text fields: any string value. For dropdown fields: must match one of the option value strings. Omit if 'isDefault' is true.
}

export interface CreateTestCaseParameterValue {
Expand All @@ -140,7 +140,7 @@ export interface CreateTestCaseRequest {
tags?: string[] // Optional: List of tag titles (max 255 characters each)
requirements?: CreateTestCaseRequirement[] // Optional: Test case requirements
links?: CreateTestCaseLink[] // Optional: Additional links relevant to the test case
customFields?: { [key: string]: CreateTestCaseCustomField } // Optional: Custom field values
customFields?: { [key: string]: TestCaseCustomFieldValue } // Optional: Custom field values
parameterValues?: CreateTestCaseParameterValue[] // Optional: Values to substitute for parameters in template test cases
filledTCaseTitleSuffixParams?: string[] // Optional: Parameters to append to filled test case titles
isDraft?: boolean // Whether to create as draft, default false
Expand Down Expand Up @@ -168,11 +168,6 @@ export interface UpdateTestCaseLink {
url: string // URL of the link (1-255 characters)
}

export interface UpdateTestCaseCustomField {
isDefault: boolean // Whether to set the default value
value: string // Custom field value to be set
}

export interface UpdateTestCaseParameterValue {
tcaseId?: string // Should be specified to update existing filled test case
values: { [key: string]: string } // Values for the parameters in the template test case
Expand All @@ -187,10 +182,36 @@ export interface UpdateTestCaseRequest {
tags?: string[] // Optional: List of tag titles (max 255 characters each)
requirements?: UpdateTestCaseRequirement[] // Optional: Test case requirements
links?: UpdateTestCaseLink[] // Optional: Additional links relevant to the test case
customFields?: { [key: string]: UpdateTestCaseCustomField } // Optional: Custom field values
customFields?: { [key: string]: TestCaseCustomFieldValue } // Optional: Custom field values
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question - how does AI infer that the key should be the system name ?

Copy link

@satvik007 satvik007 Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In zod schema, there is information about it

  .describe(
    'Custom field values. Use the systemName property from custom fields as the key. Only enabled fields should be used. Use list_custom_fields tool to get the custom fields.'
  )

but I am not sure if this information reaches the AI agent.

parameterValues?: UpdateTestCaseParameterValue[] // Optional: Values to substitute for parameters in template test cases
}

export interface UpdateTestCaseResponse {
message: string // Success message
}

// Custom Fields API Types
export interface CustomFieldOption {
id: string // Option identifier
value: string // Option display value
}

export interface CustomField {
id: string // Unique custom field identifier
type: 'text' | 'dropdown' // Field type
systemName: string // System identifier for the field (used in API requests)
name: string // Display name of the field
required: boolean // Whether the field is required for test cases
enabled: boolean // Whether the field is currently enabled
options?: CustomFieldOption[] // Available options (only for dropdown fields)
defaultValue?: string // Default value for the field
pos: number // Display position/order
allowAllProjects: boolean // Whether the field is available to all projects
allowedProjectIds?: string[] // List of project IDs if not available to all projects
createdAt: string // ISO 8601 timestamp when the field was created
updatedAt: string // ISO 8601 timestamp when the field was last updated
}

export interface CustomFieldsResponse {
customFields: CustomField[] // Array of custom fields
}