Skip to content

fix(mcp): make initiliasation faster#59232

Merged
joshsny merged 1 commit into
masterfrom
mcp/fix-initialisation
May 20, 2026
Merged

fix(mcp): make initiliasation faster#59232
joshsny merged 1 commit into
masterfrom
mcp/fix-initialisation

Conversation

@joshsny
Copy link
Copy Markdown
Contributor

@joshsny joshsny commented May 20, 2026

We’re doing a lot of work on every request, lets try frontload as much work as possible so we can make initialisation faster.

This brings init time down from ~400ms to ~15ms in local testing, and reduced how much memory is used initialising.

Copilot AI review requested due to automatic review settings May 20, 2026 16:17
@joshsny joshsny requested a review from a team May 20, 2026 16:17
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 20, 2026

Comments Outside Diff (1)

  1. services/mcp/src/hono/streamable-handler.ts, line 281-297 (link)

    P2 clearResourceCache() combined with falsy-length guard can cause per-request GitHub fetches

    After warmup succeeds, clearResourceCache() nulls out the module-level cachedResources. In mcp-server.ts the pre-warmed entries are only used when this._warmup?.resourceEntries.length is truthy. If filterValidEntries returns an empty array (e.g. all archive files are missing), _resourceEntries has length 0, the guard is falsy, and every request falls back to registerResourcesfetchContextMillResources() with no cache — re-downloading the full archive from GitHub on every call. Consider checking this._warmup existence rather than this._warmup.resourceEntries.length, or storing a boolean indicating that warmup was attempted regardless of result.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: services/mcp/src/hono/streamable-handler.ts
    Line: 281-297
    
    Comment:
    **`clearResourceCache()` combined with falsy-length guard can cause per-request GitHub fetches**
    
    After warmup succeeds, `clearResourceCache()` nulls out the module-level `cachedResources`. In `mcp-server.ts` the pre-warmed entries are only used when `this._warmup?.resourceEntries.length` is truthy. If `filterValidEntries` returns an empty array (e.g. all archive files are missing), `_resourceEntries` has length 0, the guard is falsy, and every request falls back to `registerResources``fetchContextMillResources()` with no cache — re-downloading the full archive from GitHub on every call. Consider checking `this._warmup` existence rather than `this._warmup.resourceEntries.length`, or storing a boolean indicating that warmup was attempted regardless of result.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
services/mcp/src/hono/mcp-server.ts:378-382
**Debug profiling logs shipped to production**

`_lap` and the 8+ `console.log('[init-profile]...')` calls inside `initInner` are unconditional, so every request initialisation emits a burst of structured profiling lines to stdout. At any meaningful request volume this creates significant log noise and makes it harder to signal real errors. The PR description says these were measured in "local testing", so this looks like instrumentation that was left in by accident. Either gate it behind an env-var/debug flag or remove it before merging.

### Issue 2 of 3
services/mcp/tests/unit/StateManager.test.ts:482
The test title says "return undefined" but the assertion is `toEqual(mockGroupTypes)` — the actual behaviour being verified is that stale cached data is served when a re-fetch fails, not `undefined`. The misleading name will confuse anyone reading the test output or the test suite in the future.

```suggestion
        it('should return stale cached data (not undefined) when fetch succeeds then later fails', async () => {
```

### Issue 3 of 3
services/mcp/src/hono/streamable-handler.ts:281-297
**`clearResourceCache()` combined with falsy-length guard can cause per-request GitHub fetches**

After warmup succeeds, `clearResourceCache()` nulls out the module-level `cachedResources`. In `mcp-server.ts` the pre-warmed entries are only used when `this._warmup?.resourceEntries.length` is truthy. If `filterValidEntries` returns an empty array (e.g. all archive files are missing), `_resourceEntries` has length 0, the guard is falsy, and every request falls back to `registerResources``fetchContextMillResources()` with no cache — re-downloading the full archive from GitHub on every call. Consider checking `this._warmup` existence rather than `this._warmup.resourceEntries.length`, or storing a boolean indicating that warmup was attempted regardless of result.

Reviews (1): Last reviewed commit: "fix initialisation" | Re-trigger Greptile

Comment on lines +378 to +382
const _t0 = performance.now()
const _lap = (label: string): void => {
const elapsed = performance.now() - _t0
console.log(`[init-profile] ${label.padEnd(40)} +${(elapsed).toFixed(0)}ms`)
}
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.

P1 Debug profiling logs shipped to production

_lap and the 8+ console.log('[init-profile]...') calls inside initInner are unconditional, so every request initialisation emits a burst of structured profiling lines to stdout. At any meaningful request volume this creates significant log noise and makes it harder to signal real errors. The PR description says these were measured in "local testing", so this looks like instrumentation that was left in by accident. Either gate it behind an env-var/debug flag or remove it before merging.

Prompt To Fix With AI
This is a comment left during a code review.
Path: services/mcp/src/hono/mcp-server.ts
Line: 378-382

Comment:
**Debug profiling logs shipped to production**

`_lap` and the 8+ `console.log('[init-profile]...')` calls inside `initInner` are unconditional, so every request initialisation emits a burst of structured profiling lines to stdout. At any meaningful request volume this creates significant log noise and makes it harder to signal real errors. The PR description says these were measured in "local testing", so this looks like instrumentation that was left in by accident. Either gate it behind an env-var/debug flag or remove it before merging.

How can I resolve this? If you propose a fix, please make it concise.

expect(getGroupTypes).toHaveBeenCalledTimes(2)
})

it('should return undefined (not stale data) when fetch succeeds then later fails', async () => {
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.

P2 The test title says "return undefined" but the assertion is toEqual(mockGroupTypes) — the actual behaviour being verified is that stale cached data is served when a re-fetch fails, not undefined. The misleading name will confuse anyone reading the test output or the test suite in the future.

Suggested change
it('should return undefined (not stale data) when fetch succeeds then later fails', async () => {
it('should return stale cached data (not undefined) when fetch succeeds then later fails', async () => {
Prompt To Fix With AI
This is a comment left during a code review.
Path: services/mcp/tests/unit/StateManager.test.ts
Line: 482

Comment:
The test title says "return undefined" but the assertion is `toEqual(mockGroupTypes)` — the actual behaviour being verified is that stale cached data is served when a re-fetch fails, not `undefined`. The misleading name will confuse anyone reading the test output or the test suite in the future.

```suggestion
        it('should return stale cached data (not undefined) when fetch succeeds then later fails', async () => {
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +523 to +528
if (this._warmup?.resourceEntries.length) {
const { registerResourceEntries } = await import('@/resources')
registerResourceEntries(this.server, this._warmup.resourceEntries)
} else {
await registerResources(this.server, context)
}
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.

Logic error with empty array check causes fallback even when warmup succeeded.

If warmup succeeds but finds zero valid resources (empty array), the condition this._warmup?.resourceEntries.length evaluates to 0 (falsy), causing the code to fall back to registerResources() and re-fetch everything. This defeats the warmup optimization when there are no resources.

Fix: Check for warmup data existence instead:

if (this._warmup) {
    const { registerResourceEntries } = await import('@/resources')
    registerResourceEntries(this.server, this._warmup.resourceEntries)
} else {
    await registerResources(this.server, context)
}
Suggested change
if (this._warmup?.resourceEntries.length) {
const { registerResourceEntries } = await import('@/resources')
registerResourceEntries(this.server, this._warmup.resourceEntries)
} else {
await registerResources(this.server, context)
}
if (this._warmup) {
const { registerResourceEntries } = await import('@/resources')
registerResourceEntries(this.server, this._warmup.resourceEntries)
} else {
await registerResources(this.server, context)
}

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

try {
const { fetchContextMillResources, filterValidEntries, loadManifestFromArchive, clearResourceCache } =
await import('@/resources/internals')
const archive = await fetchContextMillResources()
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.

Environment variable POSTHOG_MCP_LOCAL_SKILLS_URL is ignored during warmup.

The warmup calls fetchContextMillResources() without parameters, always fetching from GitHub. However, the fallback path in registerContextMillResources respects the local URL override. This causes inconsistent behavior in development/testing - warmup uses GitHub data while per-request fallback uses local data.

Fix: Pass environment variable through the warmup chain. The handler needs access to the environment or the local URL needs to be passed from startup.

Suggested change
const archive = await fetchContextMillResources()
const archive = await fetchContextMillResources(process.env.POSTHOG_MCP_LOCAL_SKILLS_URL)

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to reduce per-request MCP initialization cost by frontloading work (tool construction and context-mill resource parsing) into a warmup step, while also improving caching behavior for some API-derived state.

Changes:

  • Introduces a ToolCatalog that pre-builds tool bases/definitions during warmup and filters tools per-request without re-initializing everything.
  • Adds resource “internals” helpers and preloads/parses context-mill resources at server startup, then reuses pre-parsed resource entries during request init.
  • Adjusts StateManager cached-fetch behavior to support “negative caching” (avoid repeated retries on failures) and adds tests around this.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
services/mcp/tests/unit/tool-catalog.test.ts Adds unit coverage for ToolCatalog warmup and filtering behavior.
services/mcp/tests/unit/StateManager.test.ts Adds tests for stale/negative caching behavior via getOrFetchGroupTypes.
services/mcp/tests/integration/manifest.test.ts Refactors integration test to use new resource internals helpers.
services/mcp/src/resources/ui-apps.ts Avoids repeated warning spam when MCP_APPS_BASE_URL is missing.
services/mcp/src/resources/internals.ts New module that fetches/caches/unzips resources and loads/filters manifest entries.
services/mcp/src/resources/index.ts Refactors resource registration to use internals helpers and shared entry-registration helper.
services/mcp/src/lib/StateManager.ts Changes stale-cache decision logic and adds negative-caching timestamp on fetch failure.
services/mcp/src/hono/tool-catalog.ts New warmup-able catalog that pre-builds tools and filters them quickly per request.
services/mcp/src/hono/streamable-handler.ts Adds handler warmup (tools + resources) and passes warmup data into HonoMcpServer.
services/mcp/src/hono/request-utils.ts Adds unconditional console error logging in handleCatchError.
services/mcp/src/hono/mcp-server.ts Uses warmup-provided tool catalog/resources and adds init profiling logs + tool resolution refactor.
services/mcp/src/hono/index.ts Calls app warmup before serving requests.
services/mcp/src/hono/app.ts Exposes a warmup() function from createApp().
Comments suppressed due to low confidence (1)

services/mcp/src/hono/request-utils.ts:93

  • handleCatchError now logs the raw error to stderr for every caught exception. This can be noisy (especially for expected/auth-related failures) and may inadvertently include sensitive details in logs. Prefer relying on the existing captureException (or log only a sanitized message / behind a debug flag).
export function handleCatchError(error: unknown, props: RequestProperties): Response {
    console.error('[handleCatchError]', error)
    const authResponse = mapErrorToAuthResponse(error)
    if (authResponse) {
        return authResponse
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread services/mcp/src/lib/StateManager.ts
Comment on lines +379 to +381
const _lap = (label: string): void => {
const elapsed = performance.now() - _t0
console.log(`[init-profile] ${label.padEnd(40)} +${(elapsed).toFixed(0)}ms`)
try {
const { fetchContextMillResources, filterValidEntries, loadManifestFromArchive, clearResourceCache } =
await import('@/resources/internals')
const archive = await fetchContextMillResources()
import {
type ToolDefinition,
type ToolFilterOptions,
getToolDefinition,
Comment on lines +482 to +487
it('should return undefined (not stale data) when fetch succeeds then later fails', async () => {
const mockGroupTypes = [{ group_type: 'company', group_type_index: 0 }]
const getGroupTypes = vi.fn()
.mockResolvedValueOnce(mockGroupTypes)
.mockRejectedValueOnce(new Error('API error'))
const mockApi = stateManager as any
Comment on lines +521 to +526
// Register prompts and resources — use pre-parsed entries when available
await registerPrompts(this.server)
if (this._warmup?.resourceEntries.length) {
const { registerResourceEntries } = await import('@/resources')
registerResourceEntries(this.server, this._warmup.resourceEntries)
} else {
Copy link
Copy Markdown
Contributor

@skoob13 skoob13 left a comment

Choose a reason for hiding this comment

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

Looks good, stamping. From a high overview, I still couldn't understand where the bottleneck is, if it's not context-mill or feature flags. I think those two were the biggest latency issues.

Comment on lines +521 to +530
// Register prompts and resources — use pre-parsed entries when available
await registerPrompts(this.server)
if (this._warmup?.resourceEntries.length) {
const { registerResourceEntries } = await import('@/resources')
registerResourceEntries(this.server, this._warmup.resourceEntries)
} else {
await registerResources(this.server, context)
}
await registerUiAppResources(this.server, context)
_lap('registerPrompts + registerResources')
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.

Shouldn't it happen in parallel?

Comment on lines 419 to 421
flagPromise,
toolFlagsPromise,
singleExecPromise,
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.

I'm sure this is the biggest latency chunk. I have some work on that but decided to drop it back then because people yolo use properties on flags. Perhaps not a big deal if we only support a subset of props for local evaluation.

@joshsny joshsny merged commit d870696 into master May 20, 2026
168 checks passed
@joshsny joshsny deleted the mcp/fix-initialisation branch May 20, 2026 17:08
@deployment-status-posthog
Copy link
Copy Markdown

deployment-status-posthog Bot commented May 20, 2026

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-05-20 17:50 UTC Run
prod-us ✅ Deployed 2026-05-20 18:03 UTC Run
prod-eu ✅ Deployed 2026-05-20 18:04 UTC Run

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