Skip to content

Fix Kody capability search matching - #272

Merged
kentcdodds merged 2 commits into
mainfrom
cursor/kody-search-capability-matching-a8ef
Apr 27, 2026
Merged

kentcdodds merged 2 commits into
mainfrom
cursor/kody-search-capability-matching-a8ef

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Apr 27, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • route MCP unified capability results through the shared capability search implementation instead of maintaining a separate capability scorer inside search.ts
  • clamp negative vector influence in the hybrid lexical/vector blend so literal token matches (for example lutron) are not cancelled out
  • harden the capability search result boundary by exposing a finite public vectorScore and adding a runtime invariant for registry keying during unified capability candidate mapping
  • add focused regression coverage for synthesized home Lutron capabilities via natural-language queries like lutron lights color and turn office lights red

Root cause

Kody:search and meta_list_capabilities were reading from the same runtime capability registry, but they were not using the same retrieval path. meta_list_capabilities listed the runtime capability correctly, while MCP search used a separate lightweight scorer inside packages/worker/src/mcp/tools/search.ts instead of the dedicated capability search implementation in packages/worker/src/mcp/capabilities/capability-search.ts.

That separate scorer also blended lexical overlap with a raw vector cosine score that could go negative, which meant a real lexical token match could be suppressed rather than boosted.

Review follow-up

I validated the automated review comments against the actual fix. I accepted and implemented the two comments that represented real risks:

  • normalize the public CapabilitySearchHit.vectorScore to a finite value while preserving internal ranking sentinels
  • fail loudly if unified search can no longer map searchCapabilities() hits back to the capability registry

I intentionally did not do the suggested test-helper extraction because it was only optional cleanup and did not affect correctness or the original search goal.

Testing

  • npm run test -- --run packages/worker/src/mcp/tools/search.node.test.ts packages/worker/src/mcp/capabilities/capability-search.workers.test.ts
  • npm run typecheck

Artifact

Focused validation log

Open in Web Open in Cursor 

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed search tool to properly await asynchronous search operations, ensuring correct result handling.
  • New Features

    • Enhanced search results with explicit lexical and vector score metrics for improved transparency.
    • Improved hybrid scoring calculation for more accurate search ranking.
  • Tests

    • Extended test coverage with new search scenarios and domain-specific test cases.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@coderabbitai

coderabbitai Bot commented Apr 27, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The search functionality is refactored to operate asynchronously. The searchUnified function now returns a Promise, with capability candidate generation delegated to a centralized searchCapabilities function. Type definitions are enriched with explicit lexical and vector score fields, and vector scores are zero-clamped before blending.

Changes

Cohort / File(s) Summary
Agent Tool Integration
packages/worker/src/agent-turn/tools.ts
Added await to searchUnified call, ensuring the resolved search result is used before constructing the response object.
Capability Search Scoring
packages/worker/src/mcp/capabilities/capability-search.ts
Added explicit lexicalScore and vectorScore fields to CapabilitySearchHit type and implemented zero-clamping of vector scores before blending with lexical scores in hybrid scoring.
Search Implementation Refactor
packages/worker/src/mcp/tools/search.ts
Converted searchUnified to asynchronous operation; refactored buildCapabilityCandidates to delegate scoring to searchCapabilities and updated all call sites (searchPackages, registerSearchTool) to await the async operation.
Search Test Suite Updates
packages/worker/src/mcp/tools/search.node.test.ts
Converted existing searchUnified tests to async; added new home/tool domain test setup with mocked Lutron capabilities and assertions verifying ranking behavior for literal and intent-based lighting queries.

Sequence Diagram

sequenceDiagram
    participant Agent as Agent/Tool
    participant SearchUnified as searchUnified
    participant BuildCandidates as buildCapabilityCandidates
    participant SearchCap as searchCapabilities
    participant Embedding as Vector Store

    Agent->>SearchUnified: await searchUnified(input)
    SearchUnified->>BuildCandidates: async call
    BuildCandidates->>SearchCap: searchCapabilities(query, limit)
    SearchCap->>Embedding: compute embedding & similarity
    Embedding-->>SearchCap: capability matches with scores
    SearchCap-->>BuildCandidates: CapabilitySearchHit[]
    BuildCandidates->>BuildCandidates: map to SearchCandidate<br/>(clamp scores, filter base > 0)
    BuildCandidates-->>SearchUnified: SearchCandidate[]
    SearchUnified->>SearchUnified: merge packages + capabilities
    SearchUnified-->>Agent: Promise<SearchUnifiedResult>
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Poem

🐰 The search hops now in async stride,
No longer blocking, side-by-side,
Capabilities rank with vector grace,
Clamped and blended in their place!
Unified search—a faster race! 🚀✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix Kody capability search matching' clearly and concisely describes the main change: fixing how capability search matching works in Kody, which aligns with the PR's core objective of routing MCP unified capability results through the shared capability search implementation.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 cursor/kody-search-capability-matching-a8ef

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 and usage tips.

@kentcdodds
kentcdodds marked this pull request as ready for review April 27, 2026 04:44
@github-actions

github-actions Bot commented Apr 27, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-272.kentcdodds.workers.dev

Worker: kody-pr-272
D1: kody-pr-272-db
KV: kody-pr-272-oauth-kv

Mocks:

@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.

🧹 Nitpick comments (3)
packages/worker/src/mcp/tools/search.node.test.ts (1)

540-588: Optional: extract the shared HOME_CONNECTOR_SESSION stub.

buildHomeRegistryWithLutronCapabilities and the existing loadDownHomeConnectorStatus tests at lines 678–732 each hand-roll an HOME_CONNECTOR_SESSION durable-object stub with the same idFromName / get().fetch() shape. A small makeHomeConnectorSessionStub({ snapshot }) helper would tighten the file and prevent drift if the snapshot contract changes.

Also applies to: 678-732

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/tools/search.node.test.ts` around lines 540 - 588,
Extract the duplicated durable-object stub into a helper like
makeHomeConnectorSessionStub that accepts a snapshot (or tools) parameter and
returns an object shaped like HOME_CONNECTOR_SESSION (implementing
idFromName(name) { return name } and get().fetch(...) that returns
Response.json(snapshot) for the /snapshot path and throws for unexpected paths);
then replace the inline HOME_CONNECTOR_SESSION stubs used in
buildHomeRegistryWithLutronCapabilities and the loadDownHomeConnectorStatus
tests with calls to this helper (pass runtimeLutronTools or the appropriate
snapshot) so both tests reuse the same contract-producing function.
packages/worker/src/mcp/capabilities/capability-search.ts (1)

307-309: Consider a finite sentinel for vectorScore on the public hit shape.

vectorScoreById[id] ?? Number.NEGATIVE_INFINITY is now propagated onto CapabilitySearchHit.vectorScore. Today it works only because every downstream consumer (e.g. blendLexicalAndVectorScore via Math.max(0, vector), and the secondary tiebreaker on Line 330) tolerates -Infinity. Any new caller that does plain arithmetic on match.vectorScore (averaging, summing, exposing in telemetry/logs) will inherit -Infinity and likely produce NaN or misleading values. Consider normalizing to 0 (or another finite sentinel) at the boundary where the value is exposed on the hit, while keeping -Infinity only as an internal ranking sentinel.

♻️ Suggested change
-			lexicalScore: lexicalScoreById[id] ?? 0,
-			vectorScore: vectorScoreById[id] ?? Number.NEGATIVE_INFINITY,
+			lexicalScore: lexicalScoreById[id] ?? 0,
+			vectorScore: Number.isFinite(vectorScoreById[id])
+				? vectorScoreById[id]!
+				: 0,

Also applies to: 344-346

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/capabilities/capability-search.ts` around lines 307 -
309, The hit objects are exposing Number.NEGATIVE_INFINITY via
CapabilitySearchHit.vectorScore which can break downstream math; change the
construction of vectorScoreById (and the similar block at lines 344-346) to map
any -Infinity (i.e., when vectorScoreMap.get(id) is undefined or -Infinity) to a
finite sentinel such as 0 before assigning to the public hit shape; keep
Number.NEGATIVE_INFINITY only internally for ranking logic (e.g., when creating
vectorScoreMap or doing comparisons in blendLexicalAndVectorScore) but normalize
to 0 (or another chosen finite value) when populating vectorScoreById and the
values placed onto CapabilitySearchHit.vectorScore.
packages/worker/src/mcp/tools/search.ts (1)

608-629: Consider adding an explicit invariant check for registry keying.

The current code is correct: capabilitySpecs is keyed by spec.name (per createCapabilitySpecs in build-capability-registry.ts:87), and searchCapabilities returns matches where match.name === spec.name. The lookup at line 623 therefore succeeds. However, this invariant is not enforced by the type system—if a future refactor changes the registry keying (to a namespaced id, a distinct spec.id, etc.), the lookups at lines 623 and 1284 would silently return undefined and drop capabilities from results without error. Consider adding a runtime assertion in buildCapabilityCandidates to surface this assumption, or restructure searchCapabilities to return the spec id alongside the summary, making the pairing explicit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/tools/search.ts` around lines 608 - 629, Add an
explicit runtime invariant check in buildCapabilityCandidates to ensure that
input.registry.capabilitySpecs is keyed by spec.name and that lookups from
searchCapabilities never return undefined: after retrieving const spec =
input.registry.capabilitySpecs[match.name] (in buildCapabilityCandidates) assert
spec is defined and that spec.name === match.name (or throw a clear Error
including match.name and any identifying info), so missing/misaligned keys fail
fast instead of silently dropping candidates; alternatively, modify
searchCapabilities to return the spec id alongside each match and use that id to
index capabilitySpecs before calling capabilityMatchToCandidate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/worker/src/mcp/capabilities/capability-search.ts`:
- Around line 307-309: The hit objects are exposing Number.NEGATIVE_INFINITY via
CapabilitySearchHit.vectorScore which can break downstream math; change the
construction of vectorScoreById (and the similar block at lines 344-346) to map
any -Infinity (i.e., when vectorScoreMap.get(id) is undefined or -Infinity) to a
finite sentinel such as 0 before assigning to the public hit shape; keep
Number.NEGATIVE_INFINITY only internally for ranking logic (e.g., when creating
vectorScoreMap or doing comparisons in blendLexicalAndVectorScore) but normalize
to 0 (or another chosen finite value) when populating vectorScoreById and the
values placed onto CapabilitySearchHit.vectorScore.

In `@packages/worker/src/mcp/tools/search.node.test.ts`:
- Around line 540-588: Extract the duplicated durable-object stub into a helper
like makeHomeConnectorSessionStub that accepts a snapshot (or tools) parameter
and returns an object shaped like HOME_CONNECTOR_SESSION (implementing
idFromName(name) { return name } and get().fetch(...) that returns
Response.json(snapshot) for the /snapshot path and throws for unexpected paths);
then replace the inline HOME_CONNECTOR_SESSION stubs used in
buildHomeRegistryWithLutronCapabilities and the loadDownHomeConnectorStatus
tests with calls to this helper (pass runtimeLutronTools or the appropriate
snapshot) so both tests reuse the same contract-producing function.

In `@packages/worker/src/mcp/tools/search.ts`:
- Around line 608-629: Add an explicit runtime invariant check in
buildCapabilityCandidates to ensure that input.registry.capabilitySpecs is keyed
by spec.name and that lookups from searchCapabilities never return undefined:
after retrieving const spec = input.registry.capabilitySpecs[match.name] (in
buildCapabilityCandidates) assert spec is defined and that spec.name ===
match.name (or throw a clear Error including match.name and any identifying
info), so missing/misaligned keys fail fast instead of silently dropping
candidates; alternatively, modify searchCapabilities to return the spec id
alongside each match and use that id to index capabilitySpecs before calling
capabilityMatchToCandidate.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 44ff59ee-26cd-44df-a061-b1d45ab98255

📥 Commits

Reviewing files that changed from the base of the PR and between 922d129 and 44a6d31.

📒 Files selected for processing (4)
  • packages/worker/src/agent-turn/tools.ts
  • packages/worker/src/mcp/capabilities/capability-search.ts
  • packages/worker/src/mcp/tools/search.node.test.ts
  • packages/worker/src/mcp/tools/search.ts

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kentcdodds
kentcdodds merged commit 829cd0b into main Apr 27, 2026
9 checks passed
@kody-bot
kody-bot deleted the cursor/kody-search-capability-matching-a8ef branch April 30, 2026 23:15
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