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

Source of Truth: docs/ARCHITECTURE.md
Status: Canonical repo cleanup aligned to the current architecture as of 2026-05-16. This post summarizes how stdio and named pipes fit together; the focused stdio and named-pipe posts cover each transport in more detail.

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.

Related Mermaid Trace Sources

The repo already has Mermaid sources that support this article:

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

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

Source of Truth: docs/ARCHITECTURE.md
Status: Canonical repo cleanup aligned to the current VS MCP Bridge and BlogAI narrative as of 2026-05-16.

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

Related Mermaid Trace Sources

The following diagram sources help explain the host/runtime split:

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

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

Source of Truth: docs/ARCHITECTURE.md
Status: Canonical repo cleanup aligned to the current VS MCP Bridge and BlogAI narrative as of 2026-05-16.

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.

Related Mermaid Trace Sources

The following diagram sources make these boundary rules concrete:

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

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