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
4 changes: 4 additions & 0 deletions docs/agents/mcp-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ capabilities, via **`CAPABILITY_REINDEX_SECRET`**) re-embeds every row in
`mcp_skills` and upserts `skill_<uuid>` vectors. Use when D1 and Vectorize are
out of sync for saved skills (e.g. after a restore or manual D1 edits).

**`POST /__maintenance/reindex-apps`** (same auth secret) re-embeds visible
saved UI artifacts and upserts `ui_artifact_<uuid>` vectors after app-search
embed text changes or any D1/Vectorize drift.

For broken skills, prefer **`meta_update_skill`** to fix stored code in place;
alternatively **`meta_delete_skill`** + **`meta_save_skill`**. There is no
versioning.
7 changes: 4 additions & 3 deletions docs/setup-manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,10 @@ automatically:
`SKIP_CLOUDFLARE_MOCK=1`. The `cloudflare_rest` capability expects API paths
under `/client/v4/`.)
- `CAPABILITY_REINDEX_SECRET` (optional Worker secret; bearer auth for
`POST /__maintenance/reindex-capabilities` and
`POST /__maintenance/reindex-skills` to embed and upsert builtin capabilities
and all user skills into Vectorize)
`POST /__maintenance/reindex-capabilities`,
`POST /__maintenance/reindex-skills`, and
`POST /__maintenance/reindex-apps` to embed and upsert builtin capabilities,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use built-in here.

Line 93 reads more naturally as built-in capabilities.

🧰 Tools
🪛 LanguageTool

[grammar] ~93-~93: Ensure spelling is correct
Context: ...nance/reindex-apps` to embed and upsert builtin capabilities, all user skills, and di...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

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

In `@docs/setup-manifest.md` at line 93, Replace the single word "builtin" with
the hyphenated form "built-in" in the sentence that mentions the endpoint `POST
/__maintenance/reindex-apps` so it reads "built-in capabilities"; update the
phrase near that endpoint reference (`POST /__maintenance/reindex-apps`)
accordingly to use "built-in".

all user skills, and discoverable saved apps into Vectorize)

Tests run with `CLOUDFLARE_ENV=test` (set by Playwright) and still read local
secrets from `packages/worker/.env`.
Expand Down
5 changes: 5 additions & 0 deletions packages/worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
import { withCors } from './utils.ts'
import { handleCapabilityReindexRequest } from './capability-maintenance.ts'
import { handleSkillReindexRequest } from './skill-maintenance.ts'
import { handleUiArtifactReindexRequest } from './ui-artifact-maintenance.ts'
import { CodemodeFetchGateway } from '#mcp/fetch-gateway.ts'

export { ChatAgent, CodemodeFetchGateway, HomeConnectorSession, HomeMCP, MCP }
Expand Down Expand Up @@ -85,6 +86,10 @@ const appHandler = withCors({
return handleSkillReindexRequest(request, env)
}

if (url.pathname === '/__maintenance/reindex-apps') {
return handleUiArtifactReindexRequest(request, env)
}

if (url.pathname === oauthPaths.authorize) {
return handleAuthorizeRequest(request, env)
}
Expand Down
1 change: 0 additions & 1 deletion packages/worker/src/mcp/capabilities/apps/ui-save-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,6 @@ export const uiSaveAppCapability = defineDomainCapability(
embedText: buildUiArtifactEmbedText({
title: args.title,
description: args.description,
code: args.code,
runtime: args.runtime,
parameters,
}),
Expand Down
2 changes: 2 additions & 0 deletions packages/worker/src/mcp/capabilities/capability-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export function sortIdsByScore(
export type CapabilitySummaryRow = {
name: string
domain: string
description: string
requiredInputFields: Array<string>
}

Expand Down Expand Up @@ -126,6 +127,7 @@ function toSummary(spec: CapabilitySpec): CapabilitySummaryRow {
return {
name: spec.name,
domain: spec.domain,
description: spec.description,
requiredInputFields: spec.requiredInputFields,
}
}
Expand Down
67 changes: 7 additions & 60 deletions packages/worker/src/mcp/capabilities/unified-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { buildSkillEmbedText } from '#mcp/skills/skill-embed-and-flags.ts'
import { type McpSkillRow } from '#mcp/skills/mcp-skills-types.ts'
import {
parseSkillParameters,
type SkillParameterDefinition,
} from '#mcp/skills/skill-parameters.ts'
import {
type SecretMetadata,
Expand Down Expand Up @@ -90,14 +89,7 @@ export type SkillSearchHitSummary = {
vectorRank?: number
}

export type SkillSearchHitDetail = SkillSearchHitSummary & {
inferredCapabilities: Array<string>
usesCapabilities: Array<string> | null
searchText: string | null
parameters: Array<SkillParameterDefinition> | null
}

export type SkillSearchHit = SkillSearchHitSummary | SkillSearchHitDetail
export type SkillSearchHit = SkillSearchHitSummary

export type CapabilitySearchHitTyped = {
type: 'capability'
Expand All @@ -114,11 +106,7 @@ export type SecretSearchHitSummary = {
vectorRank?: number
}

export type SecretSearchHitDetail = SecretSearchHitSummary & {
updatedAt: string
}

export type SecretSearchHit = SecretSearchHitSummary | SecretSearchHitDetail
export type SecretSearchHit = SecretSearchHitSummary
Comment thread
cursor[bot] marked this conversation as resolved.

export type UnifiedSearchMatch =
| CapabilitySearchHitTyped
Expand All @@ -132,12 +120,11 @@ function buildSecretUsage(name: string) {

function rowToSecretHit(
row: SecretSearchRow,
detail: boolean,
fusedScore: number,
lexicalRank?: number,
vectorRank?: number,
): SecretSearchHit {
const base: SecretSearchHitSummary = {
return {
type: 'secret',
scope: 'user',
name: row.name,
Expand All @@ -147,39 +134,16 @@ function rowToSecretHit(
lexicalRank,
vectorRank,
}
if (detail) {
return {
...base,
updatedAt: row.updatedAt,
}
}
return base
}

function rowToSkillHit(
row: McpSkillRow,
detail: boolean,
fusedScore: number,
lexicalRank?: number,
vectorRank?: number,
): SkillSearchHit {
const keywords = parseJsonStringArray(row.keywords)
let inferred: Array<string> = []
try {
const v = JSON.parse(row.inferred_capabilities) as unknown
if (Array.isArray(v)) {
inferred = v.filter((x): x is string => typeof x === 'string')
}
} catch {
inferred = []
}
let uses: Array<string> | null = null
if (row.uses_capabilities) {
uses = parseJsonStringArray(row.uses_capabilities)
if (uses.length === 0) uses = null
}
const parameters = parseSkillParameters(row.parameters)
const base: SkillSearchHitSummary = {
return {
type: 'skill',
skillId: row.id,
domain: 'meta',
Expand All @@ -197,24 +161,12 @@ function rowToSkillHit(
lexicalRank,
vectorRank,
}
if (detail) {
const hit: SkillSearchHitDetail = {
...base,
inferredCapabilities: inferred,
usesCapabilities: uses,
searchText: row.search_text,
parameters,
}
return hit
}
return base
}

async function searchSkillsForUser(input: {
env: Env
query: string
limit: number
detail: boolean
specs: Record<string, CapabilitySpec>
userId: string
rows: Array<McpSkillRow>
Expand Down Expand Up @@ -321,7 +273,6 @@ async function searchSkillsForUser(input: {
const row = idSet.get(id)!
return rowToSkillHit(
row,
input.detail,
fused.get(id) ?? 0,
lexicalRankById.get(id),
vectorRankById.get(id),
Expand All @@ -334,7 +285,6 @@ async function searchSkillsForUser(input: {
async function searchSecretsForUser(input: {
query: string
limit: number
detail: boolean
rows: Array<SecretSearchRow>
}): Promise<{ matches: Array<SecretSearchHit>; offline: boolean }> {
const rowsByName = new Map(input.rows.map((row) => [row.name, row] as const))
Expand Down Expand Up @@ -382,7 +332,6 @@ async function searchSecretsForUser(input: {
matches: ordered.map((name) =>
rowToSecretHit(
rowsByName.get(name)!,
input.detail,
fused.get(name) ?? 0,
lexicalRankByName.get(name),
vectorRankByName.get(name),
Expand All @@ -394,9 +343,9 @@ async function searchSecretsForUser(input: {

export async function searchUnified(input: {
env: Env
baseUrl: string
query: string
limit: number
detail: boolean
specs: Record<string, CapabilitySpec>
userId: string | null
skillCollectionSlug?: string | null
Expand All @@ -412,7 +361,7 @@ export async function searchUnified(input: {
env: input.env,
query: input.query,
limit: input.limit,
detail: input.detail,
detail: false,
Comment thread
cursor[bot] marked this conversation as resolved.
specs: input.specs,
vectorMetadataFilter: isCapabilitySearchOffline(input.env)
? undefined
Expand Down Expand Up @@ -441,24 +390,22 @@ export async function searchUnified(input: {
secretResult = await searchSecretsForUser({
query: input.query,
limit: input.limit,
detail: input.detail,
rows: input.userSecretRows,
})
skillResult = await searchSkillsForUser({
env: input.env,
query: input.query,
limit: input.limit,
detail: input.detail,
specs: input.specs,
userId: input.userId,
collectionSlug: input.skillCollectionSlug,
rows: input.skillRows,
})
uiArtifactResult = await searchUiArtifactsForUser({
baseUrl: input.baseUrl,
env: input.env,
query: input.query,
limit: input.limit,
detail: input.detail,
userId: input.userId,
rows: input.uiArtifactRows,
appSecretsByAppId: input.appSecretsByAppId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ test('skill search hits include usage hints', async () => {

const result = await searchUnified({
env,
baseUrl: 'http://localhost',
query: 'open pull requests for cursor agents',
limit: 5,
detail: false,
Expand All @@ -79,36 +80,6 @@ test('skill search hits include usage hints', async () => {
expect(skill.collectionSlug).toBe('github-workflows')
})

test('skill detail hits include usage hints', async () => {
const env = { SENTRY_ENVIRONMENT: 'test' } as Env
const skillRow = createSkillRow('skill-usage-detail')
const specs = {} as Record<string, CapabilitySpec>

const result = await searchUnified({
env,
query: 'cursor agents with open prs',
limit: 5,
detail: true,
specs,
userId: 'user-123',
skillRows: [skillRow],
uiArtifactRows: [],
userSecretRows: [],
appSecretsByAppId: new Map(),
})

const skill = result.matches.find((match) => match.type === 'skill')
if (!skill || skill.type !== 'skill') {
throw new Error('Expected a skill match in results.')
}

expect(skill.usage).toContain('meta_get_skill')
expect(skill.usage).toContain(skillRow.id)
expect(skill.usage).toContain('"params"')
expect(skill.collection).toBe('GitHub Workflows')
expect(skill.collectionSlug).toBe('github-workflows')
})

test('skill collection filter narrows saved skill matches', async () => {
const env = { SENTRY_ENVIRONMENT: 'test' } as Env
const specs = {} as Record<string, CapabilitySpec>
Expand All @@ -125,9 +96,9 @@ test('skill collection filter narrows saved skill matches', async () => {

const result = await searchUnified({
env,
baseUrl: 'http://localhost',
query: 'github pull requests',
limit: 5,
detail: true,
specs,
userId: 'user-123',
skillCollectionSlug: 'github-workflows',
Expand All @@ -154,9 +125,9 @@ test('search can return standalone user secrets and nest app secrets on apps', a

const result = await searchUnified({
env,
baseUrl: 'http://localhost',
query: 'cloudflare deploy token',
limit: 5,
detail: true,
specs,
userId: 'user-123',
skillRows: [],
Expand Down Expand Up @@ -205,4 +176,5 @@ test('search can return standalone user secrets and nest app secrets on apps', a
},
])
expect(app.usage).toContain('"params"')
expect(app.hostedUrl).toBe('http://localhost/ui/app-123')
})
13 changes: 7 additions & 6 deletions packages/worker/src/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,18 @@ Domains
${domainInstructions}

How to use search
- Call the 'search' tool with a natural-language 'query' describing what you need (optional 'limit', 'detail').
- Call the 'search' tool with a natural-language 'query' describing what you need (optional 'limit').
- Search returns compact markdown plus slim structured results. Order implies rank.
- ${conversationIdGuidance}
- Narrow results by rephrasing 'query', or use the optional 'skill_collection' filter when you only want saved skills from one collection slug.
- Saved skills appear when the MCP client provides an authenticated user; use 'meta_get_skill' for full skill code.
- Use domain descriptions above as vocabulary hints in your query text.
- Use 'detail: true' when you need full JSON schemas and metadata.
- To inspect one result in more depth, call \`search\` again with \`entity: "{id}:{type}"\` where \`type\` is \`capability\`, \`skill\`, \`app\`, or \`secret\`.
- Example: search({ query: 'saved dashboard app or generated UI runtime' })
- Example: search({ query: 'repository automation pull requests', detail: true })
- Example: search({ query: 'Cloudflare API zones dns workers d1', detail: true })
- Example: search({ query: 'REST API documentation markdown' })
- Example: search({ query: 'Cloudflare API docs markdown' })
- Example: search({ query: 'repository automation pull requests' })
- Example: search({ query: 'Cloudflare API zones dns workers d1' })
- Example: search({ entity: 'cloudflare_rest:capability' })
- Example: search({ entity: '70a6a63f-2711-4617-b273-ec95f3be114b:skill' })

Destructive Cloudflare access
- The cloudflare_rest capability can change or delete Cloudflare resources such as DNS records, Workers settings, routes, or account configuration (POST/PUT/PATCH/DELETE).
Expand Down
Loading
Loading