Lifter.ModelContextProtocol.WatsonWebserver
0.1.0
dotnet add package Lifter.ModelContextProtocol.WatsonWebserver --version 0.1.0
NuGet\Install-Package Lifter.ModelContextProtocol.WatsonWebserver -Version 0.1.0
<PackageReference Include="Lifter.ModelContextProtocol.WatsonWebserver" Version="0.1.0" />
<PackageVersion Include="Lifter.ModelContextProtocol.WatsonWebserver" Version="0.1.0" />
<PackageReference Include="Lifter.ModelContextProtocol.WatsonWebserver" />
paket add Lifter.ModelContextProtocol.WatsonWebserver --version 0.1.0
#r "nuget: Lifter.ModelContextProtocol.WatsonWebserver, 0.1.0"
#:package Lifter.ModelContextProtocol.WatsonWebserver@0.1.0
#addin nuget:?package=Lifter.ModelContextProtocol.WatsonWebserver&version=0.1.0
#tool nuget:?package=Lifter.ModelContextProtocol.WatsonWebserver&version=0.1.0
🔌 Lifter.ModelContextProtocol.WatsonWebserver
Streamable HTTP transport for the official MCP C# SDK, hosted on WatsonWebserver
Host a Model Context Protocol server on any WatsonWebserver route, in any process, without ASP.NET Core.
await using var mcp = webserver.MapMcp(serverOptions);
That is the whole integration. Everything else — the tools, the prompts, the protocol — is the official MCP C# SDK.
🚀 Why
The official SDK ships two ways to host a server: standard input/output, and
ModelContextProtocol.AspNetCore. Plenty of .NET processes already serve HTTP with WatsonWebserver
and have no ASP.NET Core in them: desktop applications, embedded devices, point-of-sale terminals,
background services that must stay small, single-file AOT binaries.
This package is the glue between Watson and the SDK's own
ModelContextProtocol.Server.StreamableHttpServerTransport — the very object
ModelContextProtocol.AspNetCore drives. It is not a second implementation of the protocol: the
sessions, the JSON-RPC framing, the SSE streams and the tool invocation are all the SDK's, so a
client cannot tell the difference between a server hosted here and one hosted on Kestrel.
- No ASP.NET Core. One dependency on
Watson, one onModelContextProtocol.Core. - AOT-compatible. No reflection-based JSON: the SDK's
McpJsonUtilitiescontexts do the work. - Mounts on an existing server. Your other Watson routes keep working, on the same listener.
- Authentication stays yours. The endpoint sits behind Watson's own authentication route.
📦 Install
dotnet add package Lifter.ModelContextProtocol.WatsonWebserver
⚡ Use
using System.ComponentModel;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using Lifter.ModelContextProtocol.WatsonWebserver;
using WatsonWebserver;
using WatsonWebserver.Core;
var serverOptions = new McpServerOptions
{
ServerInfo = new Implementation { Name = "watson-sample", Version = "1.0.0" },
};
serverOptions.WithTools<SampleTools>();
var webserver = new Webserver(new WebserverSettings("127.0.0.1", 9000), DefaultRouteAsync);
await using var mcp = webserver.MapMcp(serverOptions, options => options.Path = "/mcp");
webserver.Start();
[McpServerToolType]
internal sealed class SampleTools
{
[McpServerTool(Name = "echo"), Description("Echoes the message back.")]
public static string Echo(string message) => $"echo: {message}";
}
A runnable version of this is in samples/McpWatsonSample.
🧰 Registering tools and prompts
The SDK's WithTools<T>() builder lives in the hosting package, which exists only for
dependency-injection hosts and drags ASP.NET Core's generic host in with it. This package offers the
same shape as extension methods on McpServerOptions, built on the SDK's own
McpServerTool.Create / McpServerPrompt.Create:
serverOptions
.WithTools<SampleTools>() // every [McpServerTool] method on the type
.WithTools(typeof(OtherTools), services) // instance methods get their target from the container
.WithTool(([Description("Adds.")] int a, int b) => a + b)
.WithPrompts<SamplePrompts>();
Static methods need nothing. Instance methods get a target per call: from the IServiceProvider you
pass (through ActivatorUtilities, so constructor injection works) or from the type's public
constructor when you pass none.
To hand the whole server a container — so tools can take services as parameters — use the
IServiceProvider overload of MapMcp:
await using var mcp = webserver.MapMcp(serverOptions, services, options => options.Path = "/mcp");
🗂️ Sessions vs stateless
| Sessions (default) | Stateless = true |
|
|---|---|---|
Mcp-Session-Id |
issued on initialize, required afterwards |
ignored, never issued |
| Server instance | one per session, alive between requests | one per request |
GET stream |
server-to-client notifications | 405 |
DELETE |
ends the session | 405 |
| State in a tool | survives across calls | does not |
| Idle sessions | disposed after IdleTimeout (default 2h) |
nothing to dispose |
Stateless is what you want when the process is restarted often, when several processes sit behind a load balancer, or when the tools are pure functions. Sessions are what you want when a tool holds something between calls, or when the server has to push notifications to the client.
webserver.MapMcp(serverOptions, options =>
{
options.Stateless = true;
options.Path = "/mcp";
});
⚙️ Options
| Option | Default | What it does |
|---|---|---|
Path |
/mcp |
The route the endpoint answers on |
Stateless |
false |
One server per request, no session |
IdleTimeout |
2 hours | How long a quiet session lives |
IdleSweepInterval |
5 seconds | How often idle sessions are swept |
AllowedOrigins |
empty (all) | Origin values accepted; populate when a browser can reach the listener |
SupportedProtocolVersions |
2024-11-05 … 2025-11-25 |
Revisions accepted in MCP-Protocol-Version |
RequireAuthentication |
true |
Register after Watson's authentication phase |
Services |
null |
Service provider handed to each session's McpServer |
LoggerFactory |
null |
Logger factory for the transport and the server |
ConfigureSession |
null |
Configure a session's server options from the request that opened it |
🔐 Authentication
This package does not do OAuth, and does not want to. With RequireAuthentication left at true
the endpoint is registered in Watson's post-authentication routing group, so
Routes.AuthenticateRequest runs first and can reject the request before MCP ever sees it:
webserver.Routes.AuthenticateRequest = async context =>
{
if (context.Request.Headers["Authorization"] != $"Bearer {token}")
{
context.Response.StatusCode = 401;
await context.Response.Send(context.Token);
}
};
From inside a tool, the request that carried the call is reachable through
McpWatsonRequestContext.Current — the execution context of the HTTP request flows into the
server's handling of the message it carried:
[McpServerTool(Name = "whoami"), Description("Says who is calling.")]
public static string WhoAmI() =>
McpWatsonRequestContext.Current?.Header("X-Tenant") ?? "(anonymous)";
It exposes the Watson HttpContextBase itself (HttpContext) and the session id, and is null
outside an HTTP request — in a notification the server raises on its own, for instance.
🤖 Connecting Claude Code
claude mcp add --transport http watson-sample http://127.0.0.1:9000/mcp
Any MCP client works the same way: the SDK's HttpClientTransport with
HttpTransportMode.StreamableHttp, the TypeScript SDK, MCP Inspector.
✅ What it does
POST— JSON-RPC in,text/event-streamout,202 Acceptedfor a body that produced no response.GET— the standalone server-to-client SSE stream of a session.DELETE— ends a session (204),404for one that is already gone.Mcp-Session-Idissued oninitializeand echoed on every answer;404with JSON-RPC-32001for an unknown one,400for a non-initializerequest that carries none.406when the client does not accept bothapplication/jsonandtext/event-stream,400for a body that is not a JSON-RPC message,405(withAllow) for any other method.MCP-Protocol-Versionvalidated againstSupportedProtocolVersions, answering an unsupported one with the supported list so the client can negotiate down.Originchecked againstAllowedOriginswhen you populate it.
🚫 What it does not do
- Resumability.
Last-Event-IDreplay needs an event store; the SDK has the hook (StreamableHttpServerTransport.EventStreamStore) and this package does not wire it up yet. - Session migration between processes, and the
2026-07-28per-request-metadata revision. - OAuth. Authorization is a Watson route, see above.
- Replace
ModelContextProtocol.AspNetCorewhere you already run ASP.NET Core. Use that one.
📐 Compatibility
| Package | Version |
|---|---|
ModelContextProtocol.Core |
2.2.x |
Watson |
7.0.x |
| .NET | 8, 9, 10 |
| MCP protocol | 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25 |
The tests drive the endpoint with the official ModelContextProtocol client over a real Watson
listener on a loopback port, so what passes there works against a real MCP client.
🤝 Contributing
See CONTRIBUTING.md. In short: Conventional Commits, zero warnings
(TreatWarningsAsErrors is on), and a test in tests/ for anything that changes behaviour.
📄 License
MIT. See LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- ModelContextProtocol.Core (>= 2.2.0)
- Watson (>= 7.0.13)
-
net8.0
- ModelContextProtocol.Core (>= 2.2.0)
- Watson (>= 7.0.13)
-
net9.0
- ModelContextProtocol.Core (>= 2.2.0)
- Watson (>= 7.0.13)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0 | 43 | 9/18/2026 |