Skip to content

Expose project tag membership and filtering in list_projects #130

Description

@deverman

User outcome

Let an MCP-compatible assistant find and classify projects by the tags assigned
directly to those projects without loading the entire project catalog or
confusing identically named tags in different tag hierarchies.

Example requests:

  • Which active projects are tagged Waiting?
  • Show me the projects associated with People / Richard.
  • Which of these projects have no direct project tags?

Motivation and contributor evidence

The project-tag return-field idea was independently implemented in the
Lumenbeing/FocusRelayMCP fork:

  • 70692ae
    added project tag names and IDs in the Omni Automation bridge.
  • b0d3715
    carried those values through the Swift models and output layer.

Those commits demonstrate the user need and the documented project.tags API,
but they are design evidence rather than code to import: the fork is based on an
older architecture, combines tags with folder membership, and does not include
tests or server-side filtering. Thanks to @Lumenbeing / @Defiantweb for surfacing
the workflow.

Validation impact and dependencies

User-facing acceptance journey

  1. The user asks for projects carrying a named or nested tag.
  2. The assistant calls Add parent-aware tag search and hierarchy to list_tags #70's list_tags search and obtains the intended stable
    tag ID and full path.
  3. The assistant calls list_projects with that stable tag ID, the appropriate
    status view, and compact requested fields.
  4. FocusRelay returns only matching projects and their directly assigned project
    tags.
  5. If multiple tag paths share a name, the assistant asks the user to choose
    before querying projects; FocusRelay never guesses by name.

Public query contract

Extend list_projects with requestable project fields:

  • tagIDs — stable IDs of tags assigned directly to the project;
  • tagNames — corresponding direct tag names in the same deterministic order;
  • tagPaths — when requested, one entry per direct project tag containing the
    tag's id, name, and an ordered root-to-tag path of {id,name} elements.

Add filters:

  • tagIDs — a non-empty array of stable tag IDs; a project matches when any
    requested ID is assigned directly to it;
  • untaggedOnly — when true, return projects with no directly assigned project
    tags.

Rules:

  • Reject tagIDs combined with untaggedOnly=true.
  • Reject empty tagIDs; use untaggedOnly=true explicitly.
  • Resolve every supplied tag ID before scanning projects and fail with the
    missing IDs instead of silently returning an empty result.
  • Filter only direct project.tags. Do not infer project membership from child
    task tags and do not describe child-task tags as project tags.
  • Apply tag and status filtering before pagination and before total-count
    calculation.
  • Preserve deterministic OmniFocus project order unless Add configurable sorting for task and project queries #62 later requests a
    supported sort.
  • Preserve compact defaults: no tag fields are read or returned unless requested
    or required by a tag filter.
  • Use repository field naming (tagIDs, not tagIds).
  • Unknown project field names or malformed filters fail clearly rather than
    disappearing silently.

Architecture and API constraints

  • Use only the documented project.tags, tag.parent, and stable
    id.primaryKey APIs.
  • Build ID-bearing paths so duplicate tag names remain distinguishable.
  • Keep the Bridge plug-in as the only production automation path.
  • Carry optional values through bridge payload, core model, and selected output
    without converting a missing requested wire field into a trustworthy empty
    array. A plugin/schema mismatch must produce an error or explicit warning.
  • Add new filter values to project cache keys, or deliberately bypass the cache
    with documented evidence. Results for different tag filters must never share a
    cached page.
  • Do not add tag assignment, creation, hierarchy mutation, transport changes, or
    unrelated query optimization in this issue.

Performance and context expectations

  • The stable-ID filter should prevent the model from retrieving every project
    merely to classify project tags.
  • Requesting only id, name, status, and direct tag fields should remain
    materially smaller than an unfiltered full project catalog.
  • Record matching count, returned count, serialized response bytes, bridge time,
    end-to-end latency, and model calls for a representative large project/tag
    library.
  • Measurements guide tradeoffs; no isolated byte target overrides correct tag
    identity or model routing.

Test plan

Use Swift Testing, deterministic JavaScriptCore contracts, and direct MCP/CLI
coverage for:

  • no tags, one tag, and multiple direct project tags;
  • direct project tags versus tags present only on child tasks;
  • tagIDs and tagNames order and association;
  • root and nested tag paths;
  • identical tag names under different parents;
  • any-of filtering with one and multiple stable IDs;
  • untaggedOnly behavior;
  • missing, dropped, and malformed tag IDs;
  • contradictory and empty filter validation;
  • active/onHold/dropped/done/all status composition;
  • filtering and total counts before pagination;
  • cursor continuity;
  • requested-field omission preserving compact output;
  • unsupported project fields failing clearly;
  • stale/mismatched plugin payload behavior;
  • cache separation for every new filter value;
  • CLI/MCP output parity;
  • a live Bridge read against projects with direct and child-only tags.

Add a controlled Kimi K2.7 Code and comparison-model journey. Each model must
use #70 to resolve an ambiguous tag name, query by stable ID, avoid retrieving
the full project catalog, and accurately distinguish direct project tags from
child-task tags.

Acceptance criteria

  • A user can retrieve projects carrying an unambiguous stable tag ID.
  • A user can retrieve projects with no directly assigned project tags.
  • Direct project tags are returned with stable IDs, names, and requestable
    ID-bearing paths.
  • Duplicate names remain distinguishable and never cause name-based guessing.
  • Tag and status filters compose before pagination and counting.
  • Default list_projects responses remain compact.
  • Unknown fields, invalid filters, and missing requested wire data fail clearly.
  • Cache, CLI, MCP, JavaScriptCore, live query, and model-routing tests pass.
  • The implementation remains separate from Expose project folder membership and root filtering in list_projects #88 folder membership and Create missing root or nested OmniFocus tags while assigning them #128 tag
    creation/assignment.

Release planning

Implement after #70 and before #128 so project-tag discovery and readback are
stable before create-and-assign mutations use them.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions