Skip to content

fix: graceful MCP degradation in Pi extension + ask-before-install skill wording - #48

Merged
EdwardIrby merged 6 commits into
mainfrom
fix/youcom-mcp-resilience
Jul 30, 2026
Merged

fix: graceful MCP degradation in Pi extension + ask-before-install skill wording#48
EdwardIrby merged 6 commits into
mainfrom
fix/youcom-mcp-resilience

Conversation

@EdwardIrby

Copy link
Copy Markdown
Member

Summary

Two related fixes around missing You.com MCP access:

1. Skill wording (skills/)

The fallback for unavailable MCP servers said "ask the user to enable" — passive, and it never distinguished asking permission to install from auto-installing. Updated you-discover, you-web, you-free, and you-finance to:

  • Tell the user which server/tools are missing
  • Ask whether they want to install or connect, providing server URL and auth requirements already listed in each skill
  • Point to the MCP setup mechanism of the user's current agent or MCP client (host-agnostic; no hardcoded CLI/config commands)
  • Never install or modify configuration without user approval

you-research is unchanged — it has no install/enable fallback, only docs canonical-page fallbacks.

2. Pi extension resilience (packages/pi/)

Pi has no native MCP configuration mechanism — the extension is the bridge, so the generic skill wording can't apply there. Two problems fixed:

  • All-or-nothing load failure: a missing YDC_API_KEY made createHeaders throw inside Promise.all, rejecting the whole extension load — including the keyless free-profile you-search and unauthenticated Docs MCP tools that would have worked. Now each server registers independently (Promise.allSettled + per-server try/catch); auth-missing servers skip quietly, unexpected failures are recorded and surfaced via console.error without aborting load.
  • Missing host context: on degraded registration, a before_agent_start handler appends Pi-specific context to the system prompt — naming which tools are unavailable and why (no YDC_API_KEY, or failure details per server group), directing the agent to ask the user rather than hunt for a nonexistent MCP config, and stating the ask-before-install safety boundary. Healthy registration appends nothing.

Also: rejected tool-discovery promises are evicted from discoveredToolsCache so a transient failure doesn't poison the cache for the process lifetime, and tool names in prompts derive from SERVER_CONFIGS instead of hardcoded lists.

Test plan

  • bun test in packages/pi — 14/14 pass, covering: happy path, graceful degradation without YDC_API_KEY, partial/total non-auth failures, mixed auth-skipped + failure context, cache eviction on rejection, healthy no-injection path
  • bun run --cwd packages/pi check — biome + tsc clean
  • bun test tests/validate-skills.spec.ts — 9/9 pass

Replace passive 'ask the user to enable' fallback wording with an explicit
install-or-connect offer across you-discover, you-web, you-free, and
you-finance:

- Tell the user which server or tools are missing
- Ask whether they want to install or connect, providing server URL and
  auth requirements already listed in each skill
- Point to the MCP setup mechanism of the user's current agent or MCP
  client, staying host-agnostic (no hardcoded CLI or config commands)
- State that configuration is never installed or modified without user
  approval, matching the existing recommendation policy and safety sections

you-research is unchanged: it has no install/enable fallback, only docs
canonical-page fallbacks.
@EdwardIrby
EdwardIrby requested a review from a team as a code owner July 29, 2026 19:47
@factory-droid

factory-droid Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Droid finished @EdwardIrby's task —— View job


Droid review complete. 3 findings validated and posted.

Summary: The Pi extension's graceful degradation logic is well-structured with correct Promise.allSettled handling, per-server try/catch, and cache eviction that avoids race conditions. The skill wording changes are mostly consistent across the 4 files, but the you-discover condition change from 'neither' to 'either' creates overlapping instructions with item 3, and its reference to 'auth requirement from the checks above' is inaccurate since the checks don't contain auth info. A defense-in-depth concern exists around unsanitized external error messages being interpolated into the system prompt.

Comment thread skills/you-discover/SKILL.md Outdated
2. Check whether the You.com Docs MCP tool `searchDocs` is available at `https://you.com/docs/_mcp/server`.
3. If `you-discover` is unavailable but Docs MCP is available, continue with docs-only planning and clearly state that catalog discovery was not available.
4. If neither discovery nor docs access is available, ask the user to enable the standard You.com MCP server or Docs MCP before recommending install steps.
4. If either server is unavailable, tell the user which server is missing and ask whether they want to install or connect it. Provide the server name, URL, and auth requirement from the checks above, and point to the MCP setup mechanism for their current agent or MCP client. Do not install or modify configuration without user approval.

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] "either" condition overlaps with item 3, creating ambiguous instructions

The change from "If neither discovery nor docs access is available" to "If either server is unavailable" makes item 4 trigger whenever any single server is missing. But item 3 already handles the case where you-discover is unavailable but Docs MCP is available ("continue with docs-only planning"). Now, when you-discover is missing but Docs MCP is present, both items fire: item 3 says to proceed with docs-only planning, while item 4 says to ask the user whether they want to install or connect the missing server. These create conflicting guidance (proceed vs. wait for user input). The other 3 skill files did not change their conditions, only the action wording. Consider reverting to "If neither server is available" or restructuring item 3 to incorporate the ask-to-install behavior.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Valid finding. The "either" widening made step 4 fire on the partial case that step 3 already handles, producing conflicting "proceed docs-only" vs "ask the user" guidance.

Restructured into exclusive cases in 794aff8:

  • 3. you-discover missing but Docs MCP present → docs-only planning and offer to install you-discover (offer is non-blocking, so no conflict)
  • 4. neither available → offer to install either
  • 5. shared setup guidance (name, URL, auth source, host setup mechanism, ask-before-install boundary)

Intent preserved: a missing you-discover alone still triggers the install offer, just without the conflicting "wait for user input" framing.

Comment thread skills/you-discover/SKILL.md Outdated
2. Check whether the You.com Docs MCP tool `searchDocs` is available at `https://you.com/docs/_mcp/server`.
3. If `you-discover` is unavailable but Docs MCP is available, continue with docs-only planning and clearly state that catalog discovery was not available.
4. If neither discovery nor docs access is available, ask the user to enable the standard You.com MCP server or Docs MCP before recommending install steps.
4. If either server is unavailable, tell the user which server is missing and ask whether they want to install or connect it. Provide the server name, URL, and auth requirement from the checks above, and point to the MCP setup mechanism for their current agent or MCP client. Do not install or modify configuration without user approval.

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.

[P3] "auth requirement from the checks above" references info not present in the checks

Item 4 instructs the agent to "Provide the server name, URL, and auth requirement from the checks above." The checks (items 1-2) contain server names and URLs only (you-discover at https://api.you.com/mcp and searchDocs at https://you.com/docs/_mcp/server) but no auth requirements. Auth info lives in the YAML frontmatter metadata.mcp_servers, not in the numbered checks. The other 3 skill files correctly reference "prerequisites above" and each has a ## Prerequisites section with auth details. Consider changing to "from the prerequisites and metadata above" or adding auth info to the checks.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Valid. Steps 1–2 carry names and URLs only; auth lives in the frontmatter metadata.mcp_servers, which you-discover does not surface in a ## Prerequisites section like the other three skills do.

Fixed in 794aff8: the shared setup-guidance step now reads "from the metadata.mcp_servers field in the frontmatter above" so the reference points at where auth actually is.

Comment thread packages/pi/main.ts Outdated

const formatFailureLines = (groups: FailedGroup[]): string[] => {
if (groups.length === 0) return []
return ['Failure details:', ...groups.map((g) => `- ${g.name} (${g.toolNames.join(', ')}): ${g.message}`)]

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] [security] Prompt injection via unsanitized external error messages in system prompt

Error messages from remote MCP servers are captured at line 215 (state.failedGroups.push({ name, toolNames: config.toolNames, message })) from error.message or String(error), then interpolated unsanitized into the AI agent's system prompt via formatFailureLines (line 249: `- ${g.name} (${g.toolNames.join(', ')}): ${g.message}`) and ultimately appended to event.systemPrompt at line 306. The MCP JSON-RPC protocol allows servers to include arbitrary text in error response message fields, which the SDK propagates into thrown Error objects. An attacker who can influence the error response content (through a compromised You.com MCP server, DNS hijacking, or TLS interception) could inject arbitrary instructions into the system prompt, which the AI agent treats as authoritative system-level directives. Unlike tool content (which is marked 'untrusted external data' in promptGuidelines), these error messages have no sanitization, length limits, or untrusted-content marking. Fix: sanitize or truncate error messages before including them in the system prompt, or mark them explicitly as untrusted diagnostic output.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Valid P2. Error text from remote MCP servers (JSON-RPC message fields) crosses a network→system-prompt trust boundary and was interpolated raw via formatFailureLines, unlike tool content which promptGuidelines already marks untrusted.

Fixed in 0609195:

  • sanitizeErrorMessage strips C0 control characters and DEL (by code point, so the biome/noControlCharactersInRegex lint stays clean), collapses all whitespace — including Unicode line/paragraph separators — to single spaces, and caps length at 160 chars. Injected text can no longer start a new prompt line or markdown header.
  • formatFailureLines now labels the failure-details section as untrusted external data and instructs the agent not to follow instructions within.

Test asserts a \n## Ignore all previous instructions payload cannot start its own prompt line and that the untrusted marker is present. 15/15 tests, biome, tsc all green.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Update: this finding prompted a scope rethink. The graceful-degradation machinery that interpolated error text into the system prompt has been reverted entirely (commit b6e6e01), so the injection surface no longer exists.

The extension now appends only static, trusted host context identifying the MCP adapter configs — no external data crosses into the system prompt, so there is nothing to sanitize. The original Promise.all + createHeaders behavior is retained; a missing YDC_API_KEY throws an explicit load error for authenticated variants, which is the honest failure mode. Finding 3 is therefore resolved by elimination.

@factory-droid

factory-droid Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Droid finished @EdwardIrby's task —— View job


Droid review complete. 2 findings validated and posted.

Summary: The PR's graceful degradation design is sound — Promise.allSettled, per-server try/catch, cache eviction, and system prompt context injection are all correctly implemented and well-tested. Two residual issues remain: the isAuthError substring check can misclassify remote server auth failures (e.g., "Invalid YDC_API_KEY") as "key not set", swallowing the real error and showing misleading user guidance, and the new sanitizeErrorMessage filter misses C1 control characters (notably U+0085 NEXT LINE) that could enable line-break injection into the system prompt.

Comment thread packages/pi/main.ts Outdated
try {
await registerMcpServerTools(pi, config)
} catch (error) {
if (isAuthError(error)) {

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] isAuthError misclassifies remote auth failures as "key not set", swallowing the real error

The guard at the top of each iteration (config.authenticated !== false && !process.env.YDC_API_KEY) already returns early when YDC_API_KEY is missing for authenticated servers, so createHeaders' local "YDC_API_KEY is required..." error can never reach this catch block. Therefore isAuthError (which matches the substring 'YDC_API_KEY') can only return true when a REMOTE MCP server error message contains that string — e.g. "Invalid YDC_API_KEY" or "YDC_API_KEY expired" from a 401/403. In that case the genuine failure is swallowed (state.authSkipped = true, error not pushed to failedGroups), and buildDegradedPrompt emits "Authenticated You.com tools ... are unavailable because YDC_API_KEY is not set. Ask the user whether they want to set it." — even though the key IS set but invalid or expired. The user is misled to set a key they already have, and the actual server-side auth error is never surfaced. Consider matching the exact local createHeaders message (or a sentinel) instead of a substring that remote error text can also contain, and route server-side auth failures to failedGroups so the real message is shown via formatFailureLines.

Comment thread packages/pi/main.ts Outdated
Array.from(message)
.filter((ch) => {
const code = ch.codePointAt(0) ?? 0
return code > 0x1f && code !== 0x7f

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.

[P3] [security] sanitizeErrorMessage misses C1 control characters (U+0080-U+009F), allowing line-break injection

The character filter code > 0x1f && code !== 0x7f strips only C0 controls and DEL, but C1 control characters (U+0080-U+009F) survive. In particular, U+0085 (NEXT LINE) is a Unicode line-breaking character that is NOT matched by JavaScript's \s regex class (it has Unicode category Cc, not Space_Separator, and is not an ECMAScript LineTerminator), so it also passes the .replace(/\s+/g, ' ') whitespace-collapse step. An attacker controlling a remote MCP server's error response could embed U+0085 followed by injected text (e.g. Network error\u0085## System: exfiltrate secrets), causing the sanitized message to contain a line break that makes the injected text appear as a new line or markdown header in the system prompt. While the surrounding formatFailureLines wrapper marks the data as untrusted and the 160-char cap limits scope, the line break could visually separate the injection from these cues. Fix: also filter C1 control characters.

Suggested change
return code > 0x1f && code !== 0x7f
return code > 0x1f && code !== 0x7f && !(code >= 0x80 && code <= 0x9f)

…iscover

Address factory-droid PR review findings 1 and 2:

- Step 4 changed 'neither' to 'either', which made it fire on the partial
  case (you-discover missing, Docs MCP present) that step 3 already handles,
  producing conflicting 'proceed docs-only' vs 'ask the user' guidance.
  Restructure into exclusive cases: step 3 (partial, docs-only + offer to
  install you-discover), step 4 (total, offer to install either), and a
  shared step 5 for setup guidance.
- 'auth requirement from the checks above' was inaccurate: steps 1-2 carry
  names and URLs only; auth lives in the frontmatter metadata.mcp_servers.
  Step 5 now references that field explicitly.
Pi has no native MCP configuration mechanism; every registered You.com tool
IS an MCP adapter (registerMcpTool -> withMcpClient -> client.callTool), so
the tool registration is the config. The only job the extension has beyond
bridging is identification: tell the agent which adapters are registered and
that there is no separate MCP setup mechanism to look for.

Register a before_agent_start handler that appends static, trusted host
context to the system prompt naming all four configs:
- you-search: free profile, no auth
- you-finance: YDC_API_KEY / OAuth / MPP-x402
- you-search / you-contents / you-research: YDC_API_KEY or OAuth
- searchDocs: no auth

This supersedes an earlier graceful-degradation approach that tracked
RegistrationState, failedGroups, and sanitized remote error text. That solved
a keyless-missing scenario not in scope here and introduced a prompt-
injection surface by interpolating untrusted remote error text into the
system prompt. Static context has no such boundary to cross. The original
Promise.all + createHeaders behavior is retained: a missing YDC_API_KEY
throws an explicit load error for authenticated variants.
@EdwardIrby
EdwardIrby force-pushed the fix/youcom-mcp-resilience branch from 0609195 to b6e6e01 Compare July 30, 2026 17:20
@factory-droid

factory-droid Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Droid finished @EdwardIrby's task —— View job


Droid is reviewing code and running a security check…


Droid review complete. 1 finding validated and posted.

Summary: The PR's final state is a clean, minimal change: a SERVER_CONFIGS refactor, a static HOST_CONTEXT string appended via before_agent_start, and consistent ask-before-install wording across four skill files. One minor factual inaccuracy remains in HOST_CONTEXT where you-search is listed under the authenticated base URL instead of only the keyless free-profile URL.

Comment thread packages/pi/main.ts
'Tool config:',
'- `you-search` (free profile, no auth): https://api.you.com/mcp?profile=free',
'- `you-finance` (YDC_API_KEY, OAuth, or MPP/x402): https://api.you.com/mcp?tools=you-finance',
'- `you-search` / `you-contents` / `you-research` (YDC_API_KEY or OAuth): https://api.you.com/mcp',

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.

[P3] HOST_CONTEXT lists you-search under the wrong URL and auth requirement

The third bullet of HOST_CONTEXT states `you-search` / `you-contents` / `you-research` (YDC_API_KEY or OAuth): https://api.you.com/mcp, but SERVER_CONFIGS registers you-search exclusively from https://api.you.com/mcp?profile=free with authenticated: false, and the base-URL config explicitly excludes it via includeTool: (tool) => tool.name !== 'you-search' && tool.name !== 'you-finance'. So you-search appears twice in the injected system prompt with conflicting information: the first bullet correctly says free/no-auth at the profile=free URL, while this bullet incorrectly says YDC_API_KEY/OAuth at the base URL. This is guidance text only, actual tool registration is correct, but it can mislead the agent into telling users they need YDC_API_KEY for you-search when it is keyless. Drop you-search from the third bullet so it lists only you-contents / you-research.

Suggested change
'- `you-search` / `you-contents` / `you-research` (YDC_API_KEY or OAuth): https://api.you.com/mcp',
'- `you-contents` / `you-research` (YDC_API_KEY or OAuth): https://api.you.com/mcp',

Comment thread skills/you-discover/SKILL.md Outdated
Comment on lines +22 to +23
3. If `you-discover` is unavailable but Docs MCP is available, continue with docs-only planning, clearly state that catalog discovery was not available, and ask whether the user wants to install or connect `you-discover` using the setup guidance in step 5.
4. If neither discovery nor docs access is available, ask whether the user wants to install or connect either server using the setup guidance in step 5.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I need a step here that allows the planning loop to take over explore the discovered resources before coming back to our docs.

Comment thread skills/you-discover/SKILL.md Outdated
4. If neither discovery nor docs access is available, ask the user to enable the standard You.com MCP server or Docs MCP before recommending install steps.
3. If `you-discover` is unavailable but Docs MCP is available, continue with docs-only planning, clearly state that catalog discovery was not available, and ask whether the user wants to install or connect `you-discover` using the setup guidance in step 5.
4. If neither discovery nor docs access is available, ask whether the user wants to install or connect either server using the setup guidance in step 5.
5. Setup guidance for a missing server: provide the server name, URL, and auth requirement from the `metadata.mcp_servers` field in the frontmatter above; point to the MCP setup mechanism for the user's current agent or MCP client; do not install or modify configuration without user approval.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This step is flawed as i read it. First class integration is the wrong thing here.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@factory-droid

factory-droid Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Droid encountered an error —— View job


Droid is reviewing code and running a security check…

…endpoints

The ?tools=you-finance and ?profile=free query params already scope
tools server-side, making the local includeTool filter redundant.

Rewrote main.spec.ts to use real MCP endpoints instead of mocks. The
mock didn't respect query-param filtering, so it couldn't prove the
server-side scoping works. Tests now verify no duplicate tool
registrations across endpoints.
@factory-droid

factory-droid Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Droid encountered an error —— View job


Droid is reviewing code and running a security check…

toToolResult was JSON.stringify-ing the entire MCP CallToolResult
(including content, structuredContent, isError, _meta) into a single
text block sent to the model. Since structuredContent mirrors content
on every You.com tool, the model received the same data twice.

Now passes the MCP content text blocks through directly as Pi content
(model-facing). The full raw result stays in details (UI/logs only,
never sent to provider). No JSON wrapping, no structuredContent
duplication.

Adds test against real free-profile endpoint verifying content blocks
are raw text, not a JSON wrapper containing structuredContent.

Co-authored-by: Edward Irby <edward@you.com>
@factory-droid

factory-droid Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Droid finished @EdwardIrby's task —— View job


Droid is reviewing code and running a security check…


Droid review complete. 1 finding validated and posted.

Summary: The PR's final state is a clean, minimal change: a SERVER_CONFIGS refactor with registerAs tool renaming, a static HOST_CONTEXT string appended via before_agent_start, consistent ask-before-install wording across five skill files, and a toToolResult change that passes MCP content blocks directly instead of JSON-wrapping. Production code is well-structured with no new security issues (the previous prompt-injection finding was resolved by reverting the degradation machinery). One validated finding: the test suite now requires a live YDC_API_KEY secret and network access for every test with no skip guard, which breaks forked-PR CI where GitHub Actions secrets are unavailable.


await extension(pi)
describe('tool registration', () => {
test('registers bundled skills via resources_discover', 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.

[P1] All tests hard-fail (not skip) when YDC_API_KEY is absent — no skip guard for external dependencies

Every test calls await extension(pi), which calls registerMcpToolsPromise.all over 4 MCP server configs (2 of which are authenticated). In createHeaders, if (authenticated && !process.env.YDC_API_KEY) throws when the key is empty or undefined. Since Promise.all propagates the first rejection, the entire extension(pi) call rejects, failing every test — including registers bundled skills via resources_discover, which only verifies resources_discover registration and has nothing to do with MCP endpoints. GitHub Actions secrets (including YDC_API_KEY) are not available to PRs from forks, so the key would be empty string in forked-PR CI, causing the entire test file to fail with no skip mechanism. The old tests used mocks and ran anywhere; the new tests require both a real API key and network access to api.you.com/you.com with no fallback. Consider adding test.skipIf(!YDC_API_KEY) to the tests that require authenticated endpoints, or at minimum guarding the extension(pi) call.

@EdwardIrby
EdwardIrby merged commit 62bb90d into main Jul 30, 2026
5 checks passed
@EdwardIrby
EdwardIrby deleted the fix/youcom-mcp-resilience branch July 30, 2026 19:45
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