Skip to main content
Glama
TCSoftInc

TestCollab MCP Server

by TCSoftInc

list_test_cases

Retrieve test cases from TestCollab projects with filtering, sorting, and pagination options to manage testing workflows efficiently.

Instructions

List test cases from a TestCollab project with optional filtering, sorting, and pagination. Tip: Call get_project_context first to resolve suite/tag/custom field names to IDs. Note: list_test_cases may omit full step details; use get_test_case for a complete test case with steps.

Filter fields include:

  • id, title, description, steps, priority (0=Low, 1=Normal, 2=High)

  • suite (ID or title), created_by, reviewer, poster (user IDs)

  • created_at, updated_at, last_run_on (dates)

  • tags, requirements (arrays of IDs or names)

  • under_review, is_automated (0 or 1)

  • run_count, avg_execution_time, failure_rate

Filter types:

  • text: equals, notEqual, contains, notContains, startsWith, endsWith, isBlank

  • number: equals, notEqual, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, inRange

  • date: equals, notEqual, greaterThan, lessThan, inRange

Example filter: { "priority": { "filterType": "number", "type": "greaterThanOrEqual", "filter": 1 }, "title": { "filterType": "text", "type": "contains", "filter": "login" } }

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
project_idNoProject ID (optional if TC_DEFAULT_PROJECT env var is set)
suiteNoFilter by suite ID or title
filterNoFilter conditions object
sortNoSort specification array, e.g. [{ colId: 'updated_at', sort: 'desc' }]
limitNoMaximum results to return (1-100, default: 50)
offsetNoNumber of results to skip (default: 0)

Implementation Reference

  • The handleListTestCases function, which acts as the MCP tool handler for 'list_test_cases', managing input parsing, project/suite resolution, filtering, and API invocation.
    export async function handleListTestCases(
      args: unknown
    ): Promise<{ content: Array<{ type: "text"; text: string }> }> {
      // Validate input
      const parsed = listTestCasesSchema.safeParse(args);
      if (!parsed.success) {
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify({
                error: {
                  code: "VALIDATION_ERROR",
                  message: "Invalid input parameters",
                  details: parsed.error.errors,
                },
              }),
            },
          ],
        };
      }
    
      const { project_id, suite, filter, sort, limit, offset } = parsed.data;
    
      // Resolve project ID: use provided value or fall back to default
      // Check request context first (HTTP transport), then env config (stdio transport)
      const requestContext = getRequestContext();
      const envConfig = requestContext ? null : getConfig();
      const resolvedProjectId = project_id ?? requestContext?.defaultProjectId ?? envConfig?.defaultProjectId;
    
      if (!resolvedProjectId) {
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify({
                error: {
                  code: "MISSING_PROJECT_ID",
                  message: "project_id is required. Either provide it in the request or set TC_DEFAULT_PROJECT environment variable.",
                },
              }),
            },
          ],
        };
      }
    
      try {
        const client = getApiClient();
    
        const suiteNeedsLookup =
          isNonNumericString(suite) ||
          (filter?.suite !== undefined &&
            (Array.isArray(filter.suite?.filter)
              ? filter.suite.filter.some(isNonNumericString)
              : isNonNumericString(filter.suite?.filter)));
        const tagsNeedLookup =
          filter?.tags !== undefined &&
          (Array.isArray(filter.tags.filter)
            ? filter.tags.filter.some(isNonNumericString)
            : isNonNumericString(filter.tags.filter));
        const requirementsNeedLookup =
          filter?.requirements !== undefined &&
          (Array.isArray(filter.requirements.filter)
            ? filter.requirements.filter.some(isNonNumericString)
            : isNonNumericString(filter.requirements.filter));
    
        const customFieldNameKeys =
          filter && typeof filter === "object"
            ? Object.keys(filter).filter(
                (key) => !standardFilterKeys.has(key) && !key.startsWith("cf_")
              )
            : [];
        const customFieldOptionsNeedLookup =
          filter && typeof filter === "object"
            ? Object.entries(filter).some(([key, value]) => {
                if (standardFilterKeys.has(key)) {
                  return false;
                }
                if (!value || typeof value !== "object" || Array.isArray(value)) {
                  return false;
                }
                const filterType = (value as Record<string, unknown>)["filterType"];
                const filterValue = (value as Record<string, unknown>)["filter"];
                if (filterType !== "text" || typeof filterValue !== "string") {
                  return false;
                }
                return isNonNumericString(filterValue);
              })
            : false;
        const customFieldsNeedLookup = customFieldNameKeys.length > 0;
    
        const needsLookup =
          suiteNeedsLookup ||
          tagsNeedLookup ||
          requirementsNeedLookup ||
          customFieldsNeedLookup ||
          customFieldOptionsNeedLookup;
        const cachedContext = needsLookup
          ? getCachedProjectContext(resolvedProjectId)
          : null;
        const cachedSuites = suiteNeedsLookup
          ? flattenSuiteTree(cachedContext?.suites)
          : null;
        const cachedTags = tagsNeedLookup ? cachedContext?.tags ?? null : null;
        const cachedRequirements = requirementsNeedLookup
          ? cachedContext?.requirements ?? null
          : null;
        const cachedCustomFields =
          customFieldsNeedLookup || customFieldOptionsNeedLookup
            ? cachedContext?.custom_fields ?? null
            : null;
    
        if (
          cachedSuites ||
          cachedTags ||
          cachedRequirements ||
          cachedCustomFields
        ) {
          console.log(
            `${logPrefix} Using cached project context for list_test_cases lookups (project ${resolvedProjectId})`
          );
        }
    
        const [suitesListResponse, projectForCompany] = await Promise.all([
          suiteNeedsLookup && !cachedSuites
            ? client.listSuites(resolvedProjectId)
            : Promise.resolve(null),
          (customFieldsNeedLookup || customFieldOptionsNeedLookup) &&
          !cachedCustomFields
            ? client.getProject(resolvedProjectId)
            : Promise.resolve(null),
        ]);
    
        const companyId = projectForCompany
          ? getCompanyIdFromProject(projectForCompany)
          : undefined;
    
        const [tagsListResponse, requirementsListResponse, customFieldsListResponse] =
          await Promise.all([
            tagsNeedLookup && !cachedTags
              ? client.listTags(resolvedProjectId)
              : Promise.resolve(null),
            requirementsNeedLookup && !cachedRequirements
              ? client.listRequirements(resolvedProjectId)
              : Promise.resolve(null),
            (customFieldsNeedLookup || customFieldOptionsNeedLookup) &&
            !cachedCustomFields
              ? client.listProjectCustomFields(resolvedProjectId, companyId)
              : Promise.resolve(null),
          ]);
    
        const suitesList = cachedSuites ?? suitesListResponse;
        const tagsList = cachedTags ?? tagsListResponse;
        const requirementsList = cachedRequirements ?? requirementsListResponse;
        const customFieldsList = cachedCustomFields ?? customFieldsListResponse;
    
        let resolvedSuiteId = toNumberId(suite);
        if (isNonNumericString(suite) && suitesList) {
          const match = suitesList.find(
            (suiteItem) => getField<string>(suiteItem, "title") === suite
          );
          resolvedSuiteId = toNumberId(match ? getField(match, "id") : undefined);
          if (resolvedSuiteId === undefined) {
            return {
              content: [
                {
                  type: "text",
                  text: JSON.stringify({
                    error: {
                      code: "SUITE_NOT_FOUND",
                      message: `Suite not found with title "${suite}" in that project`,
                    },
                  }),
                },
              ],
            };
          }
        }
    
        const resolvedFilter: Record<string, any> | undefined = filter
          ? { ...filter }
          : undefined;
    
        const suiteFilter = resolvedFilter?.suite as {
          filter?: unknown;
          filterType?: unknown;
          type?: unknown;
        } | undefined;
        if (suiteFilter && suiteFilter.filter !== undefined) {
          const rawValues = toArray(suiteFilter.filter);
          const shouldLookupByText =
            suiteFilter.filterType === "text" || rawValues.some(isNonNumericString);
    
          if (shouldLookupByText) {
            const match = resolveTextMatch(suiteFilter.type);
            if (!match) {
              return {
                content: [
                  {
                    type: "text",
                    text: JSON.stringify({
                      error: {
                        code: "UNSUPPORTED_FILTER",
                        message:
                          "Suite text filter type is not supported for lookups. Use equals/contains/startsWith/endsWith or notEqual/notContains.",
                      },
                    }),
                  },
                ],
              };
            }
            const { ids: resolvedIds, missing: missingSuites } = resolveLookupIds(
              rawValues,
              suitesList,
              match.match,
              (suite) => [getField<string>(suite, "title")]
            );
            if (missingSuites.length > 0) {
              return {
                content: [
                  {
                    type: "text",
                    text: JSON.stringify({
                      error: {
                        code: "SUITE_NOT_FOUND",
                        message: `Suite not found with title "${missingSuites.join(", ")}" in that project`,
                      },
                    }),
                  },
                ],
              };
            }
            if (resolvedIds.length > 0 && resolvedFilter) {
              resolvedFilter.suite = {
                ...suiteFilter,
                filterType: "number",
                type: match.negative ? "notEqual" : "equals",
                filter: resolvedIds.length === 1 ? resolvedIds[0] : resolvedIds,
              };
            }
          } else {
            const resolvedIds = rawValues
              .map((value) => toNumberId(value))
              .filter((id): id is number => typeof id === "number");
            if (resolvedIds.length > 0 && resolvedFilter) {
              resolvedFilter.suite = {
                ...suiteFilter,
                filterType: "number",
                filter: resolvedIds.length === 1 ? resolvedIds[0] : resolvedIds,
              };
            }
          }
        }
    
        if (resolvedFilter?.tags) {
          const tagsFilter = resolvedFilter.tags as {
            filter?: unknown;
            filterType?: unknown;
            type?: unknown;
          };
          if (tagsFilter.filter !== undefined) {
            const rawValues = toArray(tagsFilter.filter);
            const numericIds = rawValues
              .map((value) => toNumberId(value))
              .filter((id): id is number => typeof id === "number");
            const nameValues = rawValues.filter(isNonNumericString);
    
            let resolvedIds = [...numericIds];
            if (nameValues.length > 0) {
              const { ids: nameIds, missing: missingTags } = resolveLookupIds(
                nameValues,
                tagsList,
                "equals",
                (tag) => [getField<string>(tag, "name")]
              );
              if (missingTags.length > 0) {
                return {
                  content: [
                    {
                      type: "text",
                      text: JSON.stringify({
                        error: {
                          code: "TAG_NOT_FOUND",
                          message: `Tag(s) not found: ${missingTags.join(", ")}`,
                        },
                      }),
                    },
                  ],
                };
              }
              resolvedIds = resolvedIds.concat(nameIds);
            }
    
            if (resolvedIds.length > 0) {
              resolvedFilter.tags = {
                ...tagsFilter,
                filterType: "number",
                type: normalizeTagMatchType(tagsFilter.type),
                filter: resolvedIds,
              };
            }
          }
        }
    
        if (resolvedFilter?.requirements) {
          const requirementsFilter = resolvedFilter.requirements as {
            filter?: unknown;
            filterType?: unknown;
            type?: unknown;
          };
          if (requirementsFilter.filter !== undefined) {
            const rawValues = toArray(requirementsFilter.filter);
            const shouldLookupByText =
              requirementsFilter.filterType === "text" ||
              rawValues.some(isNonNumericString);
    
            if (shouldLookupByText) {
              const match = resolveTextMatch(requirementsFilter.type);
              if (!match) {
                return {
                  content: [
                    {
                      type: "text",
                      text: JSON.stringify({
                        error: {
                          code: "UNSUPPORTED_FILTER",
                          message:
                            "Requirement text filter type is not supported for lookups. Use equals/contains/startsWith/endsWith or notEqual/notContains.",
                        },
                      }),
                    },
                  ],
                };
              }
              const { ids: resolvedIds, missing: missingRequirements } =
                resolveLookupIds(
                  rawValues,
                  requirementsList,
                  match.match,
                  (req) => [
                    getField<string>(req, "requirement_key"),
                    getField<string>(req, "requirement_id"),
                    getField<string>(req, "title"),
                  ]
                );
              if (missingRequirements.length > 0) {
                return {
                  content: [
                    {
                      type: "text",
                      text: JSON.stringify({
                        error: {
                          code: "REQUIREMENT_NOT_FOUND",
                          message: `Requirement(s) not found: ${missingRequirements.join(", ")}`,
                        },
                      }),
                    },
                  ],
                };
              }
              resolvedFilter.requirements = {
                ...requirementsFilter,
                filterType: "number",
                type: match.negative ? "notContains" : "contains",
                filter: resolvedIds,
              };
            } else {
              const resolvedIds = rawValues
                .map((value) => toNumberId(value))
                .filter((id): id is number => typeof id === "number");
              resolvedFilter.requirements = {
                ...requirementsFilter,
                filterType: "number",
                type: normalizeTagMatchType(requirementsFilter.type),
                filter: resolvedIds,
              };
            }
          }
        }
    
        if (customFieldsNeedLookup && resolvedFilter && customFieldsList) {
          const resolvedFilterRecord = resolvedFilter as Record<string, unknown>;
          const customFieldNameMap = customFieldsList.reduce((map, cf) => {
            const name = getField<string>(cf, "name");
            const id = toNumberId(getField(cf, "id"));
            if (!name || id === undefined) {
              return map;
            }
            map.set(name, id);
            return map;
          }, new Map<string, number>());
    
          const missingCustomFields: string[] = [];
          customFieldNameKeys.forEach((key) => {
            const customFieldId = customFieldNameMap.get(key);
            if (customFieldId === undefined) {
              missingCustomFields.push(key);
              return;
            }
            const value = resolvedFilterRecord[key];
            delete resolvedFilterRecord[key];
            resolvedFilterRecord[`cf_${customFieldId}`] = value;
          });
          if (missingCustomFields.length > 0) {
            return {
              content: [
                {
                  type: "text",
                  text: JSON.stringify({
                    error: {
                      code: "CUSTOM_FIELD_NOT_FOUND",
                      message: `Custom field(s) not found: ${missingCustomFields.join(", ")}`,
                    },
                  }),
                },
              ],
            };
          }
        }
    
        if (customFieldOptionsNeedLookup && resolvedFilter && customFieldsList) {
          const resolvedFilterRecord = resolvedFilter as Record<string, unknown>;
          const missingCustomFieldOptions: string[] = [];
          const ambiguousCustomFieldOptions: string[] = [];
    
          Object.entries(resolvedFilterRecord)
            .filter(([key]) => key.startsWith("cf_"))
            .forEach(([key, value]) => {
              if (!value || typeof value !== "object" || Array.isArray(value)) {
                return;
              }
              const filterType = (value as Record<string, unknown>)["filterType"];
              const filterValue = (value as Record<string, unknown>)["filter"];
              if (filterType !== "text" || typeof filterValue !== "string") {
                return;
              }
              if (!isNonNumericString(filterValue)) {
                return;
              }
              const fieldId = getCustomFieldIdFromKey(key);
              if (!fieldId) {
                return;
              }
              const field = customFieldsList.find(
                (customField) => toNumberId(getField(customField, "id")) === fieldId
              );
              if (!field) {
                return;
              }
              const fieldType =
                getField<string>(field, "field_type") ??
                getField<string>(field, "type");
              if (!isDropdownFieldType(fieldType)) {
                return;
              }
              const options = getCustomFieldOptions(field);
              if (!options) {
                return;
              }
              const lookups = buildOptionLookup(options);
              if (!lookups.length) {
                return;
              }
              const optionIds = new Set(lookups.map((lookup) => lookup.id));
              if (optionIds.has(filterValue.trim())) {
                return;
              }
              const match = resolveTextMatch(
                (value as Record<string, unknown>)["type"]
              );
              if (!match) {
                return;
              }
              const matches = lookups.filter((lookup) =>
                matchTextValue(lookup.label, filterValue, match.match)
              );
              if (matches.length === 1) {
                resolvedFilterRecord[key] = {
                  ...value,
                  filter: matches[0].id,
                };
                return;
              }
              const displayName = getCustomFieldDisplayName(field, key);
              if (matches.length === 0) {
                missingCustomFieldOptions.push(
                  `${displayName}=${filterValue}`
                );
                return;
              }
              ambiguousCustomFieldOptions.push(
                `${displayName}=${filterValue}`
              );
            });
    
          if (
            missingCustomFieldOptions.length > 0 ||
            ambiguousCustomFieldOptions.length > 0
          ) {
            const details: string[] = [];
            if (missingCustomFieldOptions.length > 0) {
              details.push(
                `Missing: ${missingCustomFieldOptions.join(", ")}`
              );
            }
            if (ambiguousCustomFieldOptions.length > 0) {
              details.push(
                `Ambiguous: ${ambiguousCustomFieldOptions.join(", ")} (use exact label)`
              );
            }
            return {
              content: [
                {
                  type: "text",
                  text: JSON.stringify({
                    error: {
                      code: "CUSTOM_FIELD_OPTION_NOT_FOUND",
                      message: `Custom field option lookup failed. ${details.join(
                        " "
                      )}`,
                    },
                  }),
                },
              ],
            };
          }
        }
    
        const result = await client.listTestCases({
          projectId: resolvedProjectId,
          suiteId: resolvedSuiteId,
          filter: resolvedFilter as TestCaseFilter | undefined,
          sort: sort,
          limit: limit,
          offset: offset,
        });
    
        // Priority labels
        const priorityLabels: Record<number, string> = {
          0: "Low",
          1: "Normal",
          2: "High",
        };
    
        // Transform rows to include human-readable labels
        const humanizedRows = result.rows.map((tc) => ({
          id: tc.id,
          title: tc.title,
          description: tc.description,
          priority: tc.priority,
          priorityLabel: priorityLabels[tc.priority] ?? "Unknown",
          suite: typeof tc.suite === "object" ? tc.suite?.id : tc.suite,
          suiteTitle: typeof tc.suite === "object" ? tc.suite?.title : tc.suite_title,
          project: typeof tc.project === "object" ? tc.project?.id : tc.project,
          projectTitle: typeof tc.project === "object" ? tc.project?.title : undefined,
          tags: tc.tags?.map((t) => ({ id: t.id, name: t.name })),
          createdBy: tc.created_by?.name,
          createdAt: tc.created_at,
          updatedAt: tc.updated_at,
          isAutomated: tc.is_automated === 1 || tc.is_automated === true,
          automationStatus: tc.automation_status,
          runCount: tc.run_count,
          lastRunOn: tc.last_run_on,
          steps: tc.steps ?? tc.stepsParsed,
        }));
    
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify({
                testCases: humanizedRows,
                totalCount: result.totalCount,
                filteredCount: result.filteredCount,
                returned: humanizedRows.length,
              }, null, 2),
            },
          ],
        };
      } catch (error) {
        const message = error instanceof Error ? error.message : "Unknown error";
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify({
                error: {
                  code: "API_ERROR",
                  message: message,
                },
              }),
            },
          ],
        };
      }
    }
  • The Zod schema used to validate and preprocess input for the list_test_cases tool.
    export const listTestCasesSchema = z.preprocess(
      normalizeListTestCasesInput,
      z.object({
        project_id: z.number().optional().describe("Project ID (uses TC_DEFAULT_PROJECT env var if not specified)"),
        suite: z
          .union([z.number(), z.string()])
          .optional()
          .describe("Filter by suite ID or suite title"),
        filter: testCaseFilterSchema.optional().describe("Filter conditions object"),
        sort: z
          .array(sortModelSchema)
          .optional()
          .describe("Sort specification array"),
        limit: z
          .number()
          .min(1)
          .max(100)
          .default(50)
          .describe("Maximum results to return (1-100, default: 50)"),
        offset: z
          .number()
          .min(0)
          .default(0)
          .describe("Number of results to skip (default: 0)"),
      })
    );
  • The MCP tool definition for list_test_cases, including its name, description, and input schema.
    export const listTestCasesTool = {
      name: "list_test_cases",
      description: `List test cases from a TestCollab project with optional filtering, sorting, and pagination.
    
    Before calling this function, make sure project context is available.
    Note: list_test_cases may omit full step details; use get_test_case for a complete test case with steps.
    
    Filter fields include:
    - id, title, description, steps, priority (0=Low, 1=Normal, 2=High)
    - suite (ID), created_by, reviewer, poster (user IDs)
    - created_at, updated_at, last_run_on (dates)
    - tags, requirements (arrays of IDs or names)
    - under_review, is_automated (0 or 1)
    - run_count, avg_execution_time, failure_rate
    
    Filter types:
    - text: equals, notEqual, contains, notContains, startsWith, endsWith, isBlank
    - number: equals, notEqual, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, inRange
    - date: equals, notEqual, greaterThan, lessThan, inRange`,
    
      inputSchema: {
        type: "object" as const,
        properties: {
          project_id: {
            type: "number",
            description: "Project ID (optional if TC_DEFAULT_PROJECT env var is set)",
          },
          suite: {
            oneOf: [{ type: "number" }, { type: "string" }],
            description:
              "Filter by suite ID or suite title. If suite title is provided, map to ID from project context.",
          },
          filter: {
            type: "object",
            description:
              "Filter conditions. Each key is a field name with a filter object containing filterType, type, and filter value.",
            additionalProperties: true,
          },
          sort: {
            type: "array",
            description: "Sort specification",
            items: {
              type: "object",
              properties: {
                colId: { type: "string", description: "Field name to sort by" },
                sort: { type: "string", enum: ["asc", "desc"] },
              },
              required: ["colId", "sort"],
            },
          },
          limit: {
            type: "number",
            description: "Maximum results to return (1-100, default: 50)",
            default: 50,
            minimum: 1,
            maximum: 100,
          },
          offset: {
            type: "number",
            description: "Number of results to skip (default: 0)",
            default: 0,
            minimum: 0,
          },
        },
        required: [],
      },
    };
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full disclosure burden. It successfully notes that step details may be omitted, but fails to declare read-only safety, rate limits, or pagination mechanics. It also does not describe the return structure despite having no output schema.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is lengthy but well-structured with clear sections: purpose, tip, note, filter fields, filter types, and example. Given the complexity of the nested filter object, the verbosity is justified, though the filter field list could potentially be more compact.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Input documentation is comprehensive with examples, but the lack of an output schema creates a gap that the description fails to fill. It does not describe the returned test case structure, fields, or pagination metadata, leaving the agent uncertain about the response format.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

While the schema has 100% description coverage (baseline 3), the description adds substantial value by enumerating all 15+ filterable fields with their data types, listing filter operations (equals, contains, etc.) by type, and providing a concrete JSON example. This significantly aids constructing valid filter objects.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening sentence clearly states the specific action (List), resource (test cases), and scope (from a TestCollab project) with filtering/sorting/pagination capabilities. It distinguishes from sibling get_test_case by noting this returns a list while the sibling returns complete details.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit prerequisite guidance ('Call get_project_context first to resolve suite/tag/custom field names to IDs') and clear alternative selection criteria ('use get_test_case for a complete test case with steps'). This gives the agent precise workflow context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TCSoftInc/testcollab-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server