Skip to content

Batch MCP search discovery into a few product operations - #51

Merged
shuv1337 merged 1 commit into
v2from
shuv/batch-discovery-20261022
Oct 22, 2026
Merged

shuv1337 merged 1 commit into
v2from
shuv/batch-discovery-20261022

Conversation

@shuv1337

Copy link
Copy Markdown
Owner

Round 17, Fix A: batch MCP search discovery. One org-wide execute search re-discovered every app on its first tools.search, one app at a time. In production a single heavy call (trace d4837beb…, 33 apps) made about 73 product operations on the product's single thread. Throttled locally to stand in for the 2-vCPU VM, that held other sessions' MCP calls for up to 2.5 s.

Change

  • Two new backend operations replace the per-app discovery dispatches:
    • listAccountsEach lists every searched app's linked accounts in one operation, with each app's own success or error.
    • searchableKept serves, in one operation, the search rows the product already saved for those apps.
  • Same checks, read in bulk. Each app is still decided exactly as before: grant policy, app audience and group access (appAccessOf), linked accounts and active version. The reads are batched instead: one statement for membership, policies and links of all apps (resourceAuthorityForLinksOfApps), one for apps and active versions, one for selected accounts, one for kept rows, and one for the actor's recheck.
  • Anything the batch can't fully serve falls back to the old per-app path. That includes missing, stale or superseded rows, a failed check, and an app that wasn't checked in this request. So stalled-app isolation, the discovery wait and idle limits, and every error message are unchanged by construction.
  • Wiring: the grant wrapper (packages/mcp-auth) and the request forwarders handle both new operations. The local server batches listAccounts and keeps its old per-app search reads.

Measured

Native self-host, CPU throttled with taskpolicy -c background, 30 apps (10 with accounts) plus a canary app:

Per search-heavy execute Before After
Product operations 73 11
SQL statements 129 16 (8 of them are ranking queries)
First search's discovery 430–815 ms 81–181 ms
Execute p50 (10 runs) 1211 ms 734 ms

Canary MCP latency (p50 / p95 / max):

Load Before After
10 back-to-back heavy executes 503 / 724 / 724 ms 434 / 498 / 498 ms
30 s loop of heavy executes 554 / 824 / 942 ms 394 / 594 / 709 ms

In the 30 s loop this branch also completed 43 executes against 26 before. REST canary latency was about unchanged.

Tests

  • New mcp-search-statements.spec.ts: searching 12 apps uses the same 4 operations and 9 statements as searching 6. With batching switched off it fails at 28 operations and 57 statements.
  • Passing on self-host: mcp-catalog, the self-host scenarios of mcp-catalog-scale, mcp-direct-calls, kept-listings, mcp-grant-restrictions, mcp-account-choice, mcp-deferred-setup, group-visibility, the mcp-execute-* specs, mcp-connections, connection-keys, tool-call-overhead, tool-call-statements, mcp-search-concise, catalog-agent-search, linked-accounts, template-accounts, tool-index-redeploy and app-routers.
  • Passing on local: local MCP connections, account descriptions, account choice and kept listings.
  • Failures that also happen on origin/v2:
    • mcp-execute-failures "Every error a write reports… leads with the write warning", on the unclassified ConnectionKeyRefused in the OpenAPI document.
    • The 7,000-tool scenario's 15 s cold-search bound when run with 8 parallel workers. Run alone, as CI runs it, the cold search takes 3.9 s.
  • bun run check passes.

Deploy

Not deployed with this PR. It ships only if soaked and finished at least 24 h before the earliest confirmed cutover cutoff; otherwise it ships after the cutovers.

mcp-search-statements.spec.ts
mcp-catalog.spec.ts
mcp-catalog-scale.spec.ts
mcp-direct-calls.spec.ts
kept-listings.spec.ts
mcp-grant-restrictions.spec.ts
mcp-account-choice.spec.ts
mcp-deferred-setup.spec.ts
group-visibility.spec.ts
mcp-connections.spec.ts
connection-keys.spec.ts
mcp-execute-concurrency.spec.ts
mcp-execute-failures.spec.ts
mcp-execute-lazy.spec.ts
mcp-execute-reach.spec.ts
mcp-search-concise.spec.ts
catalog-agent-search.spec.ts
tool-call-overhead.spec.ts
tool-call-statements.spec.ts
linked-accounts.spec.ts
local-mcp-connections.spec.ts

An organization-wide MCP search discovered each app with two product
operations, listAccounts and searchableTools, about 73 operations and 129
PGlite statements for 30 apps, all on the thread other sessions' calls share.

Discovery now lists the searched apps' accounts with one listAccountsEach
operation and reads their kept search rows with one searchableKept
operation. The hosted backend runs the same checks per app from bulk reads:
grant and policy as before, the caller's access and links for every app with
accounts in one statement, the apps with their active versions in one, the
selected accounts in one, the kept rows in one, and the actor's recheck of
every app (appsResolving) in one. searchableKept never lists an app's tools:
an app whose rows must be written, or that any check refuses, is left out
and discovered on its own exactly as before, so stalled and slow apps stay
isolated and every error is the one its own read reports. Local serves no
kept rows this way and keeps its per-app reads.

Measured on the native self-host host under taskpolicy -c background with
30 apps (10 with accounts): a search-heavy execute went from 73 operations
and 129 statements to 11 and 16, the first search's discovery from about
550 ms to 140 ms, and canary MCP calls during it from p50 503 / max 724 ms
to 434 / 498 ms.

The new mcp-search-statements scenario checks that searching twelve apps
makes the same four product operations and the same statements as six.
@shuv1337
shuv1337 merged commit 53eac4a into v2 Oct 22, 2026
3 checks passed
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.

1 participant