Skip to content

refactor(mcp): move builtin server knowledge out of the MCP runtime - #18630

Open
DeJeune wants to merge 10 commits into
mainfrom
mcp-runtime-builtin-tool-leak
Open

refactor(mcp): move builtin server knowledge out of the MCP runtime#18630
DeJeune wants to merge 10 commits into
mainfrom
mcp-runtime-builtin-tool-leak

Conversation

@DeJeune

@DeJeune DeJeune commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Branch strategy

  • Active development targets main.

What this PR does

Before this PR:

McpRuntimeService.getOrCreateClient was 470 lines / 47 branch points and knew individual built-in servers by name: @cherry/flomo and @cherry/nowledge-mem were stored as inMemory rows but hard-coded to HTTP URLs inside the transport switch, @cherry/mcp-auto-install was excluded from the in-memory branch by name, and its MCP_REGISTRY_PATH was set inline in the npx branch.

After this PR:

The built-in catalog moved to @shared/data/presets/mcpServers.ts and declares those two servers as streamableHttp with their real baseUrl; a new BuiltinMcpServerSeeder migrates already-installed rows (and legacy inMemory mcp-auto-install rows to stdio). With the data honest, the runtime needs no name knowledge at all, so getOrCreateClient was split into reuseLiveClient / connectClient / connectWithFallback / finishOAuth, with transport construction in mcpTransport.ts, npx/uvx/uv resolution as a runner table in mcpLaunch.ts, and the SDK loader plus transport policy in mcpClientSdk.ts.

Fixes # None

Why we need it and why it was done in this way

The following tradeoffs were made:

  • The seeder only rewrites rows still stored as inMemory, so re-running is a no-op, later user edits survive, and a built-in the user never installed (or deleted) is never resurrected — at the cost of not reconciling anything else.
  • Migrating flomo to a real baseUrl changes its OAuth storage key (md5(baseUrl)), so affected users re-authorize once. Preserving the old token was rejected because md5('') is shared by every server without a baseUrl (flomo and nowledge-mem collide on it today), and a wrong token simply triggers the existing 401 → re-auth path.

The following alternatives were considered:

  • Keeping the URL table in the runtime behind a resolver function — rejected, it leaves two sources of truth for the same endpoint.
  • A hand-written Drizzle data migration instead of a seeder — rejected, the seeding guide reserves the migration chain for schema and points one-time data fixes for existing users at a run-on-change seeder with an idempotent guard.

Links to places where the discussion took place:

None

Breaking changes

In Settings → MCP, @cherry/flomo and @cherry/nowledge-mem now show as Streamable HTTP with a URL instead of in-memory, and a user who had authorized flomo is asked to authorize once more on the next connection. Recorded in v2-refactor-temp/docs/breaking-changes/2026-08-15-builtin-mcp-servers-http-transport.md.

Special notes for your reviewer

  • Two behavior fixes ride along: transport fallback (405/404 → other transport) now also applies to built-in HTTP servers, and the bun argument rewrite checks position instead of membership (args.includes('x') previously launched bun -y pkg x when a package argument happened to be x or -y).
  • Deliberately unchanged: the OAuth serverUrlHash and getServerKey still read the stored row, and resolveMcpConfigTransportType in McpServerFields.tsx is now redundant but left alone.
  • Not covered by tests: a manual smoke run against a real flomo / npx stdio / in-memory server.

Checklist

This checklist is not enforcing, but it's a reminder of items that could be relevant to every PR.
Approvers are expected to review this list.

Release note

The built-in flomo and Nowledge Mem MCP servers now connect over Streamable HTTP with their real endpoint instead of an in-memory transport; installed servers are migrated automatically. action required: if you had authorized flomo, authorize it once more on the next connection.

`@cherry/flomo` and `@cherry/nowledge-mem` were stored as `inMemory` servers
while actually talking to an HTTP endpoint, so the runtime had to special-case
them by name. Declare the real connection in the preset catalog and migrate
installed rows, matching legacy `@cherry/mcp-auto-install` rows to stdio too.

The catalog moves to `@shared/data/presets` so the renderer and the seeder read
one source. Only rows still stored as `inMemory` are rewritten: re-running is a
no-op, user edits survive, and a builtin the user never installed stays absent.

Signed-off-by: suyao <sy20010504@gmail.com>
…time

`getOrCreateClient` was 470 lines and 47 branches covering client reuse,
transport construction, package-manager resolution, OAuth and retry. Move
transport creation to `mcpTransport`, the npx/uvx/uv resolution to a runner
table in `mcpLaunch`, and the SDK loader plus transport policy to
`mcpClientSdk`; the service keeps caching, status and bookkeeping.

Also fixes the bun argument rewrite: `args.includes('x')` suppressed the
`x -y` prefix whenever a package argument happened to be `x` or `-y`, so
`bun -y pkg x` was launched instead of `bun x -y pkg x`.

Signed-off-by: suyao <sy20010504@gmail.com>
@claude

claude Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Code review complete. No high-confidence issues caused or made reachable by this PR were found.

Verified areas:

  • Runtime refactor equivalence (client reuse / fallback loop / OAuth reconnect / pendingClients lifecycle) against the pre-refactor getOrCreateClient.
  • Transport dispatch change from name-aware (isInMemoryBuiltinMcpServer) to type-only (server.type === 'inMemory'): a non-builtin inMemory+command row would now throw Unknown in-memory MCP server instead of running as stdio, but no row producer reaches that state — the form gates inMemory behind !isBuiltin, JSON import rejects non-builtin inMemory via the schema refine, and protocol/DXT/Npx only emit stdio/sse/streamableHttp.
  • BuiltinMcpServerSeeder idempotency (WHERE type='inMemory' makes re-runs no-ops once migrated), the mcp-auto-install args-clobber path being pre-empted by the form's resolveMcpConfigTransportType self-heal, headers-merge equivalence, and preset/DTO/DB field completeness.

CI, signatures, and merge readiness are outside the scope of this review.

…ool-leak

# Conflicts:
#	src/main/ai/mcp/McpRuntimeService.ts
#	src/main/ai/mcp/__tests__/McpRuntimeService.test.ts
#	src/main/ai/mcp/servers/factory.ts
…ool-leak

# Conflicts:
#	src/main/ai/mcp/McpRuntimeService.ts
#	src/main/ai/mcp/__tests__/McpRuntimeService.test.ts
#	src/main/ai/mcp/servers/factory.ts
#	src/renderer/pages/settings/McpSettings/BuiltinMcpServerList.tsx
#	src/renderer/pages/settings/McpSettings/__tests__/builtinMcpServers.test.ts
#	src/shared/data/presets/mcpServers.ts

@zhangjiadi225 zhangjiadi225 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The overall direction is sound: moving the preset catalog into shared data and separating transport/launch concerns removes static builtin endpoint knowledge from the runtime. However, the refactor currently introduces a first-connect single-flight race, leaves legacy builtin identity incomplete during seeding, and carries forward an unregistered main-process filesystem path. These should be addressed before merge.

Comment thread src/main/ai/mcp/McpRuntimeService.ts Outdated
Comment thread src/main/data/db/seeding/seeders/builtinMcpServerSeeder.ts
Comment thread src/main/ai/mcp/servers/factory.ts Outdated

@kangfenmao kangfenmao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The transport and preset refactor is moving in a good direction, and the existing review already covers the first-connect single-flight race, legacy builtin identity, and path registration. Two additional correctness gaps remain: a fresh QVeris install has no UI path to provide the credential that activation still requires after the transport change, and case-sensitive Authorization detection can mix static credentials with the OAuth provider. Please address these before merge.

Comment thread src/shared/data/presets/mcpServers.ts
Comment thread src/main/ai/mcp/mcpTransport.ts Outdated
- Restore first-connect single-flight: registering the pending promise after an
  await let two concurrent callers open two clients and leak one (A7).
- Keep the builtin identity of migrated rows: legacy builtins carry no
  installSource and were recognised by their `inMemory` type alone, so Settings
  would unlock their name and transport after the seeder (A1).
- Merge HTTP headers case-insensitively: a user-entered `authorization` now
  replaces the app default instead of travelling beside it, and it suppresses
  the OAuth provider the same way `Authorization` does (A1).
- Read the mcp-auto-install catalog through `feature.mcp.registry_file` instead
  of building a path off the bin directory and mkdir-ing it (C1/B3).
- Render and persist `env` for hosted builtins on any transport, so a freshly
  installed QVeris can still be given its API key (A1).
…ool-leak

# Conflicts:
#	src/main/ai/mcp/McpRuntimeService.ts
…ool-leak

# Conflicts:
#	src/main/ai/mcp/McpRuntimeService.ts
@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

The review is complete. No findings to report — the PR correctly addresses all previous feedback and the refactoring is well-structured with comprehensive test coverage.

@zhangjiadi225 zhangjiadi225 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM. The previously requested changes are addressed.

@cherry-ai-bot cherry-ai-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cherry Review · 阻塞

1 blocker · 2 warning · 2 notice
逐条见行内评论。

Comment thread src/main/data/db/seeding/seeders/builtinMcpServerSeeder.ts
Comment thread src/main/data/db/seeding/seeders/__tests__/builtinMcpServerSeeder.test.ts Outdated
Comment thread src/shared/data/presets/mcpServers.ts
Comment thread src/shared/data/presets/__tests__/mcpServers.test.ts Outdated
A manually added or protocol-installed server whose name collides with a
builtin preset owns its own configuration; matching on name and `inMemory`
type alone rewrote its transport. Restrict the update to rows whose
installSource is builtin or absent (the legacy builtin shape).

Also pins the preset endpoints and the seeder version contract in tests
instead of accepting any HTTP URL or re-deriving the hash, freezes the shared
preset catalog both sides read live, and attributes the breaking-change entry.
@claude

claude Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

🤖 Generated with Claude Code

Review Summary

I've completed a thorough review of this refactoring PR. The implementation is well-designed with correct handling of:

  • Seeder scope: Only migrates inMemory rows owned by builtin (installSource = 'builtin' or NULL), preserves user edits, and never installs new servers
  • Concurrent client creation: Single-flight pattern correctly prevents client leaks with synchronous pendingClients.set() registration
  • Arg transformation: Position-based check fixes the previous membership-based bug
  • QVeris API key: Properly validated at connection time
  • Breaking change: OAuth re-authorization is documented and intentional

No findings to report. The test coverage is comprehensive and the architectural separation into mcpTransport.ts, mcpLaunch.ts, and mcpClientSdk.ts is clean.

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.

4 participants