VS MCP Bridge Blog Series: Part 3

Host Correctness, Proposal State, and UI Thread Safety

Part 2 explained that useful AI-assisted development is not a single prompt turning into a single command. It is a workflow: the AI may read context, propose an edit, wait for approval, and then report a result.

Part 3 focuses on what keeps that workflow correct inside Visual Studio.

The short version is that transport can be asynchronous, but Visual Studio access, UI state, and proposal lifecycle state must be owned deliberately. The bridge became easier to test and reason about when proposal state stopped being incidental UI behavior and became an explicit lifecycle managed through IProposalManager.

Transport Work Is Not UI Work

The local bridge has background work by design. The MCP server receives requests over stdio. VS-backed requests cross the named-pipe boundary. The pipe server accepts and dispatches requests without requiring the UI thread to wait for transport input.

That part is healthy:

MCP request arrives
  -> named-pipe request is dispatched
  -> host service handles the operation
  -> result returns through the bridge

But Visual Studio API access is different. DTE, editor state, tool window state, and view-model updates have host threading rules. The bridge has to separate background request handling from UI-thread-sensitive work.

Visual Studio Host Correctness

The VSIX is the Visual Studio host. It owns Visual Studio APIs, editor state, proposal application, and tool-window interaction. The MCP server must not bypass that host boundary.

That gives the system a practical rule:

background transport can be asynchronous
Visual Studio work must happen through the host boundary
UI state must be updated through the UI orchestration path

This keeps the bridge from turning an AI tool call into arbitrary background mutation of Visual Studio state.

The Tool Window Is State, Not Just Display

The VS MCP Bridge tool window is not only a place to print logs. It is also the human review surface for proposal entry, pending approval, completed proposal previews, status messages, and reset/new-chat behavior.

That means the view model has meaningful workflow state:

  • ProposalFilePath for the current target file,
  • selected proposal files for multi-file proposals,
  • pending approval description and reviewed changes,
  • included files for pending and completed proposal review,
  • last completed proposal preview data,
  • terminal status messages,
  • reset and new-chat command state.

If that state is updated from the wrong place, the UI becomes hard to reason about. Worse, approval callbacks can outlive the proposal they were meant to control.

Why IProposalManager Matters

IProposalManager is the seam that keeps proposal lifecycle ownership out of the general presenter path.

The presenter still orchestrates the tool window, logs, and host-facing interaction. But proposal-specific lifecycle behavior belongs in the proposal manager:

  • showing an approval prompt,
  • recording pending approval state,
  • capturing completed proposal preview state,
  • clearing stale approval callbacks,
  • resetting current request state,
  • starting a new chat/session cleanup path,
  • reacting to ProposalFilePath changes.

That separation matters because proposal lifecycle is not just UI decoration. It defines when a human can approve, what proposal is being approved, what preview remains after completion, and what state must be cleared before a new request.

Approval Prompt State and Callbacks

An approval prompt carries more than a yes/no question. It carries the proposal description, original and updated content, reviewed ranges, included files, and callbacks for approve or reject.

The dangerous case is stale state. If an old callback remains available after a proposal completes, the UI can look idle while old approval behavior is still reachable. The current lifecycle avoids that by making terminal outcomes drive cleanup:

  1. A proposal reaches pending review.
  2. The view model shows the pending approval surface.
  3. The operator approves or rejects.
  4. The proposal manager captures the completed preview state.
  5. Pending approval state and callbacks are cleared.
  6. The proposal entry state is refreshed from the current file path when appropriate.

That is host correctness at the UI boundary. It prevents one proposal's approval surface from silently leaking into the next one.

Drafts, File Selection, and Manual Submission

The proposal entry path also has to support ordinary operator behavior. The user may type a file path manually, use a host-specific picker, load a file into the proposal panes, edit proposed text, and then submit.

The key design point is that there is a single authoritative load path for proposal entry. Picker selection flows through ProposalFilePath rather than creating a second hidden workflow. That keeps manual entry and picker-driven entry aligned.

When ProposalFilePath is valid, the proposal panes can populate. The original pane remains read-only. The proposed pane remains editable until submission. After submission, the review surface owns the pending decision.

Completed Proposal Preview Capture

Completed proposal preview state is intentionally retained after terminal outcomes. The operator should be able to see what just happened, even after approval, rejection, skip, drift failure, ambiguity failure, or generic failure.

That completed preview is separate from pending approval state. Pending approval answers “what can I approve now?” Completed preview answers “what just happened?”

Keeping those separate made the tool window easier to reason about. It also made terminal outcomes more diagnosable because the UI does not collapse all context at the moment the workflow completes.

Reset and New-Chat Cleanup

The reset and new-chat paths are part of proposal correctness.

A reset should clear current proposal/request state without pretending the whole application restarted. A new-chat cleanup should clear session-shaped state so the next interaction is not contaminated by stale prompt, approval, or preview context.

That is why reset/new-chat handlers are wired through the view model into the proposal manager. The UI can expose commands, but proposal lifecycle ownership stays centralized.

How This Improved Testability

Before the proposal manager seam, proposal behavior was easier to treat as presenter side effect. That made the workflow harder to test because UI orchestration, approval state, and lifecycle cleanup were tightly blended.

Moving proposal lifecycle behavior behind IProposalManager made tests more focused:

  • pending approval state can be checked directly,
  • completed preview capture can be verified separately,
  • reset behavior can be exercised without re-running transport setup,
  • file-path changes can be tested as proposal lifecycle events,
  • callbacks can be checked for terminal cleanup.

That is the same anti-black-box theme from Part 2, applied to UI state. If a workflow is important enough to approve or apply code, it should be testable without guessing what the UI happened to remember.

Relationship to Approval-Aware Execution

There are now two approval concepts in the architecture, and they should stay distinct.

  • The proposal approval workflow is the Visual Studio host workflow for reviewing and applying editor changes.
  • The approval-aware tool execution seam is part of BridgeToolExecutor for shared compiled tools whose descriptors require approval before execution.

They share a philosophy: selected actions should stop at an explicit decision boundary. But they are not the same implementation. Part 3 is primarily about the host/UI proposal lifecycle. The compiled-tool approval seam belongs to the shared executor boundary.

Logs and Traces Still Matter

Thread correctness and proposal lifecycle correctness are hard to validate from screenshots alone. Logs and trace artifacts help show where the workflow crossed from transport to host service to UI state.

For proposal work, the useful evidence is not just “the button was clicked.” It is the chain:

proposal request received
proposal loaded into review state
approval or rejection invoked
completed preview captured
pending callbacks cleared
terminal status recorded

That chain is what makes async UI behavior observable instead of mysterious.

Related Mermaid Trace Sources

The repo already has Mermaid sources that help explain the surrounding workflows:

Those .mmd files are the diagram source of truth. This post references them directly rather than embedding generated images.

Takeaway

For the current bridge, host correctness means more than switching to the UI thread at the right time. It means keeping transport, Visual Studio access, UI orchestration, and proposal lifecycle ownership separate.

The working model is:

transport can run asynchronously
host access stays behind the VSIX/service boundary
UI state changes through presenter/viewmodel orchestration
proposal lifecycle belongs to IProposalManager
approval callbacks are cleared at terminal outcomes
completed previews preserve what just happened

That separation reduced black-box behavior. It made proposal workflows easier to test, easier to reset, and easier to explain when an AI-assisted edit crosses from suggestion into human-reviewed action.

Next In The Series

The next useful topic is runtime validation: how to prove MCP tool calls, named-pipe dispatch, proposal creation, approval flow, and diagnostics work end to end in the real host environment.

VS MCP Bridge Blog Series: Part 2

Observable AI-Assisted Workflows

The first post in this series explained how VS MCP Bridge starts: Visual Studio loads the VSIX, the VSIX owns the named-pipe side, and the MCP server waits on stdio for tool calls from an AI client.

This post moves one level higher. Once the bridge is running, what makes an AI-assisted workflow understandable, safe to evolve, and debuggable?

The answer is not just “more tools.” The real lesson from the project is that observability became architecture. Sequence diagrams, request IDs, approval states, durable trace artifacts, and focused logs changed how the system was understood and refactored.

Prompt, Tool Call, Result, Action

The bridge still starts with a basic distinction:

  • A prompt is the user's natural-language request.
  • A tool call is a structured operation the AI chooses to invoke.
  • A tool result is the structured response the bridge returns.
  • A completed action is the final user-facing outcome, which may require approval or additional host work.

Those are separate things. The user talks in prompts. The AI decides whether tool use is needed. The bridge handles explicit tool and host boundaries.

That distinction is why the bridge does not behave like a generic chat box inside Visual Studio. It is an AI-facing capability layer that exposes controlled access to IDE state and approval-gated actions.

From One Prompt to a Small Workflow

A single user prompt can become a short workflow.

For example:

Please fix the selected code.

A reasonable AI-assisted flow might be:

  1. Call vs_get_selected_text.
  2. Call vs_get_active_document if file path or surrounding context is needed.
  3. Generate a proposed change.
  4. Call vs_propose_text_edit or vs_propose_text_edits.
  5. Tell the user that a proposal was created and approval is still required.

That is already more than request/response plumbing. It is a model-driven workflow with read steps, reasoning, proposal creation, human approval, and final outcome reporting.

Why Approval Is Part of the Workflow

Read-only tools can usually return data directly. Edit-oriented tools are different.

In the current bridge, MCP can propose edits, but apply still happens only after explicit approval in the host UI. The VSIX proposal workflow owns review, approval, rejection, apply, rollback behavior, and terminal outcome messages.

The short version is still:

MCP proposes
Visual Studio reviews
VSIX applies only after approval

That separation lets AI suggest useful changes without silently mutating the IDE. It also creates a natural diagnostic checkpoint: if something goes wrong, the system can distinguish proposal creation, approval state, apply validation, apply execution, and final result.

Approval-Aware Execution Beyond Proposals

The project now also has an approval-aware execution seam for shared compiled bridge tools. That is separate from the Visual Studio proposal approval workflow.

A compiled tool descriptor can declare that approval is required. When it does, BridgeToolExecutor asks IToolExecutionApprovalService for a decision before execution. An approved decision runs the tool. A denied decision returns a structured ApprovalDenied result and records audit metadata without calling the tool.

That seam matters because it keeps approval inside the execution boundary rather than scattering ad hoc checks across tools.

Why Logs Changed the Architecture

Early bridge work could answer “did it work?” only by looking at the visible behavior. That was not enough. AI-assisted development needs stronger evidence because the operator, the model, the MCP server, the VSIX, and the host UI can all be involved in a single turn.

The project evolved toward boundary-focused logging:

  • tool execution started and completed,
  • request and operation IDs preserved,
  • pipe client attempts recorded,
  • VSIX dispatch boundaries logged,
  • approval decisions captured,
  • payload-oriented logs redacted,
  • audit envelopes emitted for terminal outcomes.

Those logs were not cosmetic. They exposed where responsibilities were unclear. Once the traces made boundaries visible, the architecture could be refactored around those boundaries.

How Mermaid Diagrams Helped

Mermaid sequence diagrams became a practical design tool for the repo. A diagram forced each workflow to answer concrete questions:

  • Who initiated the request?
  • Which process handled it?
  • Which boundary did it cross?
  • Where was approval checked?
  • Where was the result recorded?
  • What evidence would remain if the workflow failed?

That discipline made the system less black-box. It also made later AI sessions safer, because future work could resume from durable traces instead of reconstructing intent from chat history.

Durable Artifacts as Part of the System

The current repo now preserves trace artifacts for important paths. These are not production telemetry systems. They are durable development evidence: logs, metadata, and Mermaid source files that show how a workflow behaves.

That evidence changed the way new work is judged. A feature is stronger when the repo can show:

  • the entry point,
  • the policy or approval decision,
  • the execution boundary,
  • the redaction boundary,
  • the audit envelope,
  • the correlation IDs that connect the steps.

In other words, traceability is now part of the architecture direction, not a debugging afterthought.

Relationship Between UI, MCP, and Execution Boundaries

The current architecture has several boundaries that cooperate without becoming the same thing:

  • The MCP server exposes explicit tools over stdio.
  • The named pipe carries VS-backed requests into the VSIX.
  • The VSIX tool window provides the human review and diagnostics surface.
  • The proposal workflow owns Visual Studio edit approval and apply behavior.
  • BridgeToolExecutor owns policy, approval, execution, audit, redaction, and correlation for shared compiled tools.

Keeping those responsibilities separate is what makes the system explainable. The AI can drive tool workflows, but it does not bypass the VSIX UI approval surface or the compiled-tool execution boundary.

Related Mermaid Trace Sources

The repo already has Mermaid sources that support this part of the series:

Those .mmd files are the diagram source of truth. This post references them directly rather than embedding generated images.

The Larger Lesson

The important project evolution was not just that VS MCP Bridge gained more tool paths. It gained better visibility into its own behavior.

That visibility changed engineering decisions:

  • logs made transport boundaries concrete,
  • diagrams made workflow assumptions testable,
  • approval traces clarified where execution could stop safely,
  • redaction traces showed where sensitive payloads needed protection,
  • activation diagnostics turned a timeout into an operator action.

That is why observable AI tooling is a core theme for the project. The bridge is not only a way for AI to call Visual Studio. It is a case study in making AI-assisted workflows inspectable enough to trust and evolve.

Takeaway

If Part 1 explained how the bridge starts, Part 2 explains how the bridge becomes usable: prompts become tool workflows, tool workflows produce traceable evidence, and approval boundaries keep AI-assisted action under host control.

The working model is:

prompt
  -> AI reasoning
  -> explicit tool call
  -> observable bridge boundary
  -> result, proposal, approval decision, or structured failure
  -> final user-facing answer

That is a more accurate mental model than one prompt mapping to one command. In VS MCP Bridge, useful AI-assisted development is a traceable workflow.

Next In The Series

The next useful topic is how the bridge keeps those workflows host-correct over time, especially when Visual Studio threading, UI state, proposal review, and asynchronous operations are all involved.

VS MCP Bridge Blog Series: Part 1

VS MCP Bridge

Source of Truth: docs/ARCHITECTURE.md
Status: Canonical repo cleanup aligned to the current architecture as of 2026-05-16. Bracket-style tokens are intentional BlogEngine/GwnWikiExtension tokens.

VS MCP Bridge Blog Series: Part 1

From VSIX Startup to the First MCP Tool Call

This post is the first in a short developer ramp-up series for the VS MCP Bridge project. Its job is to make the startup flow understandable before diving into individual tools, proposal approval, diagnostics, and security seams.

The key question is simple: what actually happens from the moment Visual Studio loads the VSIX until an AI client can make a useful MCP tool call?

The Short Version

The bridge has two local process boundaries, not one.

  • The VSIX runs inside Visual Studio and owns Visual Studio APIs, editor state, proposal approval, and the local Named Pipe Listener.
  • The MCP server runs as a separate local process and waits on Stdio for MCP protocol messages from an AI client.
  • The MCP server exposes a small explicit tool surface. For VS-backed tools, it forwards typed requests to the VSIX over the named pipe.
  • The VSIX dispatches only known bridge commands, performs the Visual Studio-side operation, and sends a structured response back.

The important mental model is that the VSIX is not a chat endpoint. It waits for structured bridge requests. Natural-language reasoning happens in the AI client; bridge execution happens through explicit tool and command boundaries.

Step 1: Visual Studio Loads the VSIX Package

The Visual Studio entry point is the package class, VsMcpBridgePackage. It is an AsyncPackage that composes VSIX services and registers the command that opens the bridge tool window.

At startup, the package path is responsible for four important things:

  1. Registering the command that can open the bridge tool window.
  2. Building the dependency injection container.
  3. Composing shared and VSIX-specific services.
  4. Starting the named-pipe side of the local bridge when the VSIX is initialized.

This is the first major clarification for new developers: Visual Studio work stays in the VSIX. The MCP-facing process does not load inside Visual Studio and does not own DTE or editor access.

Step 2: Dependency Injection Assembles the Runtime

The package itself stays relatively thin. Most behavior is composed through dependency injection so the Visual Studio host and standalone app host can share the same infrastructure where that makes sense.

During startup, the service collection registers components such as:

  • logging and unhandled exception capture
  • configuration-backed host services
  • proposal approval state
  • Visual Studio service access
  • edit proposal and apply services
  • tool window presenter and view model
  • named pipe server

That means the startup path can feel indirect in the debugger unless you remember that the package is mainly a composition root.

Step 3: The VSIX Listens on a Named Pipe

Once the pipe server is available, it listens on the fixed local pipe name VsMcpBridge. That pipe is the handoff point between the external MCP server process and the Visual Studio host.

The Visual Studio side is then alive but idle. In that idle state:

  • the VSIX package has loaded
  • services have been composed
  • the local pipe boundary is the intended VS-backed request entry point
  • no MCP request has arrived yet

Operationally, live validation has shown one practical rule: if VS-backed tools cannot connect to the pipe, open the Visual Studio Experimental Instance and then open View -> Other Windows -> VS MCP Bridge. That activation path initializes the VSIX/tool-window side in the environment where manual validation has been proven.

Step 4: The Tool Window Is the Human Review Surface

The tool window is not a generic chat window. It is the operator-facing surface for logs, proposal entry, proposal review, approval, rejection, and outcome messages.

When the tool window is created, it resolves the presenter and view model, binds the shared WPF control, and initializes the UI state. The initial UI is idle:

  • logs are ready for diagnostic output
  • proposal fields are empty
  • the approval surface is inactive until a proposal exists

This distinction matters because bridge transport, tool execution, and human approval are related but separate concerns.

Step 5: The MCP Server Waits on Stdio

The project also contains a separate process, VsMcpBridge.McpServer. This process hosts the MCP server and uses stdio for protocol traffic.

That means an AI client can launch the MCP server process, write MCP messages to standard input, and read MCP responses from standard output. The server does not need to expose an HTTP endpoint or a public network port for this local workflow.

At this point the architecture has two different waiting states:

  • the VSIX waits for a named-pipe request
  • the MCP server waits for an MCP message over stdio

Keeping those transports separate is one of the main reasons the project is understandable. Stdio gets the request into the local MCP process. The named pipe gets the VS-backed request into Visual Studio.

Step 6: The First Tool Call Arrives

When a user submits a prompt in an AI client, nothing special happens in the bridge unless the AI client chooses to call one of the registered MCP tools.

For a VS-backed tool call, the flow looks like this:

  1. The AI client sends an MCP tool request over stdio.
  2. VsMcpBridge.McpServer receives the request.
  3. The selected MCP tool method forwards typed work through the pipe client.
  4. The pipe client connects to the local VsMcpBridge named pipe.
  5. The VSIX pipe server accepts the connection and reads the request envelope.
  6. The pipe server dispatches only known commands to the Visual Studio service layer.
  7. The VSIX performs the host-side operation and returns a response.
  8. The MCP server returns the result over stdio to the AI client.

This is where the bridge stops being idle and starts doing useful work.

Read-Only Calls vs. Edit Proposals

Read-only operations such as reading the active document, reading selected text, listing solution projects, or reading the Error List are straightforward. The VSIX performs the Visual Studio operation and returns data.

Edit-oriented requests are different. MCP can create edit proposals, but apply still happens only after explicit approval in the host UI. The proposal path validates the target content before mutation and keeps Visual Studio control over file changes.

That design is deliberate: the AI side can suggest changes, but the host side owns approval, apply, rollback behavior, and final outcome reporting.

Where Shared Tool Security Fits

The current architecture also has a shared compiled tool execution boundary for bridge tools outside the direct VS-backed pipe command path. Shared tools run through BridgeToolExecutor, which is the policy, approval, execution, audit, and redaction boundary for those tools.

That boundary now carries several lightweight security and observability seams:

  • tool execution policy evaluation
  • optional approval-aware execution for tools that require it
  • declarative capability metadata
  • secret-reference indirection hooks
  • redaction before payload-oriented logs and audit metadata
  • structured audit envelopes with classification metadata
  • request and operation correlation metadata

Those seams are not a full authentication, OAuth, vault, sandbox, or SIEM system. They are intentional architecture joints that make later hardening possible without turning tool execution into a black box.

Why Diagnostics Matter

Stdio is protocol traffic, so stdout must stay clean. Diagnostics need to go through safe channels such as app-data logs, stderr where appropriate, and host UI logging.

The bridge also follows an anti-black-box rule: important workflows should leave enough evidence to reconstruct what happened. That is why recent architecture work records durable trace artifacts and Mermaid diagrams for tool execution, approval-aware execution, MEF discovery, and inactive VSIX named-pipe diagnostics.

For example, if a VS-backed tool cannot connect to the named pipe, the current server returns an activation diagnostic instead of an opaque timeout. The operator action is concrete: launch the Visual Studio Experimental Instance, open the VS MCP Bridge tool window, and retry the VS-backed tool.

Why This Architecture Exists

At first glance, it can seem odd that both stdio and named pipes exist in the same design. The reason is that they solve different problems:

  • stdio is the local protocol transport between an AI client and the MCP server process
  • named pipes are the local host bridge between the MCP server process and Visual Studio
  • the tool window is the human-facing review and diagnostics surface
  • BridgeToolExecutor is the shared compiled-tool policy and audit boundary

This split keeps Visual Studio API access inside the VSIX, keeps MCP protocol handling outside Visual Studio, and gives future tool work a clear place for policy, approval, redaction, audit, and correlation.

Takeaway

If you are trying to understand startup, the cleanest mental model is this:

Visual Studio starts VSIX
  - package initializes
  - services are composed
  - named pipe side becomes the VS-backed request boundary
  - tool window activation may be required for live validation

AI client starts MCP server
  - MCP server starts
  - stdio transport waits for MCP protocol messages
  - registered MCP tools become callable

User submits a prompt
  - AI may call an MCP tool
  - MCP server handles protocol work
  - VS-backed calls cross the named-pipe boundary
  - shared compiled tools cross BridgeToolExecutor
  - host-side results flow back to the AI client

In other words, VS MCP Bridge is not one big chat loop. It is a set of explicit local boundaries: MCP over stdio, Visual Studio work over a named pipe, user approval in the host UI, and shared tool execution behind a policy/audit executor.

Next In The Series

The next post should answer a natural follow-up question: why stdio is used at all, what it is good at, and why it should not be treated as a multi-client shared bus.

Understanding a Local MCP Server Over Stdio and Local-Only Communication Over a Named Pipe

VS MCP Bridge uses two local communication boundaries together:

  1. An AI client talks to the local MCP server over stdio.
  2. The local MCP server talks to the Visual Studio extension over a named pipe.

Those two transports are easy to blur together, but they solve different problems. Stdio is the AI-facing MCP protocol boundary. The named pipe is the Visual Studio host boundary.

The Current Runtime Shape

The current VS-backed path is:

AI client
  -> MCP over stdio
VsMcpBridge.McpServer
  -> JSON request/response over local named pipe "VsMcpBridge"
VsMcpBridge.Vsix
  -> Visual Studio SDK / DTE / editor state

The MCP server does not load inside Visual Studio. The VSIX does not speak MCP over stdout. Each side owns the work that belongs in its process.

Why stdio Exists

stdio gives the AI client a simple local way to launch and communicate with the MCP server. The client writes MCP messages to standard input and reads MCP responses from standard output. Microsoft documents the underlying .NET stream support through Process.StandardInput.

For MCP, the important rule is stricter than ordinary process communication: stdout is protocol output. It must stay clean. Random log lines, status messages, or troubleshooting text on stdout can corrupt the MCP conversation.

That is why diagnostics belong in transport-safe places: stderr where appropriate, local app-data logs, Visual Studio logs, UI logging, and durable trace artifacts. The MCP response stream should remain parseable protocol traffic.

Why the Named Pipe Exists

The named pipe exists because Visual Studio work belongs behind the VSIX boundary. The VSIX runs inside Visual Studio and can access DTE, editor state, solution state, the Error List, proposal review surfaces, and approved apply behavior.

The MCP server stays outside Visual Studio. For VS-backed tools, it uses a local named pipe to send a structured request to the VSIX. Microsoft documents the .NET named-pipe server primitive through NamedPipeServerStream.

This keeps Visual Studio concerns behind a local-only host boundary. The MCP server does not need DTE access, and the VSIX does not need to become an MCP stdio host.

The Two Boundaries Together

A normal VS-backed MCP call crosses the boundaries in order:

  1. The AI client sends an MCP tool request over stdio.
  2. VsMcpBridge.McpServer resolves the registered MCP tool.
  3. The VS-backed tool method sends a request through PipeClient.
  4. PipeClient connects to the local VsMcpBridge named pipe.
  5. PipeServer in the VSIX accepts and parses the request envelope.
  6. The pipe server dispatches only a known command to the host service layer.
  7. VsService performs the Visual Studio operation.
  8. The response returns through the pipe.
  9. The MCP server returns the tool result over stdout.

The result is a local bridge, not a remote service and not one process doing everything.

Request Envelopes and Correlation

The named-pipe hop sends structured request/response envelopes, not free-form chat text. The envelope includes command and correlation metadata such as request IDs. Those IDs are how logs and trace artifacts reconnect a tool request to the pipe command and the host operation.

This matters because the useful troubleshooting question is rarely “did the bridge fail?” The useful question is more specific:

  • Did the MCP request arrive over stdio?
  • Did the MCP server resolve the expected tool?
  • Did PipeClient attempt the expected command?
  • Did the named pipe connect?
  • Did PipeServer dispatch a known command?
  • Did the Visual Studio-side operation complete?
  • Did the response return through the same correlation chain?

That is the anti-black-box point of correlation metadata: the first missing boundary should be visible.

Startup and Activation Diagnostics

The VSIX side has to be active before VS-backed tools can succeed. In current live validation, the operator path is:

  1. Launch the Visual Studio Experimental Instance.
  2. Open View -> Other Windows -> VS MCP Bridge.
  3. Let the tool-window path initialize the VSIX/named-pipe side.
  4. Retry the VS-backed MCP tool.

If the MCP server cannot connect to the pipe, the current diagnostic path returns a structured activation message instead of an opaque timeout. That failure is still a tool result. It does not change the MCP transport, add retry loops, or write troubleshooting text outside the MCP response stream.

That distinction is useful: an inactive named pipe is not a stdio failure. It means the local MCP server could not reach the VSIX side.

Approval and Tool Execution Boundaries

The transports move requests. They do not authorize arbitrary behavior by themselves.

For Visual Studio edit operations, the named-pipe path reaches the VSIX proposal workflow. MCP can create proposals, but apply still requires explicit approval in the host UI.

For shared compiled bridge tools, execution flows through BridgeToolExecutor. That executor is the policy, approval, execution, audit, redaction, and correlation boundary for compiled tools. It owns the approval-aware tool execution seam, capability metadata evaluation hooks, secret-reference awareness, redacted audit envelopes, and classification metadata.

That means the full architecture has three different concerns, each with a different job:

  • stdio moves MCP protocol messages between the AI client and local MCP server.
  • named pipes move structured VS-backed requests between the MCP server and VSIX.
  • BridgeToolExecutor governs shared compiled tool execution behind policy, approval, audit, and redaction seams.

Trace-Only Failure Evidence

Because stdout must stay clean, failure evidence lives in trace-safe places. The current repo keeps durable logs, metadata, and Mermaid sources for important paths, including inactive VSIX pipe diagnostics and shared tool execution.

A good inactive-pipe trace can be reconstructed as:

MCP tool request received
PipeClient attempted named-pipe connection
named pipe was unavailable
activation diagnostic returned
request/correlation metadata preserved
no raw payload or secret-like values disclosed

That is more useful than a raw timeout because it tells the operator what boundary failed and what to do next.

Why This Design Is Deliberate

The two transports keep the bridge small and local while preserving clear responsibilities:

  • The AI client gets a standard MCP stdio process.
  • The MCP server stays outside Visual Studio.
  • Visual Studio APIs stay inside the VSIX.
  • VS-backed operations cross a local named-pipe request boundary.
  • Shared compiled tools have a separate execution/security boundary.
  • Diagnostics stay reconstructable without polluting MCP stdout.

That separation is what lets the project add approval-aware execution, capability metadata, secret references, audit classification, and trace artifacts without turning transport code into a security policy engine.

Takeaway

The shortest accurate model is:

stdio gets into the MCP server
named pipes get into Visual Studio
BridgeToolExecutor governs shared compiled tool execution

Once that model is clear, the bridge becomes easier to reason about. Each boundary has a narrow job, and each important failure mode has somewhere observable to land.

— AI Systems Author

Why a VSIX Project Should Target .NET Framework 4.7.2

Host Constraints, Shared Code, And Stable Bridge Boundaries

When building a Visual Studio extension, one detail is easy to underestimate: an in-process VSIX is loaded by the Visual Studio shell. It is not a standalone desktop app, and it should not be treated like one.

In VS MCP Bridge, that is why VsMcpBridge.Vsix targets .NET Framework 4.7.2. The VSIX must align with the Visual Studio SDK and in-process extension hosting model, while the rest of the solution can use other target frameworks where they make sense.

Microsoft's in-process extension guidance summarizes the rule this way: in-process extensions must target the .NET version used by the Visual Studio version they run in. The relevant guidance is here: VisualStudio.Extensibility in-process extensions.

The VSIX Runs Inside Visual Studio

The VSIX host is different from the standalone app and different from the local MCP server.

The VSIX is loaded into the Visual Studio process. It uses the Visual Studio SDK, shell services, tool window infrastructure, MEF composition expectations, DTE/editor APIs, package loading behavior, and WPF UI hosted by Visual Studio.

That hosting model is the reason the extension project follows Visual Studio's in-process runtime constraints. Trying to force the VSIX itself to behave like a modern out-of-process .NET app would make loading, packaging, dependency resolution, and tool-window behavior harder to reason about.

The Current Solution Uses Targeting Deliberately

The target framework split is part of the architecture:

  • VsMcpBridge.Vsix targets .NET Framework 4.7.2 because it is the Visual Studio in-process extension host.
  • VsMcpBridge.Shared targets netstandard2.0 so shared contracts, tools, security seams, diagnostics, and orchestration logic can be reused across hosts.
  • VsMcpBridge.Shared.Wpf multi-targets so the reusable WPF surface can support both VSIX and standalone app hosts.
  • VsMcpBridge.App can target a modern Windows desktop runtime because it is not loaded into Visual Studio.
  • VsMcpBridge.McpServer can target a modern runtime because it runs out of process and communicates over stdio plus the local named pipe.

This is not accidental legacy layering. It is how the bridge keeps Visual Studio-specific constraints from infecting every project.

Host Code And Shared Logic Stay Separate

The VSIX owns Visual Studio-specific behavior:

  • package initialization
  • tool window creation
  • Visual Studio service access
  • DTE and editor interactions
  • UI-thread switching
  • VSIX-host logging and diagnostics

Shared infrastructure owns reusable bridge behavior:

  • pipe message contracts and dispatch abstractions
  • presenter/viewmodel orchestration
  • proposal lifecycle contracts
  • bridge tool descriptors, requests, results, catalog, and executor
  • policy, approval, redaction, audit, capability, and secret-reference seams
  • diagnostic patterns and correlation metadata

That separation lets the shared layer be tested without loading Visual Studio. It also lets the standalone app reuse the same core presentation and bridge concepts without pretending to be a VSIX.

Tool Windows Follow Visual Studio Lifecycle Rules

Visual Studio owns the lifecycle of extension components. Tool windows are created by the shell, not by normal application startup code.

That matters for dependency wiring and initialization. A VSIX should not assume that every object can be created with application-style constructor injection. Tool-window initialization belongs at the lifecycle points Visual Studio provides, including ToolWindowPane.OnToolWindowCreated() where appropriate.

This lifecycle constraint connects directly to the threading post: the VSIX must respect both Visual Studio object creation and Visual Studio UI-thread requirements.

Stable Pipe Integration Depends On Host Isolation

The local MCP server does not run inside Visual Studio. It speaks MCP over stdio to the AI client and communicates with the host through the local named pipe.

That boundary is important. The MCP server should not need to reference Visual Studio SDK assemblies, know about tool-window lifecycle rules, or switch to the Visual Studio UI thread. It should remain transport-focused and protocol-safe.

The VSIX side can then own the named-pipe server and host behavior. When a pipe-backed tool needs active document state, selected text, solution projects, error list data, or proposal UI behavior, the request crosses into the VSIX host, where Visual Studio-specific services are available.

This keeps the out-of-process server stable while letting the in-process extension follow Visual Studio's runtime rules.

Testing Benefits From The Split

Because shared infrastructure is not trapped inside the VSIX target framework, much of the bridge can be tested directly:

  • shared tool execution tests can validate catalog, executor, policy, approval, audit, redaction, and correlation behavior
  • proposal lifecycle tests can validate state transitions without starting Visual Studio
  • shared WPF and presenter behavior can be exercised outside the VSIX host where appropriate
  • VSIX-specific tests can focus on composition and host-specific service behavior

That is one reason the project can evolve safely. The VSIX target framework is a host constraint, not a reason to put all behavior into untestable host code.

Transport And Tool Execution Should Not Depend On VSIX Runtime Behavior

The bridge architecture intentionally prevents shared transport and tool execution concepts from depending on VSIX-only runtime behavior.

For example, BridgeToolExecutor owns shared tool policy, approval, redaction, audit, correlation, and structured results. It should not need to know whether the caller is the VSIX, the standalone app, or a test harness. Likewise, tool descriptors and request/result models should not depend on Visual Studio shell types.

When a tool genuinely needs Visual Studio, that should be represented as host-provided behavior behind the proper boundary. The shared contract should remain portable and observable.

What This Does Not Claim

This post is not a promise that the VSIX will move to a different framework. It is also not a claim that every project in the solution must target .NET Framework.

The practical rule is narrower:

  • respect the runtime constraints of the Visual Studio in-process extension host
  • keep Visual Studio-specific code in the VSIX host
  • keep reusable bridge contracts and logic outside the VSIX where possible
  • let out-of-process components use target frameworks appropriate to their own runtime

Takeaway

Targeting .NET Framework 4.7.2 in the VSIX project is not just an old default. It is part of respecting the Visual Studio in-process hosting environment.

The maintainable design is to keep the VSIX host compatible with Visual Studio, keep shared logic portable and testable, keep the MCP server out of process, and let each boundary use the runtime model that fits its role.

That is what makes the bridge easier to build, validate, troubleshoot, and eventually evolve without turning Visual Studio hosting constraints into system-wide coupling.

WPF VSIX Threading: Understanding UI Switching, Async Behavior, and Pipe Safety

Why Reliable AI Tooling Depends On Reliable Host Boundaries

AI-assisted workflows only feel trustworthy when the host runtime is trustworthy. In a Visual Studio extension, that means WPF state, Visual Studio APIs, async work, and pipe-backed requests must respect the UI thread instead of treating it as an implementation detail.

VS MCP Bridge is a useful example because it has several boundaries active at the same time: MCP stdio, a local named pipe, Visual Studio APIs, a WPF tool window, proposal approval state, and shared tool execution. If those boundaries blur, the AI layer may look unreliable even when the real problem is host-thread misuse.

The Core Rule

The Visual Studio UI thread is a scarce resource. Treat it that way.

  • Do transport, parsing, validation, and file-independent computation off the UI thread.
  • Switch to the UI thread only for WPF state, Visual Studio shell access, editor access, or UI-bound services.
  • Do the smallest possible amount of work after switching.
  • Return to async background execution naturally after the UI-sensitive work is complete.

The goal is not to eliminate switching. The goal is to make every switch intentional, narrow, and easy to explain in logs or traces.

Why UI Locks Happen

Most VSIX threading problems come from a few familiar patterns:

  • blocking on async work with .Result or .Wait()
  • doing expensive work after switching to the UI thread
  • switching too early and carrying too much execution on the UI thread
  • letting pipe or transport code manipulate WPF state directly
  • calling Visual Studio APIs from background code without isolating the UI-thread requirement
  • assuming an await preserves thread affinity for the rest of the method

Those problems are not cosmetic. They can make tool calls hang, approval UI state appear stale, or diagnostics point at the wrong layer.

Every Await Is A Boundary

A common source of confusion is code shaped like this:

await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(ct);
// UI work

var data = await _service.GetDataAsync(ct);

await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(ct);
_viewModel.Apply(data);

The second switch is not redundant. The first switch makes the immediate continuation UI-thread-safe. The later await introduces another suspension point. After that awaited operation completes, code that touches WPF or Visual Studio state should re-establish the UI-thread requirement.

If code after an await must touch UI or Visual Studio state, switch intentionally at that point.

Pipe Safety Starts With Separation

The named pipe is not the UI. It is a local transport boundary.

In VS MCP Bridge, pipe code should handle message reading, serialization, dispatch, validation, cancellation, and transport diagnostics. It should not update WPF controls, mutate viewmodel state directly, or treat Visual Studio APIs as if they were background-safe.

The safe shape is:

MCP request
  -> stdio-safe MCP server
  -> local named-pipe client
  -> pipe server dispatch
  -> host service
  -> minimal UI-thread switch only where host state requires it
  -> structured response

That separation matters because MCP stdout must stay clean. Diagnostics belong in stderr, file logs, UI logs, trace artifacts, and structured failures, not stray stdout lines that corrupt protocol traffic.

Visual Studio Access Belongs Behind The Host Boundary

Visual Studio APIs are host-specific and often UI-thread-sensitive. The MCP server should not own that knowledge. Shared tool code should not own it either.

The VSIX host is the correct place to isolate Visual Studio access:

public async Task<string> GetActiveDocumentPathAsync(CancellationToken ct)
{
    await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(ct);
    ThreadHelper.ThrowIfNotOnUIThread();

    return _vsAdapter.GetActiveDocumentPath();
}

Everything outside that narrow section can remain async and background-friendly. That keeps host correctness visible and stops UI-thread requirements from leaking through the whole codebase.

Transport, UI Orchestration, And Execution Are Different Boundaries

One of the architecture lessons from VS MCP Bridge is that not all boundaries are the same.

  • Transport boundary: MCP stdio and the local named pipe move requests and responses.
  • Host boundary: the VSIX owns Visual Studio services, DTE access, editor state, and UI-thread switching.
  • UI orchestration boundary: the presenter and viewmodel own visible tool-window state and proposal review surfaces.
  • Execution boundary: BridgeToolExecutor owns shared tool policy, approval, redaction, audit, correlation, and structured results.

Threading bugs often happen when these responsibilities collapse into one another. A pipe handler should not become a UI controller. A presenter should not become a transport layer. A discovered tool should not bypass the executor. A model suggestion should not silently decide any of that.

Proposal State Makes Threading Visible

The proposal workflow is where threading, UI state, and AI-assisted tooling meet.

An MCP client can submit a proposed edit. The request crosses the named-pipe boundary. The VSIX host creates proposal state and displays it in the tool window. The user approves or rejects it. Apply happens only after approval, and terminal outcome state is shown back in the UI.

That workflow depends on host correctness. If UI state is updated from the wrong thread, or if async callbacks are reused after a proposal completes, the user sees confusing behavior. It may look like the AI tool is unreliable, but the real defect is usually lifecycle or thread ownership.

The current architecture separates proposal lifecycle ownership through IProposalManager, presenter orchestration, and viewmodel state. That makes the workflow easier to reason about and test.

Diagnostics Expose Hidden Execution Order

The project improved when logs and Mermaid traces made execution order visible.

For host correctness, the important question is not only "did this call succeed?" It is also:

  • Which request id was active?
  • Which layer received the request?
  • Did the request cross the pipe boundary?
  • Did the VS service operation start?
  • Did the code switch to the UI thread only where required?
  • Did visible UI state update after the host work completed?
  • Did terminal proposal state clear correctly?

When those answers are visible, troubleshooting becomes a boundary-localization exercise instead of a guessing game.

Correct Pattern: Background First, UI Last

A safe workflow keeps background work and UI work separate:

public async Task<ResponseDto> HandleRequestAsync(RequestDto request, CancellationToken ct)
{
    var parsed = Parse(request);
    var result = await _worker.ProcessAsync(parsed, ct);
    return result;
}

Then the UI layer applies the result intentionally:

public async Task RefreshAsync(CancellationToken ct)
{
    var result = await _service.HandleRequestAsync(_request, ct);

    await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(ct);
    _viewModel.Apply(result);
}

That pattern keeps transport logic, host work, and UI presentation from becoming a tangled blocking path.

Practical Checklist

  • Assume background execution by default.
  • Switch to the UI thread as late as possible.
  • Keep UI-thread sections small and explicit.
  • Never block on async work.
  • Keep pipe and transport code UI-agnostic.
  • Keep MCP stdout clean; send diagnostics through approved channels.
  • Keep proposal lifecycle state owned by the proposal/presenter/viewmodel boundary.
  • Log request ids, operation names, success or failure, and elapsed timing at meaningful boundaries.
  • Use durable traces when a workflow matters enough that a future session must reconstruct it.

Takeaway

Reliable AI tooling depends on reliable host/runtime boundaries.

In a WPF VSIX, that means switching to the UI thread only when the host actually requires it, keeping pipes and stdio transport-safe, separating UI orchestration from execution, and making important workflows observable through logs and diagrams.

Switch late, do little, leave quickly, and leave evidence.

That pattern keeps the extension responsive and makes AI-assisted workflows easier to trust, diagnose, and evolve.

Understanding Dependency Injection (DI)

IOC

LinqPad Script: WeatherForecastR5.linq (12.09 kb)

I'll start at the end (literally) and give the key information you'll need to know about dependency injection.  WebApi and ASP.NET Core applications use a dependency injection system to instantiate classes; in the case of this application, when a route is selected (figure 10b lines 211-213) the class for that route is instantiated and then invoked, e.g., HomePage, WeatherPage, and ToggleService.

the IOC system (which I'll just refer to as system) will look in its service collection registrations (figure 10a lines 174-183) to not only instantiate the class, but also provide its parameters.  The registrations will tell the system how to instantiate a class, e.g., as Transient (new instance each request), Scoped (per session / request), and Singleton (everyone shares the same instance).  The difference between scoped and singleton is that if 5 people hit the Website at the same time, each will get their own scoped instance, which is isolated from the other 4 users.  Within a session, the scoped instance behaves as a singleton, but only for that user.   Where singletons instances will be shared by "every" user.

The system uses constructor injection to instantiate and invoke the class [and its parameters].   By default, the system will look for the constructor with the largest number of parameters, get instances for each of the parameters, instantiate the class, and then invoke the class constructor with the parameters.   All classes and parameters must be declared in the service registrations, aka "container".    

Note that as each parameter is instantiated, that it's constructor parameters are also looked up in the container, instantiated and provided.   This is referred to as propagating the dependency chain; as long as "new" is never used to instantiate a class (breaking the chain) then you'll be able to simply put an interface or class in any class constructor and the system will give you an instance for it. 

Understanding this is the key, and paramount, to understanding the IOC/DI system.  It is the essence of Inversion of Control (IOC), aka Dependency Injection (DI).  Inversion of control meaning that instead of you instantiating a class, providing all of the constructor parameters, and invoking the class - the system does it for you.


Figure 1. Overview of application running

With basics out of the way.  All that remains is understanding the function of each class.  We'll cover each of the following with an overview of each classes code.  You'll find that there is a clear separation of concerns with each having a single responsibility; there is not a lot of code in each class, it does one thing, and it does it well.


Figure 2.  Skeleton view of application components

The following are the HomePage, WeatherPage, and ToggleService.  For the home page we'll introduce a second IOC Unity Container, unlike the system's container, the Unity Container supports Setter injection (discussed below) and allows you to register additional interfaces, classes, and factories on the fly.   With the system container, you'll find that you can only register during system bootstrapping - once the container is built, you cannot add any more registrations.  

You'll see that we provide an instance of IUnityContainer [in image below] and use it to instantiate (resolve) the IWeatherFormatter instance.   This uses a factory pattern, that based on the current value of IsJson (figure 10a lines 166-171) the container will provide either a JsonFormatter or TableFormatter instance.

Setter injection will kick in because these implementations of IWeatherFormatter both have the property below;
   [Dependency] Public IFoo Bar {get;set;} 

The [Dependency] tells the Unity container that it needs to populate this property in the same manner as it does constructor parameters; it provides an instance.  This is referred to as Setter injection you'll find that the system and unity both use different values (reference figure 10b and the comments on line 198-203 as to why).

Armed with the knowledge of setter injection, you should now be able to look at the code in figure 9 for Foo and understand how the "Bar" class will return "This is FooBar" for it's GetMessage() function.  

Figure 3.  Pages and service

Below we see the results of the HomePage being clicked with the TableFormatter.


Figure 4. Home page

Below we show the results of the WeatherPage being clicked with TableFormatter


Figure 5. Weather forecast page

Below we show that the ToggleService will toggle the IsJson property which is then returned (via bodyHtml) to the invoking process (in HtmlBase figure 11).  Once the state is toggle any subsequent Home or Weather clicks will result in json being displayed.


Figure 6. Toggle service

Below is the key parts to the HtmlBase, which our HomePage, WeatherPage, and ToggleService derive from.


Figure 7. HtmlBase class

Below we show our TableFormatter and JsonFormatter components


Figure 8. Formatters (json and html table)

We use IFoo to demonstrate how dependencies are propagated, and automagically populated, by either constructor or setter injection.


Figure 9. Foo

The magic happens in the container.  The system will require that all dependencies are registered so that it knows how to instantiate a components lifetime (transient, scoped, or singleton) and provide an instance.  Below the code is commented.


Figure 10a First part of WebAppBuilderExtension

Here we show how we can do a late registration (after build on line 204) and as a result change the setting for IFoo in the unity container - it will have a different implementation now then the system.   We also demonstrate how MiddleWare can use these registrations - it will send information to the console base on the registered implementation of its constructor parameters.


Figure 10b Second part of WebAppBuilderExtension

GetHtml() below is how our pages display their content with javascript code handling button clicks and clock updates.


Figure 11.  GetHtml() code 

The decoupled nature of IOC / DI will allow for easy reuse of components as it is ultimately the container that can pick and chose its implementation for any of its interfaces.


Figure 12 - where the MiddleWare parameters are displayed

How to publish your own blog [SmarterAsp]

This blog is available on GitHub: BlogEngine.NET (Billkrat fork) 

Once you have the source code available you can publish it to a SmarterASP.NET host for as little as $2.95 a month (see add on bottom right); having your own blog doesn't have to be expensive nor hard to deploy/setup.

  1. Figure 1 Creating a new site in SmarterASP
  2. Figure 2 Show Deployment Information
  3. Figure 3 Get the Web Deploy publish information

    In Visual Studio
  4. Figure 4 Add a new profile and select "Import Profile"
  5. Figure 5 Point to the file you downloaded from SmarterASP
  6. Figure 6 Publish your site


Figure 1 Creating a new site in SmarterASP 


Figure 2 Show Deployment Information


Figure 3 Get the Web Deploy publish information


Figure 4 Add a new profile and select "Import Profile"


Figure 5 Point to the file you downloaded from SmarterASP


Figure 6 Publish your site