Skip to content

Make MCP task authoring reliable - #176

Merged
mikepsinn merged 3 commits into
mainfrom
feature/mcp-task-reference-contract
Aug 1, 2026
Merged

Make MCP task authoring reliable#176
mikepsinn merged 3 commits into
mainfrom
feature/mcp-task-reference-contract

Conversation

@mikepsinn

@mikepsinn mikepsinn commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Goal

Make MCP task authoring reliable enough that an agent can create a useful task graph in one pass, save the returned references, enumerate the whole queue, and complete work through the correct workflow without guessing generated IDs or status fields.

What changed

  • add backward-compatible opt-in cursor pagination to listTasks and searchTasks, with bounded result windows and explicit errors instead of silent omission
  • accept exact task IDs or stable taskKey values across task creation, updates, dependencies, and bundles; reject ambiguous or conflicting references
  • return both id and taskId from createTask, and return a referenceMap from bundle proposals so follow-up calls can reuse persisted IDs
  • make proposal bundles atomic across task rows, relationships, communication endpoints, impact estimates, and source provenance
  • enforce personal/organization OAuth boundaries for every proposed target and persist organization ownership correctly
  • reject child/blocker candidates whose required same-bundle relation was not promoted, instead of silently creating a broken graph
  • document the decision table for completeTask, completeTaskClaim, and formal execution/verification flows
  • repair the personal task-engine smoke script so it uses exact references, pagination, parent metadata, and the correct completion operation

Compatibility and scope

  • legacy unpaginated listTasks and searchTasks response arrays remain unchanged until callers opt in with paginated: true or a cursor
  • legacy ID field names remain accepted while canonical *Ref names are documented
  • no Prisma schema changes
  • no exported @optimitron/db type changes
  • no user-interface changes
  • five files changed

Validation

  • pnpm --filter @optimitron/web exec vitest run src/lib/__tests__/mcp-server.test.ts --maxWorkers=1 --minWorkers=1 (247/247)
  • pnpm --filter @optimitron/web run typecheck:fast
  • standalone TypeScript check for scripts/mcp-personal-task-engine-smoke.ts
  • git diff --check
  • Prettier check for all changed MCP TypeScript files
  • repository pre-commit ESLint hook

The broad local parallel Vitest run exposed a pre-existing collision between task-funding test cleanup prefixes; the untouched affected test files pass serially. CI remains the clean full-suite check for this PR.

Summary by CodeRabbit

  • New Features
    • Added paginated task listing and search with cursor support.
    • Tasks can now be referenced using stable IDs, task keys, or proposal aliases.
    • Added improved dependency management, including validation and cycle prevention.
    • Added transactional proposal bundles with reference maps and promotion workflows.
  • Bug Fixes
    • Improved handling of ambiguous, inaccessible, invalid, and expired task references.
    • Preserved compatibility with legacy dependency fields and response formats.
  • Documentation
    • Expanded guidance for pagination, task references, dependency workflows, authentication, and error recovery.

Copilot AI review requested due to automatic review settings August 1, 2026 20:10
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
optimitron-web Ready Ready Preview Aug 1, 2026 8:34pm

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@mikepsinn, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6918a368-c262-4542-91d5-a1a44ffb2b45

📥 Commits

Reviewing files that changed from the base of the PR and between eb88756 and 906ceb3.

📒 Files selected for processing (5)
  • docs/MCP_SERVER.md
  • packages/web/scripts/mcp-personal-task-engine-smoke.ts
  • packages/web/src/lib/__tests__/mcp-server.test.ts
  • packages/web/src/lib/mcp-instructions.ts
  • packages/web/src/lib/mcp-server.ts
📝 Walkthrough

Walkthrough

Changes

MCP task workflows

Layer / File(s) Summary
Cursor pagination and bounded task discovery
packages/web/src/lib/mcp-server.ts, packages/web/src/lib/__tests__/mcp-server.test.ts
listTasks and searchTasks support opaque, query-bound cursors, bounded result windows, extended filters, and legacy responses.
Canonical task references and dependency edges
packages/web/src/lib/mcp-server.ts, packages/web/src/lib/__tests__/mcp-server.test.ts
Task APIs resolve IDs and task keys, reject ambiguous or inaccessible references, and distinguish created, reactivated, and updated dependency edges.
Reference-mapped transactional proposal promotion
packages/web/src/lib/mcp-server.ts, packages/web/src/lib/__tests__/mcp-server.test.ts
Proposal bundles normalize aliases, enforce visibility and dependency rules, return reference maps, and persist accepted records transactionally.
Client workflow, smoke coverage, and documentation
packages/web/scripts/mcp-personal-task-engine-smoke.ts, packages/web/src/lib/mcp-instructions.ts, docs/MCP_SERVER.md
The smoke script and MCP guidance use personal roots, stable references, pagination, canonical dependencies, and separate review and promotion workflows.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MCPClient
  participant ProposalBundleWorkflow
  participant PrismaTransaction
  participant TaskRecords
  MCPClient->>ProposalBundleWorkflow: submit reviewed candidate references
  ProposalBundleWorkflow->>ProposalBundleWorkflow: validate aliases, visibility, and dependency closure
  ProposalBundleWorkflow->>PrismaTransaction: persist accepted tasks and related records
  PrismaTransaction->>TaskRecords: create tasks, parents, blockers, impacts, and endpoints
  TaskRecords-->>MCPClient: return reference map and persisted task IDs
Loading

Possibly related PRs

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the pull request's primary goal of improving reliability for MCP task authoring.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/mcp-task-reference-contract

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eb88756ae2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/web/src/lib/mcp-server.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the MCP task-authoring contract in @optimitron/web so agents can reliably (a) enumerate task inventories without silent truncation, (b) reference tasks by stable IDs/taskKeys (plus bundle-local refs), and (c) persist complete proposal bundles atomically with correct OAuth boundary enforcement.

Changes:

  • Added opt-in cursor pagination to listTasks and searchTasks, including bounded authorized windows with explicit RESULT_WINDOW_EXCEEDED errors.
  • Expanded task reference handling across create/update/dependency/bundle flows to accept exact task IDs or exact taskKey values, plus bundle-local ref aliases and a returned referenceMap.
  • Made proposal bundle persistence atomic (tasks + edges + endpoints + impact + provenance) and tightened OAuth boundary checks; updated docs, tests, and the personal task-engine smoke script accordingly.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/web/src/lib/mcp-server.ts Implements pagination cursors/windows, exact-reference resolution, bundle alias/referenceMap support, transactional bundle writes, and stricter boundary validation.
packages/web/src/lib/mcp-instructions.ts Updates agent-facing operational guidance for pagination, references, bundles, and completion flows.
packages/web/src/lib/tests/mcp-server.test.ts Adds coverage for pagination behavior, reference resolution, bundle atomicity/alias rules, and dependency updates/outcomes.
packages/web/scripts/mcp-personal-task-engine-smoke.ts Updates smoke script to use personal roots, exact references, completion operations, and paginated listing.
docs/MCP_SERVER.md Documents pagination envelopes, bounded windows, reference rules, and the bundle/referenceMap workflow.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/web/src/lib/mcp-server.ts
Comment thread packages/web/scripts/mcp-personal-task-engine-smoke.ts Outdated
Comment thread packages/web/scripts/mcp-personal-task-engine-smoke.ts Outdated
Comment thread docs/MCP_SERVER.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/web/src/lib/mcp-server.ts (1)

12658-12712: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Bound the existing-task lookup for proposeTaskBundle.

prisma.task.findMany has no take and no reference filter at packages/web/src/lib/mcp-server.ts:12658. Admin callers can read each non-admin branch owner's tasks without the non-admin OR, and the select loads live sourceArtifacts for every result. Collect the candidate and dependency aliases first, then filter this query on those aliases and add a bound before comparing against existingTasks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/lib/mcp-server.ts` around lines 12658 - 12712, Update the
existing-task lookup inside proposeTaskBundle to collect the candidate and
dependency aliases before querying, then constrain prisma.task.findMany to those
aliases for every caller, including admins. Add the appropriate result bound and
avoid loading unrelated live sourceArtifacts; preserve the subsequent
existingTasks comparison using the bounded, alias-filtered results.
🧹 Nitpick comments (3)
packages/web/src/lib/mcp-server.ts (2)

14789-14825: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Run the edge probe and both writes in one transaction.

taskEdge.findFirst, taskEdge.updateMany, and taskEdge.createMany execute as three independent statements. Two concurrent addDependency calls for the same pair can both read existingEdge == null and both report outcome: "created". skipDuplicates protects the row, so only the reported outcome is wrong. Wrapping the three statements in prisma.$transaction makes the reported outcome match the write that actually happened.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/lib/mcp-server.ts` around lines 14789 - 14825, Wrap the
taskEdge.findFirst probe, updateMany, and createMany operations in a single
prisma.$transaction within addDependency, and derive the returned
created/outcome values from the transaction-scoped existingEdge result. Preserve
the existing edge metadata, duplicate handling, and response shape while
ensuring concurrent calls report the outcome of the serialized write.

13066-13256: 🗄️ Data Integrity & Integration | 🔵 Trivial

Bound the work performed inside the bundle transaction.

The transaction now creates every accepted draft, then loops over each created draft to update the parent, create each blocker edge, upsert the communication endpoint, create the impact estimate, and upsert the source artifact. attachProposalImpactEstimate and attachProposalSourceArtifact also perform a dynamic await import(...) inside the transaction. The work is sequential and scales with the candidate count, so a large bundle holds row locks for the full timeout: 60_000 window and can abort after doing all the work.

Consider capping the accepted candidate count per bundle, and moving the dynamic imports to before prisma.$transaction so no module load happens while the transaction is open.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/lib/mcp-server.ts` around lines 13066 - 13256, The bundle
transaction performs unbounded sequential work and dynamically imports modules
while holding locks. In the flow surrounding prisma.$transaction, cap the
accepted/promotable candidate count per bundle using the established bundle
limit behavior, and resolve the modules required by attachProposalImpactEstimate
and attachProposalSourceArtifact before entering the transaction; reuse those
imports inside the transaction while preserving all existing task, dependency,
endpoint, impact, and artifact operations.
packages/web/src/lib/__tests__/mcp-server.test.ts (1)

4211-4242: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The transaction mock cannot prove rollback.

mocks.transaction invokes the callback with transactionClient and performs no rollback, so this test proves ordering only: the impact failure aborts before sourceArtifactUpsert runs. It does not prove that the created draft rows are discarded.

mocks.taskCreate is also shared between the base client and transactionClient, so a regression that moved task.create off the transaction client would still pass. Consider giving the transaction client a distinct task.create spy, or asserting the call order so the test fails if draft creation leaves the transaction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/lib/__tests__/mcp-server.test.ts` around lines 4211 - 4242,
Strengthen the “keeps proposal attachments inside the draft transaction” test by
making transactionClient.task.create distinct from the base client’s task.create
and asserting draft creation uses the transaction client, ideally with
call-order verification before createDirectTaskImpactInTransaction. Update the
transaction mock or test setup so a rollback-related regression that moves task
creation outside the transaction cannot still pass; preserve the existing
failure and attachment assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/MCP_SERVER.md`:
- Line 64: Update the guidance sentence near the blockerTaskRefs documentation
to recommend blockerTaskRefs instead of the legacy depends_on field, while
leaving the legacy-alias clarification unchanged.
- Around line 104-120: Label the first JSON block as the paginated request and
the second as the response in the listTasks/searchTasks documentation. Add
concise labels immediately above each code block, preserving both JSON examples
unchanged.

In `@packages/web/scripts/mcp-personal-task-engine-smoke.ts`:
- Around line 146-160: Update listAllTasks so it does not request an unbounded
public task window: narrow listTasks with a query-level filter such as
parentTaskId or status, or handle RESULT_WINDOW_EXCEEDED as a skip in the
before/after count assertion while preserving the existing pagination behavior.

In `@packages/web/src/lib/mcp-server.ts`:
- Around line 13173-13230: Update the blocker-edge creation loop to deduplicate
by resolved blockerTaskId rather than blockerRef. Track task IDs already wired
for the current task, skip references resolving to an existing ID, and preserve
the existing validation and tx.taskEdge.create behavior for the first
occurrence.

---

Outside diff comments:
In `@packages/web/src/lib/mcp-server.ts`:
- Around line 12658-12712: Update the existing-task lookup inside
proposeTaskBundle to collect the candidate and dependency aliases before
querying, then constrain prisma.task.findMany to those aliases for every caller,
including admins. Add the appropriate result bound and avoid loading unrelated
live sourceArtifacts; preserve the subsequent existingTasks comparison using the
bounded, alias-filtered results.

---

Nitpick comments:
In `@packages/web/src/lib/__tests__/mcp-server.test.ts`:
- Around line 4211-4242: Strengthen the “keeps proposal attachments inside the
draft transaction” test by making transactionClient.task.create distinct from
the base client’s task.create and asserting draft creation uses the transaction
client, ideally with call-order verification before
createDirectTaskImpactInTransaction. Update the transaction mock or test setup
so a rollback-related regression that moves task creation outside the
transaction cannot still pass; preserve the existing failure and attachment
assertions.

In `@packages/web/src/lib/mcp-server.ts`:
- Around line 14789-14825: Wrap the taskEdge.findFirst probe, updateMany, and
createMany operations in a single prisma.$transaction within addDependency, and
derive the returned created/outcome values from the transaction-scoped
existingEdge result. Preserve the existing edge metadata, duplicate handling,
and response shape while ensuring concurrent calls report the outcome of the
serialized write.
- Around line 13066-13256: The bundle transaction performs unbounded sequential
work and dynamically imports modules while holding locks. In the flow
surrounding prisma.$transaction, cap the accepted/promotable candidate count per
bundle using the established bundle limit behavior, and resolve the modules
required by attachProposalImpactEstimate and attachProposalSourceArtifact before
entering the transaction; reuse those imports inside the transaction while
preserving all existing task, dependency, endpoint, impact, and artifact
operations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e96246ac-04c2-438f-8b4c-f10f0fa0df98

📥 Commits

Reviewing files that changed from the base of the PR and between 18256f8 and eb88756.

📒 Files selected for processing (5)
  • docs/MCP_SERVER.md
  • packages/web/scripts/mcp-personal-task-engine-smoke.ts
  • packages/web/src/lib/__tests__/mcp-server.test.ts
  • packages/web/src/lib/mcp-instructions.ts
  • packages/web/src/lib/mcp-server.ts

Comment thread docs/MCP_SERVER.md
Comment thread docs/MCP_SERVER.md Outdated
Comment thread packages/web/scripts/mcp-personal-task-engine-smoke.ts Outdated
Comment thread packages/web/src/lib/mcp-server.ts
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

PR review packet

Start here

  • 🖼️ Visual review
  • 📏 Screenshot baseline: exact PR base main@18256f89305f from CI run 30715650072.
  • 🚀 Preview deployment
  • ☝️ Cmd/Ctrl-click review links to keep this PR open.
  • 🔑 ?login=demo signs in as the demo user; ?logout=1 clears the session.
  • 💬 For a visual problem, use the comment button in latest.html or reply here with @claude and the checklist item.

Review checklist

Changed files considered
  • docs/MCP_SERVER.md
  • packages/web/scripts/mcp-personal-task-engine-smoke.ts
  • packages/web/src/lib/__tests__/mcp-server.test.ts
  • packages/web/src/lib/mcp-instructions.ts
  • packages/web/src/lib/mcp-server.ts

Updated automatically when this PR's preview or visual review reruns.

@claude

claude Bot commented Aug 1, 2026

Copy link
Copy Markdown

Code review

One high-confidence bug found (bug agents disagreed on the exact mechanism; verified directly against the PR's actual code before reporting).

proposeTaskBundle closure check treats a candidate with no matching review decision as "not rejected," which can roll back an entire bundle transaction with the wrong error

impact: getProposalGovernanceImpact(c),
status: "DRAFT",
})),
existingTasks: existingTasks.map((t) => ({
id: t.id,
title: t.title,
taskKey: t.taskKey,
roleTitle: t.roleTitle,
assigneePersonId: t.assigneePersonId,
assigneeOrganizationId: t.assigneeOrganizationId,
status: t.status,
})),
});
const decisionByCandidate = new Map<
Record<string, unknown>,
(typeof review.decisions)[number]
>();
for (const decision of review.decisions) {
const candidate = newCandidates.find((item) =>
matchCandidateToDecision(item, decision),
);
if (candidate) decisionByCandidate.set(candidate, decision);
}
for (const [candidate, decision] of decisionByCandidate) {
if (!decision.promotable) continue;
const rejectedReference = dedupeStrings([
optionalString(candidate.parentTaskRef),
...asStringArray(candidate.blockerRefs),
]).find((ref) => {
const referencedCandidate = candidateByRef.get(ref);

The new closure guard is:

decisionByCandidate.get(referencedCandidate)?.promotable === false

decisionByCandidate is populated only for candidates matched by matchCandidateToDecision, which now compares raw (untrimmed) ref/id/taskKey strings against decision.proposalRef. proposalRef() in packages/agent/src/task-governance.ts trims (task.id?.trim()), but normalizeProposalCandidate in mcp-server.ts only normalizes refid/taskKey pass through raw from caller input, which has no minLength/trim validation in the tool's input schema.

Failure scenario: a bundle candidate P is sent with a whitespace-padded id (e.g. "x ") and taskKey: "y". The review step trims and returns proposalRef: "x", but matchCandidateToDecision compares "x " === "x" → false on all three checks, so P never lands in decisionByCandidate. A sibling candidate C references P via parentTaskRef: "y" (matched instead through the separately-built, untrimmed candidateByRef map, so pre-validation passes). The closure check then evaluates undefined === falsefalse, so it does not return the documented BUNDLE_REFERENCE_CLOSURE_VIOLATION and proceeds into prisma.$transaction. Because P was never matched to a decision, the creation loop skips it (if (!candidate) continue), and when C's parent wiring runs, the reference is unresolved, hitting the hard throw new Error('Accepted task references unavailable parent "y".') at mcp-server.ts:13158-13188inside the transaction, rolling back every draft in the bundle with a raw Error instead of the intended BUNDLE_REFERENCE_CLOSURE_VIOLATION.

Suggested fix (either, or both):

  • Trim candidate.ref/id/taskKey before comparison in matchCandidateToDecision, or normalize id/taskKey (not just ref) in normalizeProposalCandidate.
  • Harden the closure check itself so a missing decision is treated as not-promotable (!== true instead of === false), so this class of mismatch fails fast with BUNDLE_REFERENCE_CLOSURE_VIOLATION before the transaction starts, rather than as an uncaught throw mid-transaction.

No CLAUDE.md/AGENTS.md compliance issues found (two independent passes — Prisma-in-library-package rules don't apply since all changes are in packages/web; no schema/@optimitron/db type changes; branch/PR naming, testing rules, and copy-approval gate all check out for this dev-tooling change).

@claude

claude Bot commented Aug 1, 2026

Copy link
Copy Markdown

Code review

Reviewed for CLAUDE.md/AGENTS.md compliance (two independent passes) — no violations found; the PR stays within its stated scope.

Two confirmed bugs in packages/web/src/lib/mcp-server.ts, both independently verified against the PR head commit:

1. listTasks hard-fails legacy (non-paginated) calls that use extended filters

: null;
const needsCompleteAuthorizedWindow =
wantsPagination || needsExtendedFiltering;
const list = await tasks.listTasks({
status,
category,
clientAccessBoundary:
visibility === "public" ? undefined : taskClientBoundary,
assigneePersonId,
assigneeOrganizationId,
parentTaskId: parentTaskIdFilter,
// Pagination and post-query filters need a stable authorized
// window. Fetch one sentinel row past the advertised maximum so
// oversized queries fail instead of doing unbounded work or
// silently omitting matches.
limit: needsCompleteAuthorizedWindow ? 5001 : limit,
personId: visibility === "public" ? null : viewerPersonId,
userId: visibility === "public" ? null : userId,
visibility,
});
// parentTaskId is filtered in the Prisma query above; no in-memory pass.
const authorizedWindow = Array.isArray(list) ? list : [];
if (
needsCompleteAuthorizedWindow &&
authorizedWindow.length > 5000
) {
return err(
"This task listing exceeds the 5000-task result window. Narrow the query-level filters before using pagination or extended filters.",
{ code: "RESULT_WINDOW_EXCEEDED" },
);
}
let filtered = authorizedWindow;

needsCompleteAuthorizedWindow is wantsPagination || needsExtendedFiltering — not gated on wantsPagination alone — and the RESULT_WINDOW_EXCEEDED error (L9668-9676) is returned before the !wantsPagination legacy-response branch (L9719). So a plain call like listTasks({ executionMode: "AGENT_ONLY", limit: 5 }) (no paginated, no cursor) now returns a hard error with zero results whenever the caller's authorized task set exceeds 5000 rows. Before this PR the same call fetched a bounded window and returned best-effort matches. This contradicts the PR's own new doc text in docs/MCP_SERVER.md: "Calls with neither paginated: true nor cursor retain the legacy one-page array response."

It also makes the error message's remedy unactionable in this path: it says "Narrow the query-level filters," but executionMode (and requiredTags, compensationKind, engagementKind, remotePolicy, applicationPolicy, ownerOrganizationId) are applied in-memory after this check (L9678+), not pushed into the Prisma where clause — there is no query-level filter to narrow.

Contrast with the searchTasks handler, which correctly returns the legacy response before checking the window.

Suggested fix: gate the window check on wantsPagination only, or move the !wantsPagination legacy return above the window check (as searchTasks does).

2. searchTasks pagination window guard is off-by-one — both false-rejects and can silently omit matches

// authorized window for every page; paginateAuthorizedTasks then
// traverses it by immutable task ID rather than mutable rank.
const results = await tasks.searchTasks(query, {
clientAccessBoundary:
scope === "public" ? undefined : taskClientBoundary,
limit: wantsPagination ? 500 : limit,
userId: scope === "public" ? null : userId,
status,
visibility: scope,
});
if (!wantsPagination) return ok(results.slice(0, limit));
if (results.length >= 500) {
return err(
"This search matched the server's 500-candidate ranking window. Narrow the query or filters before paginating so no matches are silently omitted.",
{ code: "RESULT_WINDOW_EXCEEDED" },
);
}

tasks.searchTasks clamps its DB candidateLimit to Math.min(Math.max(limit * 4, 64), 500), so limit: 500 (passed here when paginating) always yields DB take: 500 — no sentinel row past the window (contrast listTasks, which correctly fetches 5001 and guards on greater than 5000). The guard here checks results.length >= 500, the same value as the fetch cap rather than one past it:

  • A query matching exactly 500 tasks (all scoring > 0) is rejected with RESULT_WINDOW_EXCEEDED even though the full result set fits in the window and is fully paginable.
  • tasks.searchTasks applies a .filter(task => task.score > 0) after the DB fetch but before this length check. If the DB truncates a larger match set to 500 rows and some of those rows score 0 at the JS layer (e.g. queries containing the percent sign, which getSearchTerms preserves and Postgres ILIKE matches on, but which isn't reflected in the in-memory scorer), results.length can drop below 500 and slip past this guard — silently omitting matches beyond the DB window, which is exactly the failure mode this error message says it prevents.

Note: fixing this isn't a one-line change in this file alone — candidateLimit is hard-clamped to 500 in tasks.server.ts, so raising the mcp-server fetch limit to 501 requires also raising that clamp (or having searchTasks return an explicit truncation signal).

No other significant or high-confidence issues found.

@mikepsinn
mikepsinn merged commit 7aef858 into main Aug 1, 2026
71 checks passed
@mikepsinn
mikepsinn deleted the feature/mcp-task-reference-contract branch August 1, 2026 22:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants