Skip to main content
Glama
soil-dev

capsulemcp

filter_parties

Filter your Capsule parties using structured conditions like date ranges, tags, or custom fields. Retrieve newest records by date filter and highest ID.

Instructions

Filter parties by structured conditions (date ranges, tags, fields). Use this — not search_parties — for questions like 'most recent client', 'parties added this week', 'parties tagged VIP'. Capsule's API does not support ad-hoc sort, but for 'most recent X' you can filter by a date field (e.g. {field: 'addedOn', operator: 'is within last', value: 30}) and pick the highest-id row from the result — Capsule IDs are monotonic, so newest id = newest record.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
conditionsYesArray of filter conditions. All conditions are ANDed together. To get newest records, use a date condition like {field: 'addedOn', operator: 'is within last', value: 7} and pick the highest-id row from the result (Capsule IDs are monotonic).
embedNoComma-separated embeds, e.g. 'tags,fields'
pageNo
perPageNo

Implementation Reference

  • The handler function for filter_parties. Calls runFilter with entity path 'parties' and returns paginated results.
    export const filterPartiesSchema = FilterInputSchema;
    export async function filterParties(input: FilterInput) {
      // Common patterns:
      //   "Parties contacted in last N days":
      //     [{field: "lastContactedOn", operator: "is within last", value: N}]
      //   "Parties added in last N days":
      //     [{field: "addedOn", operator: "is within last", value: N}]
      //   "Organisations only":
      //     [{field: "type", operator: "is", value: "organisation"}]
      //   "Tagged X" (by name or id):
      //     [{field: "tag", operator: "is", value: "VIP"}]
      //   "Has at least one tag" (filter out untagged auto-imports):
      //     [{field: "hasTags", operator: "is", value: true}]
      return runFilter<{ parties: unknown[]; nextPage: number | undefined }>("parties", input);
    }
  • Input schema (FilterInputSchema) shared by all filter tools including filter_parties. Defines 'conditions', 'embed', 'page', 'perPage' with Zod validation.
    const FilterConditionSchema = z.object({
      field: z
        .string()
        .describe(
          "The Capsule filter-side field name (these differ from response field names — e.g. response.createdAt is filter-side 'addedOn', response.lastContactedAt is filter-side 'lastContactedOn'). Common: 'addedOn' (date created), 'updatedOn' (date last modified), 'lastContactedOn' (parties only), 'name', 'tag', 'owner', 'team', 'type' (parties: person|organisation), 'milestone' (opportunities), 'status' (opp/project: OPEN|CLOSED), 'closedOn' (opp/project), 'expectedCloseOn' (opp/project), 'hasTags', 'hasEmailAddress' (parties), 'isOpen', 'isStale' (opportunities), 'custom:{fieldId}'. Full per-entity list: https://developer.capsulecrm.com/v2/reference/filters",
        ),
      operator: z
        .string()
        .describe(
          "The filter operator. Common: 'is', 'is not' (use value=null to test for null), 'contains', 'does not contain', 'is greater than', 'is less than', 'is within last' (date fields, value=integer days), 'is more than' (date fields, value=integer days ago), 'starts with', 'ends with'. Operator validity depends on the field's type.",
        ),
      value: z
        .union([z.string(), z.number(), z.boolean(), z.null()])
        .describe(
          "The value to compare against. For 'is within last' on date fields, pass an integer number of days. For tag filters, pass the tag name (string) or tag id (number). For 'is not' null tests, pass null literally.",
        ),
    });
    
    const FilterInputSchema = z.object({
      conditions: z
        .array(FilterConditionSchema)
        .min(1)
        .describe(
          "Array of filter conditions. All conditions are ANDed together. To get newest records, use a date condition like {field: 'addedOn', operator: 'is within last', value: 7} and pick the highest-id row from the result (Capsule IDs are monotonic).",
        ),
      embed: z.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
      page: z.number().int().positive().optional().default(1),
      perPage: z.number().int().min(1).max(100).optional().default(25),
    });
  • Generic runFilter helper that POSTs filter conditions to Capsule's /<entity>/filters/results endpoint via capsuleSearch and returns paginated data.
    async function runFilter<T>(entityPath: string, input: FilterInput): Promise<T> {
      const { data, nextPage } = await capsuleSearch<T & object>(
        `/${entityPath}/filters/results`,
        { filter: { conditions: input.conditions } },
        {
          page: input.page,
          perPage: input.perPage,
          embed: input.embed,
        },
      );
      return { ...data, nextPage } as T;
    }
  • src/server.ts:237-243 (registration)
    Registration of the 'filter_parties' MCP tool in createCapsuleMcpServer(). Binds the tool name, description, schema, and handler via registerTool().
    registerTool(
      server,
      "filter_parties",
      "Filter parties by structured conditions (date ranges, tags, fields). Use this — not search_parties — for questions like 'most recent client', 'parties added this week', 'parties tagged VIP'. Capsule's API does not support ad-hoc sort, but for 'most recent X' you can filter by a date field (e.g. {field: 'addedOn', operator: 'is within last', value: 30}) and pick the highest-id row from the result — Capsule IDs are monotonic, so newest id = newest record.",
      filterPartiesSchema,
      filterParties,
    );
  • capsuleSearch helper - used by runFilter to POST filter conditions to Capsule's API. Handles auth, retries on 429, and parses pagination from Link headers.
    export async function capsuleSearch<T>(
      path: string,
      body: unknown,
      params?: QueryParams,
    ): Promise<PagedResult<T>> {
      const token = getToken();
      const url = buildUrl(path, params);
      const { res, cleanup } = await doFetch(url, {
        method: "POST",
        headers: { ...baseHeaders(token), "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      try {
        const data = await handleResponse<T>(res);
        const nextPage = parseNextPage(res.headers.get("Link"));
        return { data, nextPage };
      } finally {
        cleanup();
      }
    }
Behavior5/5

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

No annotations present, so description carries full burden. Discloses that Capsule API does not support ad-hoc sort, explains the monotonic ID workaround, and notes field name differences (response vs filter-side). Also links to full filter field reference. This is exceptional transparency for a filtering tool.

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

Conciseness5/5

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

Three tightly crafted sentences. First sentence states purpose and scope, second gives usage directive, third explains workaround. No filler, every sentence adds unique value. Front-loaded with key information.

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

Completeness4/5

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

Covers filtering mechanics, workaround for sorting, and parameter semantics. No output schema exists, but description focuses on input behavior which is appropriate. Could optionally mention pagination defaults (already in schema) or response structure, but the provided info is sufficient for correct invocation.

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

Parameters4/5

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

Schema coverage is 50%, but description adds substantial value beyond schema: explains filter-side field naming conventions, provides operator examples, describes value types (including null for 'is not'), and gives practical tips (e.g., using 'is within last'). The monotonic ID trick is an extra semantic bonus.

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?

Clearly states the tool filters parties by structured conditions (date ranges, tags, fields). Differentiates from search_parties with specific examples like 'most recent client', 'parties added this week', 'parties tagged VIP'. The monotonic ID workaround for sorting adds further specificity.

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?

Explicitly instructs 'Use this — not search_parties — for questions like...' and provides a concrete workaround for unsupported ad-hoc sort. Clearly sets expectations on when to use versus alternatives.

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/soil-dev/capsulemcp'

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