Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Build it as two protocol adapters around one language-service core. Let an LSP endpoint handle editor state—documents, versions, diagnostics, completion and navigation—while an MCP server exposes narrowly scoped tools and resources to an AI host. Keep parsing, indexing, configuration and cancellation in shared services, then choose stdio for a local process or HTTP for a remotely hosted service. This separation prevents editor lifecycle rules from leaking into model-facing tools and lets one implementation support both deployments.

The architecture that keeps LSP and MCP maintainable

LSP and MCP solve different problems. The Language Server Protocol carries editor requests and notifications such as document changes, diagnostics, hover and definitions. The Model Context Protocol standardizes how an AI application obtains context and invokes external tools. Do not treat MCP as a replacement for LSP.

A practical topology is:

Editor ⇄ LSP JSON-RPC ⇄ C# language-service core ⇄ MCP adapter ⇄ MCP host/model

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The core owns source text, document versions, parsing, semantic analysis, project configuration, workspace indexing and cancellation. The LSP adapter owns editor-facing protocol types and lifecycle. The MCP adapter translates safe language-workflow operations into tools or resources. The official C# SDK is intended for .NET applications, services and libraries that implement or interact with MCP clients and servers; its client/server transport model supports this adapter approach.

Choose the C# SDK package

Start with the package that matches the process you are shipping. Package maturity and prerelease flags can change, so check the current release channel before pinning a command in automation.

Package Use it for Typical choice
ModelContextProtocol.Core Low-level APIs or a client with minimum dependencies Custom hosting, protocol plumbing, or client-only applications
ModelContextProtocol Hosted clients and local stdio servers, including dependency injection and attribute-based discovery Most new C# MCP servers
ModelContextProtocol.AspNetCore HTTP-based MCP servers ASP.NET Core hosting, authentication and remote deployment

The normal starting point for a local server is ModelContextProtocol. The ASP.NET Core package references it and adds the HTTP hosting path. Microsoft’s getting-started material has shown installation with dotnet add package ModelContextProtocol --prerelease; use the flag only when the release you have selected requires it.

Create the solution and shared services

  1. Create a solution with separate projects so protocol code does not become entangled with analysis code:

    Free tools Windows power users keep installed

    One-click scans. No signup required.

    Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
    dotnet new sln -n CSharpMcpLanguageServer
    dotnet new classlib -n LanguageService.Core
    dotnet new console -n LanguageMcp.Stdio
    dotnet sln add LanguageService.Core/LanguageService.Core.csproj
    dotnet sln add LanguageMcp.Stdio/LanguageMcp.Stdio.csproj
    dotnet add LanguageMcp.Stdio reference LanguageService.Core
  2. Add the MCP package to the stdio host. Confirm the current stable or prerelease channel first:

    dotnet add LanguageMcp.Stdio package ModelContextProtocol
  3. Keep the core independent of transport. A minimal shape is:

    namespace LanguageService.Core;
    
    public sealed record TextDocument(string Uri, int Version, string Text);
    
    public sealed class WorkspaceIndex
    {
        private readonly object gate = new();
        private readonly Dictionary<string, TextDocument> documents = new();
    
        public void OpenOrUpdate(TextDocument document)
        {
            lock (gate)
            {
                if (documents.TryGetValue(document.Uri, out var old) && document.Version <= old.Version)
                    return; // Ignore an out-of-order notification.
                documents[document.Uri] = document;
            }
        }
    
        public bool TryGet(string uri, out TextDocument? document)
        {
            lock (gate) return documents.TryGetValue(uri, out document);
        }
    
        public IReadOnlyList<TextDocument> Snapshot()
        {
            lock (gate) return documents.Values.ToArray();
        }
    }

In production, replace the dictionary with a workspace model that tracks projects, files, references and an incrementally updated symbol index. The important invariant is that a background analysis result may be applied only if it still corresponds to the document version that triggered it.

Host a local MCP server over stdio

The following host uses the SDK’s dependency injection and attribute discovery. It is a complete starting point for a local MCP process launched by an editor or AI client:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using ModelContextProtocol.Server;
using LanguageService.Core;

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton<WorkspaceIndex>();
builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()
    .WithToolsFromAssembly();

await builder.Build().RunAsync();

Define tools in the same assembly (or register the assembly that contains them). Keep every operation bounded by an explicitly configured workspace root:

using ModelContextProtocol.Server;
using LanguageService.Core;

[McpServerToolType]
public static class LanguageTools
{
    [McpServerTool, Description("Return a short list of symbols matching a name in the configured workspace.")]
    public static object[] FindSymbols(
        WorkspaceIndex index,
        [Description("Exact or partial symbol name")] string name,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        return index.Snapshot()
            .Where(d => d.Text.Contains(name, StringComparison.OrdinalIgnoreCase))
            .Select(d => new { uri = d.Uri, version = d.Version })
            .Cast<object>()
            .Take(100)
            .ToArray();
    }
}

The example deliberately returns a small, structured result rather than raw filesystem access. A real implementation should parse syntax trees, return symbol names and ranges, validate all arguments and reject paths outside the workspace.

Implement the LSP endpoint as a separate adapter

Use a maintained C# LSP library for JSON-RPC framing and protocol types rather than hand-writing every message. The LSP literature recommends delegating protocol details to an SDK where practical. Your handlers should map protocol events to the shared core:

  • initialize: advertise only the capabilities you implement and record the client’s root and settings.
  • initialized: start workspace discovery and publish progress if the client supports it.
  • textDocument/didOpen: insert the complete text and version into the core.
  • textDocument/didChange: apply ordered edits, reject or resynchronize stale versions, then schedule cancellable analysis.
  • textDocument/didClose: release unsaved buffers while retaining files that remain part of the workspace.
  • textDocument/publishDiagnostics: publish diagnostics tagged with the analyzed document version when the client supports versioned reports.
  • completion, hover, definition, references and document symbols: query immutable snapshots of the index so a concurrent edit cannot mutate a response halfway through serialization.
  • shutdown and exit: stop new work, cancel analysis, flush diagnostics and close transport in the order required by the LSP client.

Do not call MCP tools from an LSP handler merely to reuse code. Call the same core service directly. MCP calls can be slower, have different authorization, and may be unavailable when the editor is offline.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Implement MCP initialization before tools

MCP clients and servers negotiate capabilities during initialization. Follow this sequence before exposing useful operations:

  1. Accept the client’s initialize request and validate the protocol version you support.
  2. Return server information and only the capabilities that are actually enabled, such as tools, resources, prompts or completion.
  3. Wait for the client’s initialized notification before serving normal discovery traffic.
  4. Implement ping and return promptly so hosts can detect a dead process.
  5. Add cancellation tokens to parsing, indexing and every MCP operation that can exceed a few milliseconds.
  6. Report progress for indexing or large workspace queries when the negotiated capability permits it.
  7. Add task handling only when an operation genuinely needs multiple round trips; define polling, completion and failure states explicitly.

Capability negotiation, stdio, Streamable HTTP and SSE transports, ping, progress, cancellation and tasks are described in the MCP conceptual documentation. Treat the negotiated capabilities as a runtime contract: never send a notification or result shape that the client did not agree to receive.

Synchronize documents without race conditions

Version every buffer

Store the editor-provided integer version with each open document. A change whose base version is older than the stored version must not overwrite newer text. If edits arrive out of order, request a full resynchronization through the LSP mechanism supported by your client or mark the document temporarily unavailable for analysis.

Cancel obsolete analysis

Associate a CancellationTokenSource with each document’s pending analysis. Cancel it when a newer change arrives. Before publishing diagnostics or updating the symbol index, compare the result’s source version with the current version. This prevents a slow parse from replacing a newer, correct result.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Separate snapshots from mutable state

Build an immutable snapshot for completion, hover and navigation. Readers then see a consistent project graph while a writer updates the index. Use bounded queues for file watching and indexing; otherwise a generated-code directory can consume all memory and starve interactive requests.

Choose stdio or HTTP

Decision stdio HTTP
Best fit Local editor or AI client launches and supervises one process Shared, remote or centrally managed service
Operational model Process lifetime usually follows the client Independent service with routing, authentication and deployment concerns
State Easy to keep workspace state in memory for one client Choose explicitly between per-session state and stateless workers
Failure handling Restart the child process and rebuild its index Use timeouts, retries and health checks without duplicating non-idempotent work
Security OS process permissions and workspace configuration Authentication, authorization, TLS and tenant isolation

For a local language server, start with stdio. For a remotely reachable MCP endpoint, use ModelContextProtocol.AspNetCore and keep workspace identity in authenticated request context rather than trusting a path supplied by a model.

The SDK v2.0 announcement dated 2026-07-28 describes implementation of the 2026-07-28 MCP revision, including HTTP being stateless by default, a standardized HTTP surface and multi-round-trip requests. Re-check those semantics when designing session state, load balancing, authentication and retry behavior; do not assume an older stateful HTTP example still applies.

Security boundaries for model-facing tools

  • Resolve every path against an allow-listed workspace root and reject traversal, symlink escapes and drive changes.
  • Prefer read-only symbol, diagnostics and metadata tools. Put mutations behind separate, explicit tools with confirmation requirements.
  • Validate lengths, selectors, project names and query limits before invoking the parser or filesystem.
  • Never expose an unrestricted shell, process launcher or arbitrary file-write tool merely because the language server can access those APIs.
  • Return structured errors that identify invalid arguments without disclosing secrets, environment variables or unrelated workspace files.
  • Log authorization decisions and request identifiers, but send logs to stderr when using stdio so stdout remains protocol-clean.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Test both protocols before release

Unit-test the core with version races and cancellation, then test each adapter against a real JSON-RPC client. Your minimum matrix should include:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Correct framing for every request, notification and response, including Unicode and large payloads.
  • Malformed JSON, unknown methods, missing fields and invalid capability declarations.
  • Initialize, initialized, ping, shutdown and process-exit behavior.
  • Out-of-order document versions, overlapping edits and a slow analysis canceled by a newer edit.
  • Diagnostics, completion, hover, definitions, references and empty-workspace responses.
  • MCP tool and resource discovery, invalid arguments, cancellation, progress and task polling where implemented.
  • Client disconnects, stdio restarts, HTTP timeouts, reconnects and duplicate requests.
  • Workspace-boundary violations and authorization failures for every tool that accepts a URI or path.

Run editor-to-model end-to-end tests after the adapter tests. The LSP and MCP implementations can each pass isolated tests while their shared index still publishes stale data or leaks a workspace between sessions.

Performance, reliability and cost considerations

Keep interactive work bounded

Completion and hover should use an already-built snapshot. Schedule full indexing in the background, cap concurrent parses and return partial results only when the client understands them. Cache syntax trees by document version and invalidate dependent projects when references change.

Make retries safe

Read-only tools can usually be retried after a transport failure. A mutation must carry an idempotency key or be rejected on retry, otherwise an AI host may apply it twice. For HTTP, include request correlation IDs and enforce per-tenant timeouts.

Control resource growth

Limit open-document size, symbol-result count, queued files and task retention. Dispose cancellation sources and file watchers on shutdown. Measure parse latency, index queue depth, MCP request duration and cancellation rate; the protocol specifications do not provide a universal performance target, so establish budgets for your own workspace sizes.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Troubleshooting common failures

Symptom Likely cause Fix
The client reports invalid JSON immediately Diagnostic text was written to stdout in a stdio server Send logs to stderr and reserve stdout for MCP framing.
Tools do not appear after initialization Tool attributes were not discovered or the capability was not advertised Call the assembly-discovery registration, verify the tool type is public, and inspect the initialize result.
Diagnostics revert after typing A slower analysis published a stale version Attach the source version to each job and discard results that are no longer current.
HTTP clients lose context between requests The service assumed stateful sessions while the selected MCP HTTP revision is stateless by default Persist required state in an authenticated session store or design every request to carry its context.
Cancellation has no effect The token is not passed into parser, indexer or downstream MCP calls Thread the token through every async method and check it before expensive loops and publication.
A tool can read files outside the project URI normalization occurs after authorization or symlinks are ignored Canonicalize first, enforce the allow-list, then open the file using the validated path.
The server hangs during shutdown Background watchers or tasks keep the host alive Cancel hosted services, await their completion and close the transport after pending responses finish.

Or skip the browser setup

If your workflow also needs reliable screenshots of documentation, test pages or generated UI, ScreenshotNeo provides a one-request website screenshot API and MCP server. It removes cookie and consent banners, newsletter popups and chat widgets before capture; bot checks, blank pages, failed loads and cache hits are not billed, and response headers identify the page verdict and billing status. AI agents can call its take_screenshot, get_page_info and capture_pdf tools through MCP.

Use the API documentation at https://screenshotneo.com/docs/ for the full option set. A basic call is:

curl -G 'https://api.screenshotneo.com/v1/shot' -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Equivalent clients:

import requests
r = requests.get('https://api.screenshotneo.com/v1/shot', params={'access_key': 'YOUR_API_KEY', 'url': 'https://stripe.com'}, timeout=90)
open('shot.webp', 'wb').write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

There is a free allowance of 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Frequently Asked Questions

Where should protocol logs go when the server uses stdio?

Write diagnostics to stderr or a file. Stdout is reserved exclusively for protocol messages, so even one startup banner can corrupt framing.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When is a separate MCP process preferable to an in-process adapter?

Use a separate process when you need independent restarts, stronger permission boundaries or different release cycles. An in-process adapter is simpler when the editor host already controls the same workspace and failure domain.

How should an MCP tool report a canceled operation?

Honor the cancellation token, stop work promptly and return the SDK’s structured cancellation/error result rather than emitting a partial success that the client may treat as complete.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.