From 198c92489860ab0734a73f96ff11a4a326dc9d31 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 4 May 2026 23:51:22 +0000 Subject: [PATCH 1/6] Make MCP search query results concise Co-authored-by: Kent C. Dodds --- docs/use/search.md | 29 ++-- .../src/mcp/tools/search-format.node.test.ts | 51 +++++- .../worker/src/mcp/tools/search-format.ts | 156 +++--------------- .../src/mcp/tools/search-handler.node.test.ts | 63 ++++++- packages/worker/src/mcp/tools/search.ts | 48 ++---- 5 files changed, 163 insertions(+), 184 deletions(-) diff --git a/docs/use/search.md b/docs/use/search.md index 67027ee418..002be962aa 100644 --- a/docs/use/search.md +++ b/docs/use/search.md @@ -7,19 +7,21 @@ The **search** tool finds **built-in capabilities**, **saved packages**, ## Queries and ranking Pass a **`query`** string that describes what you want to do. Results are -ranked; order in the response matters. +ranked; order in the response matters. Query responses are intentionally +compact: the markdown response is a short list of matches with the result type, +title, one-line summary, and entity reference when applicable. -When a tool call also includes **`memoryContext`**, Kody may attach a small -number of relevant long-term memories that have not already been surfaced for -that **`conversationId`**. +When a tool call also includes **`memoryContext`**, Kody may include relevant +long-term memory metadata in structured content, but broad query markdown stays +focused on the ranked matches. Search responses also return top-level **`timing`** metadata with **`startedAt`**, **`endedAt`**, and **`durationMs`** so hosts can reason about how long the ranked lookup or entity lookup took. Optional **`limit`** caps how many ranked hits return. Optional -**`maxResponseSize`** trims low-ranked matches when the response must stay -small. +**`maxResponseSize`** trims low-ranked matches against the compact list when the +response must stay small. ## Single-entity detail @@ -57,16 +59,11 @@ for an empty ranked list. Saved **packages** require a signed-in MCP user. Capabilities and built-in behavior work without user-scoped data. -Package search hits summarize whether the package has an app surface and, when -they do, include a direct `open_generated_ui({ kody_id: "..." })` hint. -When a saved package includes a root `README` file, ranked search hits include a -short README excerpt so agents can quickly see usage notes, examples, or -maintenance caveats without separately inspecting the package repo. Exact -package detail (`entity: "my-package:package"`) includes the README content, or -a substantial leading section when the README is long. -Connector hits also include operational details such as the token URL, API base -URL, required hosts, and related stored value/secret names so common -setup/debugging work can often proceed without an immediate detail lookup. +Package and connector query hits stay summary-only. Exact package detail +(`entity: "my-package:package"`) includes package app, export, job, retriever, +and README metadata. Exact connector detail (`entity: "github:connector"`) +includes operational details such as token URL, API base URL, required hosts, +and related stored value/secret names. Long-term memory retrieval also requires a signed-in MCP user. diff --git a/packages/worker/src/mcp/tools/search-format.node.test.ts b/packages/worker/src/mcp/tools/search-format.node.test.ts index 5868d3b4ad..d4374ae448 100644 --- a/packages/worker/src/mcp/tools/search-format.node.test.ts +++ b/packages/worker/src/mcp/tools/search-format.node.test.ts @@ -346,7 +346,9 @@ export declare function fetch(request: Request): Promise }, }) expect(packageDetail.markdown).toContain('## README (`README.md`)') - expect(packageDetail.markdown).toContain('Use this package to inspect observed UI state.') + expect(packageDetail.markdown).toContain( + 'Use this package to inspect observed UI state.', + ) }) test('package search formatting keeps runnable package actions in structured output', () => { @@ -396,10 +398,22 @@ test('package search formatting keeps runnable package actions in structured out expect(packageMatch?.nextStep).toEqual(expect.any(String)) }) -test('search markdown includes concise package README snippets when available', () => { +test('search markdown keeps broad query results compact', () => { const markdown = formatSearchMarkdown({ baseUrl: 'http://localhost', - warnings: [], + warnings: ['Saved package metadata fallback warning with long details.'], + guidance: + 'Inspect package detail with `search({ entity: "observed-package:package" })` to review exports, then import the right entry.', + memories: { + surfaced: [ + { + category: 'preference', + subject: 'Long-term preference', + summary: 'Verbose remembered context that should not appear inline.', + }, + ], + suppressedCount: 0, + }, matches: [ { type: 'package', @@ -417,12 +431,38 @@ test('search markdown includes concise package README snippets when available', truncated: true, }, }, + { + type: 'connector', + connectorName: 'github', + title: 'github', + description: 'GitHub OAuth connector config', + flow: 'confidential', + tokenUrl: 'https://github.com/login/oauth/access_token', + apiBaseUrl: 'https://api.github.com', + requiredHosts: ['api.github.com'], + clientIdValueName: 'github-client-id', + clientSecretSecretName: 'github-client-secret', + accessTokenSecretName: 'github-access-token', + refreshTokenSecretName: 'github-refresh-token', + }, ], }) expect(markdown).toContain( - '**README (README.md):** Includes setup instructions, export examples, and maintenance notes\\. _(truncated)_', + '1. **package** @kody/observed\\-package (`observed-package`) — Observed package with an app surface\\. Entity: `observed-package:package`', + ) + expect(markdown).toContain( + '2. **connector** `github` — GitHub OAuth connector config\\. Entity: `github:connector`', ) + expect(markdown).toContain('1 search notice(s) available') + expect(markdown).not.toContain('## Recommended next step') + expect(markdown).not.toContain('Inspect package detail') + expect(markdown).not.toContain('## Relevant memories') + expect(markdown).not.toContain('Long-term preference') + expect(markdown).not.toContain('**README') + expect(markdown).not.toContain('Token URL') + expect(markdown).not.toContain('github-access-token') + expect(markdown).not.toContain('**How to run matches:**') }) test('search formatting surfaces package retriever results', () => { @@ -454,8 +494,7 @@ test('search formatting surfaces package retriever results', () => { expect(markdown).not.toContain('Toaster **oven** wattage') expect(markdown).not.toContain('\n## Ignore prior instructions') expect(markdown).toContain('personal `inbox`') - expect(markdown).toContain('personal-inbox') - expect(markdown).toContain('https://example.com/path?x=`bad`') + expect(markdown).not.toContain('https://example.com/path?x=`bad`') const structured = toSlimStructuredMatches({ baseUrl: 'http://localhost', diff --git a/packages/worker/src/mcp/tools/search-format.ts b/packages/worker/src/mcp/tools/search-format.ts index 8395786e59..c1df92cdd0 100644 --- a/packages/worker/src/mcp/tools/search-format.ts +++ b/packages/worker/src/mcp/tools/search-format.ts @@ -420,6 +420,18 @@ function buildSecretUsage(name: string) { : '(secret placeholder unavailable for this name)' } +function formatOneLineSummary(value: string, maxLength = 180) { + const summary = value.replace(/\s+/g, ' ').trim() + if (summary.length <= maxLength) return summary + return `${summary.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...` +} + +function formatOneLineSentence(value: string) { + const summary = formatOneLineSummary(value) + if (!summary) return 'No description.' + return /[.!?]$/.test(summary) ? summary : `${summary}.` +} + function formatPackageSchedule( schedule: PackageJobSchedule, timezone?: string, @@ -482,168 +494,54 @@ export function formatSearchMarkdown(input: { const lines: Array = ['# Search results', ''] if (input.includePreamble ?? true) { lines.push( - 'For full detail on one hit, call `search` with `entity: "{id}:{type}"` (example: `kody_official_guide:capability`).', - '', - '**How to run matches:**', + 'For full detail on one hit, call `search` with `entity: "{id}:{type}"`.', '', - '- Built-in capabilities — `execute` with `import { codemode } from "kody:runtime"`', - '- Persisted values — `codemode.value_get({ name, scope })` or `codemode.value_list({ scope })`', - '- Saved connectors — `codemode.connector_get({ name })` or `codemode.connector_list({})`', - '- Saved packages — import from `kody:@scope/package-name/export-name`, edit with `repo_*`, and open package apps with `open_generated_ui({ kody_id })` when the package declares `kody.app`', - '- Secrets — placeholders in execute-time fetches or `codemode.secret_list` (never paste raw secrets in chat or embed `{{secret:...}}` literally into visible content such as comments, prompts, or issue bodies)', ) } - if (input.warnings.length > 0) { - lines.push('', '## Warnings', '') - for (const warning of input.warnings) { - lines.push(`- ${warning}`) - } - } - - if (input.memories && input.memories.surfaced.length > 0) { - lines.push('', '## Relevant memories', '') - for (const memory of input.memories.surfaced) { - const categorySuffix = memory.category ? ` (${memory.category})` : '' - lines.push(`- **${memory.subject}**${categorySuffix}: ${memory.summary}`) - } - if (input.memories.suppressedCount > 0) { - lines.push( - `- ${String(input.memories.suppressedCount)} additional memory item(s) were suppressed for this conversation.`, - ) - } - } - if (input.memories?.retrieverResults?.length) { - lines.push('', '## Relevant retriever results', '') - for (const result of input.memories.retrieverResults) { - lines.push( - `- **${escapeMarkdownText(result.title)}** — ${escapeMarkdownText(result.summary)} (${formatMarkdownInlineCode(`${result.kodyId}/${result.retrieverKey}`)})`, - ) - } - } - - if (input.guidance?.trim()) { - lines.push('', '## Recommended next step', '', input.guidance.trim()) - } - - for (const match of input.matches) { - lines.push('', ...formatMatchBlock(match, input.baseUrl)) - } - if (input.matches.length === 0) { lines.push( - '', '> **No matches.** Rephrase `query` or call `meta_list_capabilities` for the full capability registry. `entity` looks up a known id — it does not improve an empty ranked list.', ) + } else { + input.matches.forEach((match, index) => { + lines.push(formatMatchListItem(match, index)) + }) } - if ( - input.matches.length > 0 && - input.matches.every((match) => match.type !== 'secret') - ) { + if (input.warnings.length > 0) { lines.push( '', - '> **Note:** This page does not include any matching user secret references. If you need credential metadata, use `codemode.secret_list` inside `execute` or save secrets via generated UI.', + `> ${String(input.warnings.length)} search notice(s) available in the structured result.`, ) } return lines.join('\n').trim() } -function formatMatchBlock(match: SearchMatch, baseUrl: string) { +function formatMatchListItem(match: SearchMatch, index: number) { if (match.type === 'capability') { const entityRef = buildEntityRef(match.name, 'capability') - return [ - `## Capability — \`${match.name}\``, - '', - 'description' in match ? match.description : '', - '', - `**Entity:** \`${entityRef}\``, - `**Run:** \`${buildCapabilityUsage(match.name)}\``, - ] + return `${String(index + 1)}. **capability** ${formatMarkdownInlineCode(match.name)} — ${escapeMarkdownText(formatOneLineSentence(match.description))} Entity: ${formatMarkdownInlineCode(entityRef)}` } if (match.type === 'package') { - const hostedUrl = match.hasApp - ? buildPackageHostedUrl(baseUrl, match.kodyId) - : null const entityRef = buildEntityRef(match.kodyId, 'package') - const rootImportUsage = buildPackageRootImportUsage(match.name) - const openGeneratedUiUsage = match.hasApp - ? buildPackageAppUsage(match.kodyId) - : null - return [ - `## Package — ${match.title} (\`${match.kodyId}\`)`, - '', - match.description, - '', - `**Entity:** \`${entityRef}\``, - `**Package ID:** \`${match.packageId}\``, - ...(openGeneratedUiUsage - ? [`**Open app:** \`${openGeneratedUiUsage}\``] - : []), - `**Import:** \`${rootImportUsage}\``, - `**Tags:** ${match.tags.length > 0 ? match.tags.map((tag) => `\`${tag}\``).join(', ') : 'none'}`, - `**Has app:** ${match.hasApp ? 'yes' : 'no'}`, - ...(hostedUrl ? [`**Hosted URL:** \`${hostedUrl}\``] : []), - ...(match.readmeSnippet - ? [ - `**README (${match.readmeSnippet.path}):** ${escapeMarkdownText(match.readmeSnippet.snippet)}${match.readmeSnippet.truncated ? ' _(truncated)_' : ''}`, - ] - : []), - ] + return `${String(index + 1)}. **package** ${escapeMarkdownText(match.title)} (${formatMarkdownInlineCode(match.kodyId)}) — ${escapeMarkdownText(formatOneLineSentence(match.description))} Entity: ${formatMarkdownInlineCode(entityRef)}` } if (match.type === 'value') { const entityRef = buildEntityRef(match.valueId, 'value') - return [ - `## Value — \`${match.name}\` (\`${match.scope}\` scope)`, - '', - match.description, - '', - `**Entity:** \`${entityRef}\``, - `**Read:** \`${buildValueUsage(match.name, match.scope)}\``, - ] + return `${String(index + 1)}. **value** ${formatMarkdownInlineCode(match.name)} (${formatMarkdownInlineCode(match.scope)} scope) — ${escapeMarkdownText(formatOneLineSentence(match.description))} Entity: ${formatMarkdownInlineCode(entityRef)}` } if (match.type === 'connector') { const entityRef = buildEntityRef(match.connectorName, 'connector') - return [ - `## Connector — \`${match.connectorName}\``, - '', - match.description, - '', - `**Entity:** \`${entityRef}\``, - `**Read:** \`${buildConnectorUsage(match.connectorName)}\``, - `**Flow:** \`${match.flow}\``, - `**Token URL:** \`${match.tokenUrl}\``, - `**API base URL:** ${match.apiBaseUrl ? `\`${match.apiBaseUrl}\`` : 'none'}`, - `**Required hosts:** ${formatList(match.requiredHosts)}`, - `**Client ID value:** \`${match.clientIdValueName}\``, - `**Client secret secret:** ${match.clientSecretSecretName ? `\`${match.clientSecretSecretName}\`` : 'none'}`, - `**Access token secret:** \`${match.accessTokenSecretName}\``, - `**Refresh token secret:** ${match.refreshTokenSecretName ? `\`${match.refreshTokenSecretName}\`` : 'none'}`, - ] + return `${String(index + 1)}. **connector** ${formatMarkdownInlineCode(match.connectorName)} — ${escapeMarkdownText(formatOneLineSentence(match.description))} Entity: ${formatMarkdownInlineCode(entityRef)}` } if (match.type === 'retriever_result') { const source = match.source ?? `${match.kodyId}/${match.retrieverKey}` - return [ - `## Retrieved context — ${escapeMarkdownText(match.title)}`, - '', - escapeMarkdownText(match.summary), - ...(match.details ? ['', escapeMarkdownText(match.details)] : []), - '', - `**Source:** ${formatMarkdownInlineCode(source)}`, - `**Package:** ${formatMarkdownInlineCode(match.kodyId)}`, - `**Retriever:** ${formatMarkdownInlineCode(match.retrieverName)}`, - ...(match.url ? [`**URL:** ${formatMarkdownInlineCode(match.url)}`] : []), - ] + return `${String(index + 1)}. **retriever result** ${escapeMarkdownText(match.title)} — ${escapeMarkdownText(formatOneLineSentence(match.summary))} Source: ${formatMarkdownInlineCode(source)}` } - return [ - `## Secret — \`${match.name}\``, - '', - match.description, - '', - `**Entity:** \`${buildEntityRef(match.name, 'secret')}\``, - `**Usage:** \`${buildSecretUsage(match.name)}\``, - ] + const entityRef = buildEntityRef(match.name, 'secret') + return `${String(index + 1)}. **secret** ${formatMarkdownInlineCode(match.name)} — ${escapeMarkdownText(formatOneLineSentence(match.description))} Entity: ${formatMarkdownInlineCode(entityRef)}` } export function toSlimStructuredMatches(input: { diff --git a/packages/worker/src/mcp/tools/search-handler.node.test.ts b/packages/worker/src/mcp/tools/search-handler.node.test.ts index 51218fe0d1..78fbd0b3bd 100644 --- a/packages/worker/src/mcp/tools/search-handler.node.test.ts +++ b/packages/worker/src/mcp/tools/search-handler.node.test.ts @@ -93,8 +93,13 @@ async function getSearchHandler() { query?: string entity?: string limit?: number + maxResponseSize?: number conversationId?: string }) => Promise<{ + content: Array<{ + type: 'text' + text: string + }> structuredContent: { conversationId: string timing: { @@ -126,9 +131,9 @@ test('search tool reports timing metadata across success and error flows', async endedAt: expect.any(String), }, }) - expect(successResponse.structuredContent.timing.durationMs).toBeGreaterThanOrEqual( - 0, - ) + expect( + successResponse.structuredContent.timing.durationMs, + ).toBeGreaterThanOrEqual(0) mockPerformanceNow.mockReturnValueOnce(5).mockReturnValueOnce(9) const validationErrorResponse = await handler({ @@ -170,3 +175,55 @@ test('search tool reports timing metadata across success and error flows', async handledErrorResponse.structuredContent.timing.durationMs, ).toBeGreaterThanOrEqual(0) }) + +test('search tool returns compact query markdown while preserving structured auxiliary detail', async () => { + vi.clearAllMocks() + mockModule.loadRelevantMemoriesForTool.mockResolvedValueOnce({ + memories: [ + { + id: 'memory-1', + category: 'preference', + status: 'active', + subject: 'Verbose memory subject', + summary: + 'This memory summary is intentionally long and should not be rendered into broad search markdown.', + details: 'Long memory details should stay out of the text response.', + tags: ['search'], + updatedAt: '2026-04-20T00:00:00.000Z', + }, + ], + suppressedCount: 0, + retrievalQuery: 'search docs', + retrieverResults: [], + retrieverWarnings: ['Memory retriever warning should remain structured.'], + }) + const handler = await getSearchHandler() + + const response = await handler({ + query: 'search docs', + conversationId: 'conv-compact-search', + }) + const text = response.content.map((item) => item.text).join('\n') + + expect(response.isError).toBeUndefined() + expect(text).toContain('# Search results') + expect(text).toContain('1. **capability** `search_docs`') + expect(text).toContain('Entity: `search_docs:capability`') + expect(text).not.toContain('## Relevant memories') + expect(text).not.toContain('Verbose memory subject') + expect(text).not.toContain('## Recommended next step') + expect(text).not.toContain('## Warnings') + + const result = response.structuredContent.result as { + warnings: Array + guidance?: string + memories?: { surfaced: Array<{ id: string }> } + } + expect(result.warnings).toContain( + 'Memory retriever warning should remain structured.', + ) + expect(result.guidance).toContain('search_docs:capability') + expect(result.memories?.surfaced).toEqual([ + expect.objectContaining({ id: 'memory-1' }), + ]) +}) diff --git a/packages/worker/src/mcp/tools/search.ts b/packages/worker/src/mcp/tools/search.ts index a52e731260..b68b6a326f 100644 --- a/packages/worker/src/mcp/tools/search.ts +++ b/packages/worker/src/mcp/tools/search.ts @@ -1141,9 +1141,10 @@ Find **built-in capabilities**, **saved packages**, **persisted values**, **saved connectors**, and **user secret references** (metadata only) before \`execute\` or \`open_generated_ui\`. -**query** — ranked markdown + structured matches (order matters). If nothing useful -returns, rephrase or call \`meta_list_capabilities\`; \`entity\` does not fix an -empty ranked list. +**query** — compact ranked markdown + structured matches (order matters). Query +markdown is summary-only: type, title/name, one-line description, and entity ref. +If nothing useful returns, rephrase or call \`meta_list_capabilities\`; \`entity\` +does not fix an empty ranked list. **entity: "{id}:{type}"** — detail for one hit (\`capability\` | \`value\` | \`connector\` | \`package\` | \`secret\`). Capability detail includes @@ -1684,15 +1685,7 @@ export async function registerSearchTool(agent: McpRegistrationAgent) { return { mode: 'list' as const, - result: { - ...result, - matches: await enrichSearchMatchesWithPackageReadmes({ - env: agent.getEnv(), - baseUrl, - userId, - matches: result.matches, - }), - }, + result, } } @@ -1772,6 +1765,13 @@ export async function registerSearchTool(agent: McpRegistrationAgent) { if (memoryToolContext) { warnings.push(...memoryToolContext.retrieverWarnings) } + const structuredWarnings = warnings + const conciseWarnings = + warnings.length > 0 + ? [ + `${warnings.length} auxiliary search warning(s) available in structuredContent.result.warnings.`, + ] + : [] const payload: { matches: Array @@ -1796,17 +1796,7 @@ export async function registerSearchTool(agent: McpRegistrationAgent) { } = { matches: outcome.result.matches, offline: outcome.result.offline, - warnings, - ...(outcome.result.guidance - ? { - guidance: outcome.result.guidance, - } - : {}), - ...(searchMemories - ? { - memories: searchMemories, - } - : {}), + warnings: conciseWarnings, ...(normalizedRemoteConnectorStatuses ? { remoteConnectorStatuses: normalizedRemoteConnectorStatuses, @@ -1851,8 +1841,6 @@ export async function registerSearchTool(agent: McpRegistrationAgent) { formatSearchMarkdown({ matches: value.matches, warnings: value.warnings, - guidance: value.guidance, - memories: value.memories, baseUrl, includePreamble, }), @@ -1872,10 +1860,10 @@ export async function registerSearchTool(agent: McpRegistrationAgent) { ) const result: SearchResultStructuredContent = { offline: trimmedPayload.offline, - warnings: trimmedPayload.warnings, - ...(trimmedPayload.guidance + warnings: structuredWarnings, + ...(outcome.result.guidance ? { - guidance: trimmedPayload.guidance, + guidance: outcome.result.guidance, } : {}), telemetry: { @@ -1890,9 +1878,9 @@ export async function registerSearchTool(agent: McpRegistrationAgent) { ...outcome.result.phaseTimings, formattingMs, }, - ...(trimmedPayload.memories + ...(searchMemories ? { - memories: trimmedPayload.memories, + memories: searchMemories, } : {}), ...(trimmedPayload.homeConnectorStatus From 8a14e83c40801048d5b74e32d9664df6aa48c1b8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 5 May 2026 00:02:37 +0000 Subject: [PATCH 2/6] Address search result review feedback Co-authored-by: Kent C. Dodds --- .../src/mcp/tools/search-format.node.test.ts | 45 ++++++++++++++++++- .../worker/src/mcp/tools/search-format.ts | 13 ++++-- .../src/mcp/tools/search-handler.node.test.ts | 13 +++++- packages/worker/src/mcp/tools/search.ts | 1 + 4 files changed, 64 insertions(+), 8 deletions(-) diff --git a/packages/worker/src/mcp/tools/search-format.node.test.ts b/packages/worker/src/mcp/tools/search-format.node.test.ts index d4374ae448..d3dbe40b8a 100644 --- a/packages/worker/src/mcp/tools/search-format.node.test.ts +++ b/packages/worker/src/mcp/tools/search-format.node.test.ts @@ -401,7 +401,10 @@ test('package search formatting keeps runnable package actions in structured out test('search markdown keeps broad query results compact', () => { const markdown = formatSearchMarkdown({ baseUrl: 'http://localhost', - warnings: ['Saved package metadata fallback warning with long details.'], + warnings: [ + 'Saved package metadata fallback warning with long details.', + 'Package retrievers are temporarily unavailable.', + ], guidance: 'Inspect package detail with `search({ entity: "observed-package:package" })` to review exports, then import the right entry.', memories: { @@ -454,7 +457,9 @@ test('search markdown keeps broad query results compact', () => { expect(markdown).toContain( '2. **connector** `github` — GitHub OAuth connector config\\. Entity: `github:connector`', ) - expect(markdown).toContain('1 search notice(s) available') + expect(markdown).toContain( + '2 search notice(s) available in the structured result.', + ) expect(markdown).not.toContain('## Recommended next step') expect(markdown).not.toContain('Inspect package detail') expect(markdown).not.toContain('## Relevant memories') @@ -465,6 +470,42 @@ test('search markdown keeps broad query results compact', () => { expect(markdown).not.toContain('**How to run matches:**') }) +test('search markdown only suggests entity detail for entity-backed hits', () => { + const entityMarkdown = formatSearchMarkdown({ + baseUrl: 'http://localhost', + warnings: [], + matches: [ + { + type: 'capability', + name: 'search_docs', + description: 'Search docs capability', + }, + ], + }) + expect(entityMarkdown).toContain( + 'For full detail on entity-backed hits, call `search` with `entity: "{id}:{type}"`.', + ) + + const retrieverMarkdown = formatSearchMarkdown({ + baseUrl: 'http://localhost', + warnings: [], + matches: [ + { + type: 'retriever_result', + id: 'note-1', + title: 'Toaster oven wattage', + summary: 'The toaster oven is 1800 watts.', + score: 0.92, + packageId: 'package-1', + kodyId: 'personal-inbox', + retrieverKey: 'notes', + retrieverName: 'Personal notes', + }, + ], + }) + expect(retrieverMarkdown).not.toContain('entity: "{id}:{type}"') +}) + test('search formatting surfaces package retriever results', () => { const markdown = formatSearchMarkdown({ baseUrl: 'http://localhost', diff --git a/packages/worker/src/mcp/tools/search-format.ts b/packages/worker/src/mcp/tools/search-format.ts index c1df92cdd0..32293c01fc 100644 --- a/packages/worker/src/mcp/tools/search-format.ts +++ b/packages/worker/src/mcp/tools/search-format.ts @@ -478,6 +478,7 @@ export function parseEntityRef(entity: string): { export function formatSearchMarkdown(input: { matches: Array warnings: Array + warningCount?: number baseUrl: string includePreamble?: boolean guidance?: string @@ -492,9 +493,12 @@ export function formatSearchMarkdown(input: { } }) { const lines: Array = ['# Search results', ''] - if (input.includePreamble ?? true) { + const hasEntityBackedMatch = input.matches.some( + (match) => match.type !== 'retriever_result', + ) + if ((input.includePreamble ?? true) && hasEntityBackedMatch) { lines.push( - 'For full detail on one hit, call `search` with `entity: "{id}:{type}"`.', + 'For full detail on entity-backed hits, call `search` with `entity: "{id}:{type}"`.', '', ) } @@ -509,10 +513,11 @@ export function formatSearchMarkdown(input: { }) } - if (input.warnings.length > 0) { + const warningCount = input.warningCount ?? input.warnings.length + if (warningCount > 0) { lines.push( '', - `> ${String(input.warnings.length)} search notice(s) available in the structured result.`, + `> ${String(warningCount)} search notice(s) available in the structured result.`, ) } diff --git a/packages/worker/src/mcp/tools/search-handler.node.test.ts b/packages/worker/src/mcp/tools/search-handler.node.test.ts index 78fbd0b3bd..64b87ff91a 100644 --- a/packages/worker/src/mcp/tools/search-handler.node.test.ts +++ b/packages/worker/src/mcp/tools/search-handler.node.test.ts @@ -195,7 +195,10 @@ test('search tool returns compact query markdown while preserving structured aux suppressedCount: 0, retrievalQuery: 'search docs', retrieverResults: [], - retrieverWarnings: ['Memory retriever warning should remain structured.'], + retrieverWarnings: [ + 'First memory retriever warning should remain structured.', + 'Second memory retriever warning should remain structured.', + ], }) const handler = await getSearchHandler() @@ -213,6 +216,9 @@ test('search tool returns compact query markdown while preserving structured aux expect(text).not.toContain('Verbose memory subject') expect(text).not.toContain('## Recommended next step') expect(text).not.toContain('## Warnings') + expect(text).toContain( + '2 search notice(s) available in the structured result.', + ) const result = response.structuredContent.result as { warnings: Array @@ -220,7 +226,10 @@ test('search tool returns compact query markdown while preserving structured aux memories?: { surfaced: Array<{ id: string }> } } expect(result.warnings).toContain( - 'Memory retriever warning should remain structured.', + 'First memory retriever warning should remain structured.', + ) + expect(result.warnings).toContain( + 'Second memory retriever warning should remain structured.', ) expect(result.guidance).toContain('search_docs:capability') expect(result.memories?.surfaced).toEqual([ diff --git a/packages/worker/src/mcp/tools/search.ts b/packages/worker/src/mcp/tools/search.ts index b68b6a326f..8a3bd72681 100644 --- a/packages/worker/src/mcp/tools/search.ts +++ b/packages/worker/src/mcp/tools/search.ts @@ -1841,6 +1841,7 @@ export async function registerSearchTool(agent: McpRegistrationAgent) { formatSearchMarkdown({ matches: value.matches, warnings: value.warnings, + warningCount: structuredWarnings.length, baseUrl, includePreamble, }), From 5a39526dbddddce658759bca86d08f45c9030bce Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 5 May 2026 00:11:04 +0000 Subject: [PATCH 3/6] Remove stale search payload fields Co-authored-by: Kent C. Dodds --- packages/worker/src/mcp/tools/search.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/worker/src/mcp/tools/search.ts b/packages/worker/src/mcp/tools/search.ts index 8a3bd72681..7590a32a58 100644 --- a/packages/worker/src/mcp/tools/search.ts +++ b/packages/worker/src/mcp/tools/search.ts @@ -1777,8 +1777,6 @@ export async function registerSearchTool(agent: McpRegistrationAgent) { matches: Array offline: boolean warnings: Array - guidance?: string - memories?: SearchResultStructuredContent['memories'] homeConnectorStatus?: { connectorKind: string connectorId: string From 0a524b5d1bde37c7d58a17bc2f33e7ab9ce0fdb7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 5 May 2026 00:20:37 +0000 Subject: [PATCH 4/6] Remove unused search README enrichment Co-authored-by: Kent C. Dodds --- .../worker/src/mcp/tools/search.node.test.ts | 86 ------------------- packages/worker/src/mcp/tools/search.ts | 53 +----------- 2 files changed, 1 insertion(+), 138 deletions(-) diff --git a/packages/worker/src/mcp/tools/search.node.test.ts b/packages/worker/src/mcp/tools/search.node.test.ts index 8bdf8cf799..1ab5a1ff70 100644 --- a/packages/worker/src/mcp/tools/search.node.test.ts +++ b/packages/worker/src/mcp/tools/search.node.test.ts @@ -3,7 +3,6 @@ import { buildCapabilityRegistry } from '#mcp/capabilities/build-capability-regi import { buildConnectorValueName } from '#mcp/capabilities/values/connector-shared.ts' import { buildSavedPackageSearchRows, - enrichSearchMatchesWithPackageReadmes, loadDownHomeConnectorStatus, loadOptionalSearchRows, resolveSearchMemoryContext, @@ -25,11 +24,6 @@ function createPackageExportProjection(subpath: string) { const sourceMocks = vi.hoisted(() => ({ loadPackageManifestBySourceId: vi.fn(), - loadPackageSourceBySourceId: vi.fn(), -})) - -const packageRepoMocks = vi.hoisted(() => ({ - getSavedPackageByKodyId: vi.fn(), })) vi.mock('#worker/package-registry/source.ts', async () => { @@ -40,19 +34,6 @@ vi.mock('#worker/package-registry/source.ts', async () => { ...actual, loadPackageManifestBySourceId: (...args: Array) => sourceMocks.loadPackageManifestBySourceId(...args), - loadPackageSourceBySourceId: (...args: Array) => - sourceMocks.loadPackageSourceBySourceId(...args), - } -}) - -vi.mock('#worker/package-registry/repo.ts', async () => { - const actual = await vi.importActual< - typeof import('#worker/package-registry/repo.ts') - >('#worker/package-registry/repo.ts') - return { - ...actual, - getSavedPackageByKodyId: (...args: Array) => - packageRepoMocks.getSavedPackageByKodyId(...args), } }) @@ -196,73 +177,6 @@ test('searchUnified ranks mixed search rows through one shared pipeline', async ) }) -test('package README enrichment adds snippets onto ranked package matches', async () => { - packageRepoMocks.getSavedPackageByKodyId.mockResolvedValue({ - id: 'package-spotify', - userId: 'user-1', - name: '@kody/spotify-playback', - kodyId: 'spotify-playback', - description: 'Saved package for Spotify playback controls.', - tags: ['spotify', 'playback'], - searchText: 'spotify playback remote package', - sourceId: 'source-spotify', - hasApp: true, - createdAt: '2026-04-20T00:00:00.000Z', - updatedAt: '2026-04-20T00:00:00.000Z', - }) - sourceMocks.loadPackageSourceBySourceId.mockResolvedValue({ - manifest: {} as never, - source: {} as never, - files: { - 'README.md': `# Spotify playback - -Playback controls, queue helpers, and maintenance notes for the hosted remote. -`, - 'package.json': '{}', - }, - }) - - const matches = await enrichSearchMatchesWithPackageReadmes({ - env: { - APP_DB: {}, - } as Env, - baseUrl: 'http://localhost', - userId: 'user-1', - matches: [ - { - type: 'package', - packageId: 'package-spotify', - kodyId: 'spotify-playback', - name: '@kody/spotify-playback', - title: '@kody/spotify-playback', - description: 'Saved package for Spotify playback controls.', - tags: ['spotify', 'playback'], - hasApp: true, - }, - ], - }) - - expect(matches).toEqual([ - expect.objectContaining({ - type: 'package', - kodyId: 'spotify-playback', - readmeSnippet: { - path: 'README.md', - snippet: - 'Playback controls, queue helpers, and maintenance notes for the hosted remote.', - truncated: false, - }, - }), - ]) - expect(sourceMocks.loadPackageSourceBySourceId).toHaveBeenCalledWith( - expect.objectContaining({ - baseUrl: 'http://localhost', - userId: 'user-1', - sourceId: 'source-spotify', - }), - ) -}) - test('searchUnified includes package retriever results as first-class matches', async () => { const registry = buildCapabilityRegistry([]) const result = await searchUnified({ diff --git a/packages/worker/src/mcp/tools/search.ts b/packages/worker/src/mcp/tools/search.ts index 7590a32a58..fa5b6ca87a 100644 --- a/packages/worker/src/mcp/tools/search.ts +++ b/packages/worker/src/mcp/tools/search.ts @@ -35,7 +35,6 @@ import { buildPackageSearchProjection, type PackageSearchProjection, } from '#worker/package-registry/manifest.ts' -import { buildPackageReadmeSnippet } from '#worker/package-registry/package-readme.ts' import { loadPackageManifestBySourceId, loadPackageSourceBySourceId, @@ -1470,56 +1469,6 @@ async function resolveEntityDetail(input: { } } -export async function enrichSearchMatchesWithPackageReadmes(input: { - env: Env - baseUrl: string - userId: string | null - matches: Array -}): Promise> { - if (!input.userId) { - return input.matches - } - return await Promise.all( - input.matches.map(async (match) => { - if (match.type !== 'package') { - return match - } - try { - const record = await getSavedPackageByKodyId(input.env.APP_DB, { - userId: input.userId!, - kodyId: match.kodyId, - }) - if (!record) { - return match - } - const loaded = await loadPackageSourceBySourceId({ - env: input.env, - baseUrl: input.baseUrl, - userId: input.userId!, - sourceId: record.sourceId, - }) - return { - ...match, - readmeSnippet: buildPackageReadmeSnippet({ - files: loaded.files, - }), - } satisfies SearchMatch - } catch (cause) { - Sentry.captureException(cause, { - tags: { - scope: 'search.enrichPackageReadmeSnippet', - }, - extra: { - kodyId: match.kodyId, - packageId: match.packageId, - }, - }) - return match - } - }), - ) -} - export async function registerSearchTool(agent: McpRegistrationAgent) { agent.server.registerTool( searchTool.name, @@ -1765,7 +1714,7 @@ export async function registerSearchTool(agent: McpRegistrationAgent) { if (memoryToolContext) { warnings.push(...memoryToolContext.retrieverWarnings) } - const structuredWarnings = warnings + const structuredWarnings = [...warnings] const conciseWarnings = warnings.length > 0 ? [ From 16ad5b6b3c0c9042dcb6c344aa9f739b26b67e28 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 5 May 2026 00:28:51 +0000 Subject: [PATCH 5/6] Clean up compact search formatter inputs Co-authored-by: Kent C. Dodds --- .../src/mcp/tools/search-format.node.test.ts | 16 ---------------- packages/worker/src/mcp/tools/search-format.ts | 11 ----------- packages/worker/src/mcp/tools/search.ts | 1 - 3 files changed, 28 deletions(-) diff --git a/packages/worker/src/mcp/tools/search-format.node.test.ts b/packages/worker/src/mcp/tools/search-format.node.test.ts index d3dbe40b8a..6d54f87f23 100644 --- a/packages/worker/src/mcp/tools/search-format.node.test.ts +++ b/packages/worker/src/mcp/tools/search-format.node.test.ts @@ -400,23 +400,10 @@ test('package search formatting keeps runnable package actions in structured out test('search markdown keeps broad query results compact', () => { const markdown = formatSearchMarkdown({ - baseUrl: 'http://localhost', warnings: [ 'Saved package metadata fallback warning with long details.', 'Package retrievers are temporarily unavailable.', ], - guidance: - 'Inspect package detail with `search({ entity: "observed-package:package" })` to review exports, then import the right entry.', - memories: { - surfaced: [ - { - category: 'preference', - subject: 'Long-term preference', - summary: 'Verbose remembered context that should not appear inline.', - }, - ], - suppressedCount: 0, - }, matches: [ { type: 'package', @@ -472,7 +459,6 @@ test('search markdown keeps broad query results compact', () => { test('search markdown only suggests entity detail for entity-backed hits', () => { const entityMarkdown = formatSearchMarkdown({ - baseUrl: 'http://localhost', warnings: [], matches: [ { @@ -487,7 +473,6 @@ test('search markdown only suggests entity detail for entity-backed hits', () => ) const retrieverMarkdown = formatSearchMarkdown({ - baseUrl: 'http://localhost', warnings: [], matches: [ { @@ -508,7 +493,6 @@ test('search markdown only suggests entity detail for entity-backed hits', () => test('search formatting surfaces package retriever results', () => { const markdown = formatSearchMarkdown({ - baseUrl: 'http://localhost', warnings: [], matches: [ { diff --git a/packages/worker/src/mcp/tools/search-format.ts b/packages/worker/src/mcp/tools/search-format.ts index 32293c01fc..4310b38233 100644 --- a/packages/worker/src/mcp/tools/search-format.ts +++ b/packages/worker/src/mcp/tools/search-format.ts @@ -479,18 +479,7 @@ export function formatSearchMarkdown(input: { matches: Array warnings: Array warningCount?: number - baseUrl: string includePreamble?: boolean - guidance?: string - memories?: { - surfaced: Array<{ - category: string | null - subject: string - summary: string - }> - suppressedCount: number - retrieverResults?: Array - } }) { const lines: Array = ['# Search results', ''] const hasEntityBackedMatch = input.matches.some( diff --git a/packages/worker/src/mcp/tools/search.ts b/packages/worker/src/mcp/tools/search.ts index fa5b6ca87a..3d3c5469e1 100644 --- a/packages/worker/src/mcp/tools/search.ts +++ b/packages/worker/src/mcp/tools/search.ts @@ -1789,7 +1789,6 @@ export async function registerSearchTool(agent: McpRegistrationAgent) { matches: value.matches, warnings: value.warnings, warningCount: structuredWarnings.length, - baseUrl, includePreamble, }), (value, count) => ({ From 025e441bd3bfc5edd6376d4efe4393f811e03aa7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 5 May 2026 00:38:12 +0000 Subject: [PATCH 6/6] Remove dead compact warning payload Co-authored-by: Kent C. Dodds --- packages/worker/src/mcp/tools/search-format.ts | 4 ++-- packages/worker/src/mcp/tools/search.ts | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/worker/src/mcp/tools/search-format.ts b/packages/worker/src/mcp/tools/search-format.ts index 4310b38233..52f9ead5c2 100644 --- a/packages/worker/src/mcp/tools/search-format.ts +++ b/packages/worker/src/mcp/tools/search-format.ts @@ -477,7 +477,7 @@ export function parseEntityRef(entity: string): { export function formatSearchMarkdown(input: { matches: Array - warnings: Array + warnings?: Array warningCount?: number includePreamble?: boolean }) { @@ -502,7 +502,7 @@ export function formatSearchMarkdown(input: { }) } - const warningCount = input.warningCount ?? input.warnings.length + const warningCount = input.warningCount ?? input.warnings?.length ?? 0 if (warningCount > 0) { lines.push( '', diff --git a/packages/worker/src/mcp/tools/search.ts b/packages/worker/src/mcp/tools/search.ts index 3d3c5469e1..750aaa6257 100644 --- a/packages/worker/src/mcp/tools/search.ts +++ b/packages/worker/src/mcp/tools/search.ts @@ -1715,17 +1715,10 @@ export async function registerSearchTool(agent: McpRegistrationAgent) { warnings.push(...memoryToolContext.retrieverWarnings) } const structuredWarnings = [...warnings] - const conciseWarnings = - warnings.length > 0 - ? [ - `${warnings.length} auxiliary search warning(s) available in structuredContent.result.warnings.`, - ] - : [] const payload: { matches: Array offline: boolean - warnings: Array homeConnectorStatus?: { connectorKind: string connectorId: string @@ -1743,7 +1736,6 @@ export async function registerSearchTool(agent: McpRegistrationAgent) { } = { matches: outcome.result.matches, offline: outcome.result.offline, - warnings: conciseWarnings, ...(normalizedRemoteConnectorStatuses ? { remoteConnectorStatuses: normalizedRemoteConnectorStatuses, @@ -1787,7 +1779,6 @@ export async function registerSearchTool(agent: McpRegistrationAgent) { (value) => formatSearchMarkdown({ matches: value.matches, - warnings: value.warnings, warningCount: structuredWarnings.length, includePreamble, }),