Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 80 additions & 38 deletions packages/worker/src/mcp/capabilities/unified-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ import {
type SecretMetadata,
type SecretSearchRow,
} from '#mcp/secrets/types.ts'
import { buildValueEntityId, describeValue } from '#mcp/tools/search-entities.ts'
import {
buildValueEntityId,
describeValue,
} from '#mcp/tools/search-entities.ts'
import {
type UiArtifactSearchHit,
searchUiArtifactsForUser,
Expand Down Expand Up @@ -89,6 +92,45 @@ function scoreSkillLexicalMatch(
return lexicalScore(query, doc) + bonus
}

type ValueLexicalFields = Pick<
ValueMetadata,
'name' | 'description' | 'scope' | 'value' | 'appId'
>

function scoreValuePhraseBonus(query: string, row: ValueLexicalFields): number {
const normalizedQuery = normalizeSearchPhrase(query)
let bonus = 0
bonus += scoreSkillPhraseMatch(normalizedQuery, row.name) * 2
bonus += scoreSkillPhraseMatch(normalizedQuery, row.description) * 1.5
bonus += scoreSkillPhraseMatch(normalizedQuery, row.scope) * 0.5
bonus += scoreSkillPhraseMatch(normalizedQuery, row.appId) * 0.5
bonus += scoreSkillPhraseMatch(normalizedQuery, row.value) * 1
return bonus
}

type ConnectorLexicalFields = {
connectorName: string
description: string | null | undefined
apiBaseUrl: string | null | undefined
tokenUrl: string | null | undefined
requiredHosts: ReadonlyArray<string>
}

function scoreConnectorPhraseBonus(
query: string,
entry: ConnectorLexicalFields,
): number {
const normalizedQuery = normalizeSearchPhrase(query)
let bonus = 0
bonus += scoreSkillPhraseMatch(normalizedQuery, entry.connectorName) * 2
bonus += scoreSkillPhraseMatch(normalizedQuery, entry.description) * 1.5
bonus += scoreSkillPhraseMatch(normalizedQuery, entry.apiBaseUrl) * 1
bonus += scoreSkillPhraseMatch(normalizedQuery, entry.tokenUrl) * 0.75
bonus +=
scoreSkillPhraseMatch(normalizedQuery, entry.requiredHosts.join(' ')) * 0.75
return bonus
}

function scoreCapabilityLexicalMatch(
query: string,
hit: CapabilitySearchHit,
Expand Down Expand Up @@ -136,14 +178,7 @@ function scoreValueLexicalMatch(
row: ValueMetadata,
doc: string,
): number {
const normalizedQuery = normalizeSearchPhrase(query)
let bonus = 0
bonus += scoreSkillPhraseMatch(normalizedQuery, row.name) * 2
bonus += scoreSkillPhraseMatch(normalizedQuery, row.description) * 1.5
bonus += scoreSkillPhraseMatch(normalizedQuery, row.scope) * 0.5
bonus += scoreSkillPhraseMatch(normalizedQuery, row.appId) * 0.5
bonus += scoreSkillPhraseMatch(normalizedQuery, row.value) * 1
return lexicalScore(query, doc) + bonus
return lexicalScore(query, doc) + scoreValuePhraseBonus(query, row)
}

function buildValueEmbedDoc(row: ValueMetadata): string {
Expand Down Expand Up @@ -174,20 +209,16 @@ function scoreConnectorLexicalMatch(
entry: ConnectorSearchEntry,
doc: string,
): number {
const requiredHosts = entry.config.requiredHosts ?? []
const normalizedQuery = normalizeSearchPhrase(query)
let bonus = 0
bonus += scoreSkillPhraseMatch(normalizedQuery, entry.config.name) * 2
bonus +=
scoreSkillPhraseMatch(
normalizedQuery,
describeConnector(entry.config, entry.row.description),
) * 1.5
bonus += scoreSkillPhraseMatch(normalizedQuery, entry.config.apiBaseUrl) * 1
bonus += scoreSkillPhraseMatch(normalizedQuery, entry.config.tokenUrl) * 0.75
bonus +=
scoreSkillPhraseMatch(normalizedQuery, requiredHosts.join(' ')) * 0.75
return lexicalScore(query, doc) + bonus
return (
lexicalScore(query, doc) +
scoreConnectorPhraseBonus(query, {
connectorName: entry.config.name,
description: describeConnector(entry.config, entry.row.description),
apiBaseUrl: entry.config.apiBaseUrl,
tokenUrl: entry.config.tokenUrl,
requiredHosts: entry.config.requiredHosts ?? [],
})
)
}

function buildConnectorEmbedDoc(entry: ConnectorSearchEntry): string {
Expand Down Expand Up @@ -930,16 +961,23 @@ export async function searchUnified(input: {
if (key.startsWith('n:')) {
const hit = connectorByName.get(key.slice(2))
if (!hit) return 0
return lexicalScore(
input.query,
[
hit.connectorName,
hit.description,
hit.flow,
hit.tokenUrl,
hit.apiBaseUrl ?? '',
hit.requiredHosts.join(' '),
].join('\n'),
const doc = [
hit.connectorName,
hit.description,
hit.flow,
hit.tokenUrl,
hit.apiBaseUrl ?? '',
hit.requiredHosts.join(' '),
].join('\n')
return (
lexicalScore(input.query, doc) +
scoreConnectorPhraseBonus(input.query, {
connectorName: hit.connectorName,
description: hit.description,
apiBaseUrl: hit.apiBaseUrl,
tokenUrl: hit.tokenUrl,
requiredHosts: hit.requiredHosts,
})
)
}
if (key.startsWith('c:')) {
Expand Down Expand Up @@ -981,11 +1019,15 @@ export async function searchUnified(input: {
if (key.startsWith('v:')) {
const hit = valueById.get(key.slice(2))
if (!hit) return 0
return lexicalScore(
input.query,
[hit.name, hit.description, hit.scope, hit.value, hit.appId ?? ''].join(
'\n',
),
const doc = [
hit.name,
hit.description,
hit.scope,
hit.value,
hit.appId ?? '',
].join('\n')
return (
lexicalScore(input.query, doc) + scoreValuePhraseBonus(input.query, hit)
)
}
if (key.startsWith('a:')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,97 @@ test('search skips connector rows whose stored id disagrees with config.name', a

expect(result.matches.some((match) => match.type === 'connector')).toBe(false)
})

test('exact value-name matches win cross-entity lexical ties', async () => {
const env = { SENTRY_ENVIRONMENT: 'test' } as Env
const specs = {
preferred_org_lookup: {
name: 'preferred_org_lookup',
domain: 'meta',
description: 'Look up preferred org settings.',
keywords: ['preferred', 'org', 'lookup'],
readOnly: true,
idempotent: true,
destructive: false,
inputFields: ['name'],
requiredInputFields: ['name'],
outputFields: ['value'],
inputSchema: {},
},
} satisfies Record<string, CapabilitySpec>
const valueRow = createValueRow('preferred_org', {
description: 'Stored organization preference',
value: 'kentcdodds',
})

const result = await searchUnified({
env,
baseUrl: 'http://localhost',
query: 'preferred org',
limit: 5,
specs,
userId: 'user-123',
skillRows: [],
uiArtifactRows: [],
userSecretRows: [],
userValueRows: [valueRow],
appSecretsByAppId: new Map(),
})

expect(result.matches[0]).toMatchObject({
type: 'value',
name: 'preferred_org',
})
})

test('exact connector-name matches win cross-entity lexical ties', async () => {
const env = { SENTRY_ENVIRONMENT: 'test' } as Env
const specs = {
github_connector_lookup: {
name: 'github_connector_lookup',
domain: 'meta',
description: 'Inspect saved GitHub connector details.',
keywords: ['github', 'connector', 'lookup'],
readOnly: true,
idempotent: true,
destructive: false,
inputFields: ['name'],
requiredInputFields: ['name'],
outputFields: ['connector'],
inputSchema: {},
},
} satisfies Record<string, CapabilitySpec>
const connectorRow = createValueRow('_connector:github', {
value: JSON.stringify({
name: 'github',
tokenUrl: 'https://github.com/login/oauth/access_token',
apiBaseUrl: 'https://api.github.com',
flow: 'confidential',
clientIdValueName: 'github_client_id',
clientSecretSecretName: 'github_client_secret',
accessTokenSecretName: 'github_access_token',
refreshTokenSecretName: 'github_refresh_token',
requiredHosts: ['api.github.com'],
}),
description: 'GitHub OAuth connector config',
})

const result = await searchUnified({
env,
baseUrl: 'http://localhost',
query: 'github',
limit: 5,
specs,
userId: 'user-123',
skillRows: [],
uiArtifactRows: [],
userSecretRows: [],
userValueRows: [connectorRow],
appSecretsByAppId: new Map(),
})

expect(result.matches[0]).toMatchObject({
type: 'connector',
connectorName: 'github',
})
})
Loading