Skip to content

fix(mcp): guide model to refresh tool catalog when MCP server is missing#63

Merged
vi70x4 merged 6 commits into
mainfrom
fix/mcp-missing-server-ux
Jun 30, 2026
Merged

fix(mcp): guide model to refresh tool catalog when MCP server is missing#63
vi70x4 merged 6 commits into
mainfrom
fix/mcp-missing-server-ux

Conversation

@vi70x4

@vi70x4 vi70x4 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • MCP "server not running" error now lists running servers and instructs model to re-call builtIn_mcpListTools
  • builtIn_mcpCallTool catch block appends corrective guidance so model retries with a refreshed tool instead of falling back to manual file generation
  • Fixes the UX flaw where model called unavailable tool, got opaque error, and abandoned the tool-call path

Test coverage

Adds 29 unit tests, full coding-workspace coverage now at 63 tests:

  • coding workspace store lifecycle, backend state matrix, workspace root trimming, concurrent enable/disable leak
  • CodingWorkspaceControls.vue visibility, mode badges, native-only UI
  • Serena preset insertion and duplicate-server guard
  • workspace_ranked_context fallback, spec artifact path defaulting, mode gating, JSON parse error shaping, symbols/diagnostics coverage

Verification

  • pnpm -F @proj-airi/stage-ui exec vitest run src/coding-workspace: 38 passed
  • pnpm exec vitest run apps/stage-tamagotchi/src/renderer/stores/coding-workspace.test.ts apps/stage-tamagotchi/src/renderer/stores/mcp-tools.test.ts apps/stage-tamagotchi/src/renderer/pages/settings/modules/mcp.vue.serena.test.ts apps/stage-tamagotchi/src/renderer/components/coding-workspace/CodingWorkspaceControls.test.ts: 25 passed
  • @proj-airi/stage-ui vue-tsc --noEmit: passed
  • @proj-airi/stage-tamagotchi vue-tsc --noEmit: passed

Manual QA

  • Verified MCP error path now lists running servers and includes listMcpTools retry instruction
  • Confirmed no behavioral change to happy-path tool routing or spec mode state machine

🤖 Generated with Factory

vi70x3 added 5 commits June 30, 2026 00:00
Summarizes the AIRI-native coding workspace architecture, Serena/MCP code intelligence, guarded workspace tools, and optional ACP subprocess engines for Pi/Codex.

Tests: rg -n "TBD|TODO|PLACEHOLDER|FIXME|\?\?" docs/architecture/coding-workspace.md
Tests: sed -n '1,420p' docs/architecture/coding-workspace.md

Follow-up: review and resolve open architecture questions before implementation planning.
Records decisions from spec review: coding controls stay inside the vanilla chat flow and remain unobtrusive, coding state is scoped to coding-enabled chat sessions, and Serena is offered as a predefined MCP settings template with uv/setup guidance rather than auto-installed.

Tests: rg -n "TBD|TODO|PLACEHOLDER|FIXME|\?\?" docs/architecture/coding-workspace.md
Tests: sed -n '100,240p' docs/architecture/coding-workspace.md
Tests: sed -n '350,520p' docs/architecture/coding-workspace.md

Follow-up: decide Architect write approvals and first ACP backend.
Verification:
- rg -n "TBD|TODO|PLACEHOLDER|FIXME|\?\?" docs/specs/coding-workspace-spec-mode-and-subagents/requirements.md
- sed -n '1,260p' docs/specs/coding-workspace-spec-mode-and-subagents/requirements.md
- git diff --cached -- docs/specs/coding-workspace-spec-mode-and-subagents/requirements.md
- git status --short
Verification:
- rg -n "TBD|TODO|PLACEHOLDER|FIXME|\\?\\?" docs/architecture/coding-workspace.md docs/specs/coding-workspace-spec-mode-and-subagents/requirements.md docs/specs/coding-workspace-swarm-runtime/requirements.md
- rg -n "Architect mode|### Architect|all Ask and Architect|Which ACP backend|initial engine identifiers" docs/architecture/coding-workspace.md docs/specs/coding-workspace-spec-mode-and-subagents/requirements.md docs/specs/coding-workspace-swarm-runtime/requirements.md
- sed -n "1,280p" docs/specs/coding-workspace-swarm-runtime/requirements.md
- git diff --cached --stat
- git diff --cached -- docs/architecture/coding-workspace.md docs/specs/coding-workspace-spec-mode-and-subagents/requirements.md docs/specs/coding-workspace-swarm-runtime/requirements.md
- git status --short
Verification:
- rg -n "TBD|TODO|PLACEHOLDER|FIXME|\\?\\?|fill in|implement later|appropriate error handling|Similar to" docs/specs/coding-workspace-spec-mode-and-subagents/tasks.md
- sed -n "1,460p" docs/specs/coding-workspace-spec-mode-and-subagents/tasks.md
- git diff --cached --stat
- git diff --cached -- docs/specs/coding-workspace-spec-mode-and-subagents/tasks.md
- git status --short
@deepsource-io

deepsource-io Bot commented Jun 30, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in daf9605...26f177d on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

Important

Some issues found as part of this review are outside of the diff in this pull request and aren't shown in the inline review comments due to GitHub's API limitations. You can see those issues on the DeepSource dashboard.

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
JavaScript Jun 30, 2026 4:09p.m. Review ↗
Shell Jun 30, 2026 4:09p.m. Review ↗
C & C++ Jun 30, 2026 4:09p.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

@@ -0,0 +1,59 @@
// @vitest-environment jsdom

import { describe, expect, it, vi } from 'vitest'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

'vi' is defined but never used


Unused variables are generally considered a code smell and should be avoided.

})

it('registers coding tools and prompt contributions only while coding context is enabled', async () => {
const callMcpTool = vi.fn(async (input) => ({ toolResult: { input } }))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found `async` function without any `await` expressions


A function that does not contain any await expressions should not be async (except for some edge cases in TypeScript which are discussed below). Asynchronous functions in JavaScript behave differently than other functions in two important ways:

Comment on lines +35 to +38
const listMcpTools = vi.fn(async () => [
{ serverName: 'serena', name: 'serena::find_symbol', toolName: 'find_symbol' },
{ serverName: 'serena', name: 'serena::search_for_pattern', toolName: 'search_for_pattern' },
])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found `async` function without any `await` expressions


A function that does not contain any await expressions should not be async (except for some edge cases in TypeScript which are discussed below). Asynchronous functions in JavaScript behave differently than other functions in two important ways:

// -- New tests --
describe('runtime behavior', () => {
// helper: enable the workspace with a default mock runtime and return both tools + status tool
async function enable(tools: any[] = []) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unexpected any. Specify a different type


The any type can sometimes leak into your codebase. TypeScript compiler skips the type checking of the any typed variables, so it creates a potential safety hole, and source of bugs in your codebase. We recommend using unknown or never type variable.

describe('runtime behavior', () => {
// helper: enable the workspace with a default mock runtime and return both tools + status tool
async function enable(tools: any[] = []) {
const callMcpTool = vi.fn(async (input) => ({ toolResult: { input }, called: input }))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found `async` function without any `await` expressions


A function that does not contain any await expressions should not be async (except for some edge cases in TypeScript which are discussed below). Asynchronous functions in JavaScript behave differently than other functions in two important ways:

Comment on lines +397 to +404
async listMcpTools() {
return [
{ serverName: 'serena', name: 'serena::get_symbols_overview', toolName: 'get_symbols_overview' },
{ serverName: 'serena', name: 'serena::find_symbol', toolName: 'find_symbol' },
{ serverName: 'serena', name: 'serena::get_diagnostics_for_file', toolName: 'get_diagnostics_for_file' },
{ serverName: 'serena', name: 'serena::search_for_pattern', toolName: 'search_for_pattern' },
]
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found `async` function without any `await` expressions


A function that does not contain any await expressions should not be async (except for some edge cases in TypeScript which are discussed below). Asynchronous functions in JavaScript behave differently than other functions in two important ways:

const tools = await createCodingWorkspaceTools(runtime)
const symbolsOverview = tools.find((tool) => tool.function.name === 'workspace_get_symbols_overview')

const result = await symbolsOverview?.execute({ relativePath: 'src/runtime.ts' }, toolExecutionContext)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

'toolExecutionContext' was used before it was defined


Variables, functions and types should always be used after they've been defined. This issue will flag any code snippets that use variables or types before definition.

Comment on lines +424 to +431
async listMcpTools() {
return [
{ serverName: 'serena', name: 'serena::get_symbols_overview', toolName: 'get_symbols_overview' },
{ serverName: 'serena', name: 'serena::find_symbol', toolName: 'find_symbol' },
{ serverName: 'serena', name: 'serena::get_diagnostics_for_file', toolName: 'get_diagnostics_for_file' },
{ serverName: 'serena', name: 'serena::search_for_pattern', toolName: 'search_for_pattern' },
]
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found `async` function without any `await` expressions


A function that does not contain any await expressions should not be async (except for some edge cases in TypeScript which are discussed below). Asynchronous functions in JavaScript behave differently than other functions in two important ways:

const tools = await createCodingWorkspaceTools(runtime)
const diagnostics = tools.find((tool) => tool.function.name === 'workspace_get_diagnostics')

const result = await diagnostics?.execute({ relativePath: 'src/runtime.ts' }, toolExecutionContext)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

'toolExecutionContext' was used before it was defined


Variables, functions and types should always be used after they've been defined. This issue will flag any code snippets that use variables or types before definition.

Comment on lines +481 to +485
updateSpecArtifact: vi.fn(async (input) => ({
artifactName: input.artifactName,
path: input.path,
content: input.content,
})),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found `async` function without any `await` expressions


A function that does not contain any await expressions should not be async (except for some edge cases in TypeScript which are discussed below). Asynchronous functions in JavaScript behave differently than other functions in two important ways:

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request establishes the architecture, requirements, and testing infrastructure for the AIRI-native Coding Workspace, introducing Spec mode, async subagents, and a swarm runtime. It also updates MCP tool error handling to provide actionable recovery instructions. The reviewer feedback correctly identifies that the error message in the MCP server manager refers to a non-existent listTools tool instead of the correct builtIn_mcpListTools identifier, which should be corrected to prevent model invocation failures.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +279 to +283
throw new Error(
runningServers.length === 0
? `MCP server "${serverName}" is not running. No MCP servers are currently running.`
: `MCP server "${serverName}" is not running. Running servers: ${runningServers.map((s) => `"${s}"`).join(', ')}. Call listTools exactly once to refresh available tools.`,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The error message instructs the model to call listTools to refresh available tools. However, the actual tool registered in the LLM context is named builtIn_mcpListTools (as defined in packages/stage-ui/src/tools/mcp.ts). Instructing the model to call listTools will cause it to attempt to invoke a non-existent tool, leading to further errors. The message should be updated to refer to builtIn_mcpListTools instead, which also aligns with the PR description ("instructs model to re-call builtIn_mcpListTools").

Suggested change
throw new Error(
runningServers.length === 0
? `MCP server "${serverName}" is not running. No MCP servers are currently running.`
: `MCP server "${serverName}" is not running. Running servers: ${runningServers.map((s) => `"${s}"`).join(', ')}. Call listTools exactly once to refresh available tools.`,
)
throw new Error(
runningServers.length === 0
? `MCP server "${serverName}" is not running. No MCP servers are currently running.`
: `MCP server "${serverName}" is not running. Running servers: ${runningServers.map((s) => `"${s}"`).join(', ')}. Call builtIn_mcpListTools exactly once to refresh available tools.`,
)

When the model calls an MCP tool whose server is not running (the user
appended tool references from stale context or hallucinated a new
server), the response now:

- surfaces the list of currently-running MCP servers so the model can
  pick a real one instead of concluding the tool does not exist
- appends an explicit instruction to re-call builtIn_mcpListTools and
  retry with a tool from the refreshed catalog

This stops the model from falling back to manual file generation or
abandoning the tool-call path when a transient server lookup fails.

Also adds 29 unit tests covering the previously-untested gaps:
- coding workspace store lifecycle, backend state matrix, workspace
  root trimming, concurrent enable/disable registration leak
- CodingWorkspaceControls.vue visibility, mode badges, native-only UI
- Serena preset insertion and duplicate-server guard at the settings
- workspace_ranked_context fallback, spec artifact path defaulting,
  mode gating, JSON parse error shaping, symbols/diagnostics coverage

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@vi70x4 vi70x4 force-pushed the fix/mcp-missing-server-ux branch from dfaa9cf to 26f177d Compare June 30, 2026 16:09
})

it('registers coding tools and prompt contributions only while coding context is enabled', async () => {
const callMcpTool = vi.fn(async (input) => ({ toolResult: { input } }))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found `async` function without any `await` expressions


A function that does not contain any await expressions should not be async (except for some edge cases in TypeScript which are discussed below). Asynchronous functions in JavaScript behave differently than other functions in two important ways:

Comment on lines +34 to +37
const listMcpTools = vi.fn(async () => [
{ serverName: 'serena', name: 'serena::find_symbol', toolName: 'find_symbol' },
{ serverName: 'serena', name: 'serena::search_for_pattern', toolName: 'search_for_pattern' },
])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found `async` function without any `await` expressions


A function that does not contain any await expressions should not be async (except for some edge cases in TypeScript which are discussed below). Asynchronous functions in JavaScript behave differently than other functions in two important ways:

Comment on lines +138 to +149
;async () => {
const callMcpTool = vi.fn(async (input: unknown) => ({ toolResult: { input }, called: input }))
const listMcpTools = vi.fn(async () => tools)
const store = useTamagotchiCodingWorkspaceStore()
const llmToolsStore = useLlmToolsStore()
store.setMcpRuntime({ callMcpTool, listMcpTools })
store.setActiveWorkspaceRoot('/repo/airi')
store.setCodingMode('code')
await store.setCodingContextEnabled(true)
const codingTools = llmToolsStore.toolsByProvider['coding-workspace'] ?? []
return { store, callMcpTool, listMcpTools, codingTools, llmToolsStore }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found unused expression


An unused expression that does not affect the state of the program indicates a logic error.

Comment on lines +138 to +149
;async () => {
const callMcpTool = vi.fn(async (input: unknown) => ({ toolResult: { input }, called: input }))
const listMcpTools = vi.fn(async () => tools)
const store = useTamagotchiCodingWorkspaceStore()
const llmToolsStore = useLlmToolsStore()
store.setMcpRuntime({ callMcpTool, listMcpTools })
store.setActiveWorkspaceRoot('/repo/airi')
store.setCodingMode('code')
await store.setCodingContextEnabled(true)
const codingTools = llmToolsStore.toolsByProvider['coding-workspace'] ?? []
return { store, callMcpTool, listMcpTools, codingTools, llmToolsStore }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Expected an assignment or function call and instead saw an expression


An unused expression that does not affect the state of the program indicates a logic error.

describe('runtime behavior', () => {
// helper: enable the workspace with a default mock runtime and return both tools + status tool
;async () => {
const callMcpTool = vi.fn(async (input: unknown) => ({ toolResult: { input }, called: input }))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found `async` function without any `await` expressions


A function that does not contain any await expressions should not be async (except for some edge cases in TypeScript which are discussed below). Asynchronous functions in JavaScript behave differently than other functions in two important ways:

// helper: enable the workspace with a default mock runtime and return both tools + status tool
;async () => {
const callMcpTool = vi.fn(async (input: unknown) => ({ toolResult: { input }, called: input }))
const listMcpTools = vi.fn(async () => tools)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found `async` function without any `await` expressions


A function that does not contain any await expressions should not be async (except for some edge cases in TypeScript which are discussed below). Asynchronous functions in JavaScript behave differently than other functions in two important ways:

Comment on lines +221 to +224
const listMcpTools = vi.fn(async () => [
{ serverName: 'serena', name: 'serena::find_symbol', toolName: 'find_symbol' },
{ serverName: 'serena', name: 'serena::search_for_pattern', toolName: 'search_for_pattern' },
])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found `async` function without any `await` expressions


A function that does not contain any await expressions should not be async (except for some edge cases in TypeScript which are discussed below). Asynchronous functions in JavaScript behave differently than other functions in two important ways:

{ serverName: 'serena', name: 'serena::find_symbol', toolName: 'find_symbol' },
{ serverName: 'serena', name: 'serena::search_for_pattern', toolName: 'search_for_pattern' },
])
const callMcpTool = vi.fn(async (input) => ({ toolResult: { input } }))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Found `async` function without any `await` expressions


A function that does not contain any await expressions should not be async (except for some edge cases in TypeScript which are discussed below). Asynchronous functions in JavaScript behave differently than other functions in two important ways:

@vi70x4 vi70x4 merged commit a4600e8 into main Jun 30, 2026
2 of 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.

2 participants