{"id":50607114,"url":"https://github.com/diomonogatari/mcp-server-factory","last_synced_at":"2026-06-06T00:03:44.018Z","repository":{"id":338258719,"uuid":"1157109024","full_name":"diomonogatari/mcp-server-factory","owner":"diomonogatari","description":"Provides an in-memory test harness for .NET MCP servers, similar to WebApplicationFactory\u003cT\u003e for Web APIs.","archived":false,"fork":false,"pushed_at":"2026-02-13T17:27:39.000Z","size":36,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-14T00:00:15.068Z","etag":null,"topics":["dotnet","integration-testing","mcp","mcp-server","model-context-protocol","nuget","test-harness","testing"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/diomonogatari.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-13T12:48:25.000Z","updated_at":"2026-02-13T17:53:57.000Z","dependencies_parsed_at":"2026-02-14T00:00:23.715Z","dependency_job_id":null,"html_url":"https://github.com/diomonogatari/mcp-server-factory","commit_stats":null,"previous_names":["diomonogatari/mcp-server-factory"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/diomonogatari/mcp-server-factory","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diomonogatari%2Fmcp-server-factory","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diomonogatari%2Fmcp-server-factory/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diomonogatari%2Fmcp-server-factory/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diomonogatari%2Fmcp-server-factory/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/diomonogatari","download_url":"https://codeload.github.com/diomonogatari/mcp-server-factory/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/diomonogatari%2Fmcp-server-factory/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33964367,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-05T02:00:06.157Z","response_time":120,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dotnet","integration-testing","mcp","mcp-server","model-context-protocol","nuget","test-harness","testing"],"created_at":"2026-06-06T00:03:43.949Z","updated_at":"2026-06-06T00:03:44.013Z","avatar_url":"https://github.com/diomonogatari.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# McpServerFactory\r\n\r\n[![NuGet](https://img.shields.io/nuget/v/McpServerFactory.svg)](https://www.nuget.org/packages/McpServerFactory)\r\n[![NuGet Downloads](https://img.shields.io/nuget/dt/McpServerFactory.svg)](https://www.nuget.org/packages/McpServerFactory)\r\n[![CI](https://github.com/diomonogatari/mcp-server-factory/actions/workflows/ci.yml/badge.svg)](https://github.com/diomonogatari/mcp-server-factory/actions/workflows/ci.yml)\r\n[![codecov](https://codecov.io/gh/diomonogatari/mcp-server-factory/branch/main/graph/badge.svg)](https://codecov.io/gh/diomonogatari/mcp-server-factory)\r\n[![license](https://img.shields.io/github/license/diomonogatari/mcp-server-factory.svg?maxAge=2592000)](https://github.com/diomonogatari/mcp-server-factory/blob/main/LICENSE)\r\n![.NET](https://img.shields.io/badge/.net-8.0%20%7C%209.0%20%7C%2010.0-yellowgreen.svg)\r\n\r\nIn-memory integration **test harness** for .NET Model Context Protocol (MCP) servers.\r\n\r\n**Your MCP server's real contract isn't with your code — it's with the model.** The tool names\r\nand JSON schemas, the descriptions the agent reads, the error text it sees, the sampling\r\nround-trips it triggers. McpServerFactory boots your server **in-process** and connects a *real*\r\n`McpClient` over in-memory pipes, so you can assert on that contract in a plain unit test:\r\nbreakpoints on both sides, dependencies swapped for fakes, no subprocess, no ports, no Docker,\r\nno live model.\r\n\r\nThink `WebApplicationFactory\u003cT\u003e` — but for MCP.\r\n\r\n```csharp\r\n// one process: a real client ⇄ your real server, over in-memory pipes\r\nMcpTestClient client = await factory.CreateTestClientAsync();\r\n\r\nAssert.Equal(\"hello\", await client.CallToolForTextAsync(\r\n    \"echo\", new Dictionary\u003cstring, object?\u003e { [\"message\"] = \"hello\" }));\r\n```\r\n\r\n## What you can actually verify\r\n\r\n- A tool **exists**, and its input schema/description are what you think (`GetToolAsync`, `McpAssert.ToolExistsAsync`).\r\n- A tool returns the right **text** or **typed JSON** (`CallToolForTextAsync`, `CallToolForJsonAsync\u003cT\u003e`).\r\n- A tool **fails the way you intend** — `IsError` results throw instead of quietly passing a test (`CallToolExpectingErrorAsync`).\r\n- **Server-initiated sampling** does the right thing, answered by a fake model you control (`FakeSamplingHandler`).\r\n- The expected **notifications** fire — logging, progress, list-changed (`NotificationRecorder`).\r\n- Your **real DI graph** wires up, with the slow/external bits mocked (`configureServices` / `ConfigureHost`).\r\n\r\n\u003e **Scope:** by default the factory hosts the **tool/resource/prompt classes you register**\r\n\u003e over an in-memory transport. It does not auto-run your server's `Program.cs`. To exercise\r\n\u003e your real composition root (configuration, options, hosted services), use the\r\n\u003e [`ConfigureHost`](#testing-your-real-composition-root) hook.\r\n\r\n## Installation\r\n\r\n```bash\r\ndotnet add package McpServerFactory\r\n```\r\n\r\n## Template-based scaffolding\r\n\r\nInstall the template pack to bootstrap an MCP integration test project:\r\n\r\n```bash\r\ndotnet new install McpServerFactory.Templates\r\ndotnet new mcp-itest -n MyServer.Tests\r\n```\r\n\r\nThe `mcp-itest` template accepts `--McpServerFactoryVersion` to override the\r\n`McpServerFactory` package version; the default tracks the version of the\r\ntemplate pack you installed.\r\n\r\n## Quick start\r\n\r\n```csharp\r\nusing McpServerFactory.Testing;\r\nusing Microsoft.Extensions.DependencyInjection;\r\nusing ModelContextProtocol.Server;\r\nusing Xunit;\r\n\r\n[McpServerToolType]\r\npublic sealed class EchoTools\r\n{\r\n    [McpServerTool(Name = \"echo\")]\r\n    public string Echo(string message) =\u003e message;\r\n}\r\n\r\npublic class EchoTests\r\n{\r\n    [Fact]\r\n    public async Task Echo_ReturnsInput()\r\n    {\r\n        await using McpServerIntegrationFactory factory = new(\r\n            configureMcpServer: builder =\u003e builder.WithTools\u003cEchoTools\u003e());\r\n\r\n        // The factory owns the client; you do not dispose it yourself.\r\n        McpTestClient client = await factory.CreateTestClientAsync();\r\n\r\n        string text = await client.CallToolForTextAsync(\r\n            \"echo\",\r\n            new Dictionary\u003cstring, object?\u003e { [\"message\"] = \"hello\" });\r\n\r\n        Assert.Equal(\"hello\", text);\r\n    }\r\n}\r\n```\r\n\r\nUsing xUnit? The companion [`McpServerFactory.Xunit`](#boot-once-per-class-with-xunit) package\r\nboots the server once per test class via `IClassFixture\u003cT\u003e`.\r\n\r\n## Why use this library\r\n\r\n- **Fast and in-process** — no subprocess, ports, Docker, or stdio plumbing.\r\n- **Real protocol flow** — a real `McpClient` over a real (in-memory) transport: `tools/list`,\r\n  `tools/call`, resources, prompts, and server-initiated sampling all run end-to-end.\r\n- **Easy dependency overrides** — substitute services via `configureServices`.\r\n- **Debuggable** — set breakpoints on both sides; it's all one process.\r\n- **Framework-agnostic core** — works with xUnit, NUnit, and MSTest.\r\n\r\n## How it compares\r\n\r\n| Approach | Speed | No process/port | Breakpoints both sides | DI substitution | Exercises real `Program.cs` |\r\n| --- | --- | --- | --- | --- | --- |\r\n| **McpServerFactory** | fast | ✅ | ✅ | ✅ | via `ConfigureHost` |\r\n| stdio subprocess + `McpClient` | slow | ❌ | server only | ❌ | ✅ |\r\n| raw pipes + `StreamClientTransport` | fast | ✅ | ✅ | ✅ (manual) | ❌ |\r\n| MCP Inspector (manual) | n/a | ❌ | ❌ | ❌ | ✅ |\r\n\r\n**Use it when** you want fast, automated, in-process tests of your tools/resources/prompts and\r\nhandlers with dependency substitution. **Reach for a stdio subprocess instead** when you must\r\nvalidate the actual published binary, its real transport configuration, or process-level startup.\r\n\r\n## Used in the wild\r\n\r\n[`stash-mcp`](https://github.com/diomonogatari/stash-mcp) — a 40-tool MCP server for Bitbucket\r\nServer — tests its tools with McpServerFactory. It subclasses the factory, registers its real tool\r\nassembly, and swaps the live Bitbucket client, cache, and resilience services for fakes, so the\r\nwhole tool surface is exercised in-process without ever touching a Bitbucket instance:\r\n\r\n```csharp\r\npublic sealed class StashMcpTestFactory(Action\u003cStashMcpTestFactory\u003e? configureMocks = null)\r\n    : McpServerFactory.Testing.McpServerFactory\r\n{\r\n    public IBitbucketClient BitbucketClient { get; } = Substitute.For\u003cIBitbucketClient\u003e();\r\n\r\n    protected override void ConfigureMcpServer(IMcpServerBuilder builder) =\u003e\r\n        builder.WithToolsFromAssembly(typeof(ProjectTools).Assembly);\r\n\r\n    protected override void ConfigureServices(IServiceCollection services) =\u003e\r\n        services.AddSingleton(BitbucketClient); // ...plus cache, settings, resilience fakes\r\n}\r\n```\r\n\r\n## Testing tools, resources, prompts, and structured output\r\n\r\n`McpTestClient` wraps a real `McpClient` with test-shaped helpers (all pagination-safe):\r\n\r\n```csharp\r\nMcpTestClient client = await factory.CreateTestClientAsync();\r\n\r\nstring[] tools   = await client.GetToolNamesAsync();\r\nstring greeting  = await client.CallToolForTextAsync(\"greet\");\r\nMyDto result     = await client.CallToolForJsonAsync\u003cMyDto\u003e(\"compute\", args); // structured or JSON-text\r\nstring contents  = await client.ReadResourceTextAsync(\"resource://config\");\r\nstring[] prompts = await client.GetPromptNamesAsync();\r\n```\r\n\r\nTool failures (`IsError`) no longer pass silently: `CallToolForTextAsync` throws\r\n`McpToolCallException`, and `CallToolExpectingErrorAsync` asserts the negative path. The\r\nframework-agnostic `McpAssert` helpers (`ToolExistsAsync`, `Succeeded`, `IsError`, `TextEquals`)\r\nwork under any test framework.\r\n\r\n## Testing server-initiated sampling\r\n\r\nIf your server calls the model (server-initiated sampling), wire a deterministic fake so tests\r\nnever need a real LLM:\r\n\r\n```csharp\r\nFakeSamplingHandler sampling = FakeSamplingHandler.Returning(\"42\");\r\n\r\nawait using McpServerIntegrationFactory factory = new(\r\n    configureMcpServer: builder =\u003e builder.WithTools\u003cAskTools\u003e(),\r\n    options: new McpServerFactoryOptions\r\n    {\r\n        ConfigureClient = client =\u003e client.UseSamplingHandler(sampling),\r\n    });\r\n\r\nMcpTestClient client = await factory.CreateTestClientAsync();\r\nstring answer = await client.CallToolForTextAsync(\r\n    \"ask\", new Dictionary\u003cstring, object?\u003e { [\"question\"] = \"...\" });\r\n\r\nAssert.Single(sampling.ReceivedRequests); // assert what the server asked the model\r\n```\r\n\r\n`ConfigureClient` exposes the full `McpClientOptions`, so you can also declare elicitation, roots,\r\nor notification handlers. Capture server-sent notifications (logging, progress, list-changed) with\r\n`NotificationRecorder.Attach(client.Inner)` and `await recorder.WaitForMethodAsync(...)`.\r\n\r\n## Testing your real composition root\r\n\r\nBy default the factory hosts the classes you register. To exercise the same registration your\r\nserver's `Program.cs` uses — real configuration binding, options, and hosted services — supply\r\n`ConfigureHost`. The factory always owns the MCP server registration and the in-memory transport,\r\nso configure everything **except** the transport; register tools/resources/prompts via\r\n`configureMcpServer`:\r\n\r\n```csharp\r\nawait using McpServerIntegrationFactory factory = new(\r\n    configureMcpServer: builder =\u003e builder.WithTools\u003cMyTools\u003e(),\r\n    options: new McpServerFactoryOptions\r\n    {\r\n        ConfigureHost = builder =\u003e\r\n        {\r\n            builder.Configuration.AddInMemoryCollection(/* test config */);\r\n            builder.Services.AddMyDomainServices();   // your real registration, minus the transport\r\n        },\r\n    });\r\n```\r\n\r\n## Boot once per class with xUnit\r\n\r\nInstall the companion package and derive a fixture:\r\n\r\n```bash\r\ndotnet add package McpServerFactory.Xunit\r\n```\r\n\r\n```csharp\r\nusing McpServerFactory.Testing.Xunit;\r\n\r\npublic sealed class EchoFixture : McpServerFixture\r\n{\r\n    protected override void ConfigureMcpServer(IMcpServerBuilder builder) =\u003e builder.WithTools\u003cEchoTools\u003e();\r\n}\r\n\r\npublic sealed class EchoTests(EchoFixture fixture) : IClassFixture\u003cEchoFixture\u003e\r\n{\r\n    [Fact]\r\n    public async Task Echoes() =\u003e\r\n        Assert.Equal(\"hi\", await fixture.TestClient.CallToolForTextAsync(\r\n            \"echo\", new Dictionary\u003cstring, object?\u003e { [\"message\"] = \"hi\" }));\r\n}\r\n```\r\n\r\n## Behavioral guarantees\r\n\r\n- `CreateClientAsync` / `CreateTestClientAsync` are thread-safe and idempotent per factory instance.\r\n- Concurrent calls return the same connected client; **the factory owns and disposes it** — you do\r\n  not need to dispose the returned client yourself.\r\n- Startup failures do not leak the temporary host instance or its pipes.\r\n- `DisposeAsync` is safe to call multiple times and is bounded by `ShutdownTimeout` (it will not hang\r\n  teardown).\r\n- `CreateClientAsync` throws `ObjectDisposedException` after disposal.\r\n- Need independent server instances (isolation)? Create multiple factory instances — each owns its\r\n  own host. A single factory exposes one in-memory session.\r\n\r\n## Compatibility and support\r\n\r\n- Target frameworks: `net8.0`, `net9.0`, `net10.0`.\r\n- MCP SDK dependency: `ModelContextProtocol` `1.4.0`.\r\n- Compatibility promise: each package release is validated against the pinned MCP SDK version on all\r\n  target frameworks.\r\n- Upgrade policy: MCP SDK bumps are explicit and called out in [CHANGELOG.md](CHANGELOG.md). Now that\r\n  the MCP SDK is stable, `McpServerFactory` follows [Semantic Versioning](https://semver.org/): an MCP\r\n  SDK change that breaks this library's public surface ships as a new major version.\r\n\r\n| McpServerFactory | MCP SDK | Target frameworks |\r\n| --- | --- | --- |\r\n| 1.0.x | 1.4.0 | net8.0, net9.0, net10.0 |\r\n| 0.2.x | 0.4.0-preview.3 | net8.0, net9.0, net10.0 |\r\n| 0.1.x | 0.4.0-preview.3 | net10.0 |\r\n\r\n## API overview\r\n\r\n- `McpServerFactory` / `McpServerIntegrationFactory`\r\n  - Start an in-process server host; create a connected client via `CreateClientAsync()` or a\r\n    factory-owned wrapper via `CreateTestClientAsync()`.\r\n  - Expose `Services` for DI validation after startup.\r\n- `McpServerFactoryOptions`\r\n  - Configure server identity, timeouts, instructions, logging, the client (`ConfigureClient`), and\r\n    the host composition root (`ConfigureHost`).\r\n- `McpTestClient`\r\n  - Wrapper with tool, resource, prompt, structured-output, and server-metadata helpers.\r\n- `FakeSamplingHandler` / `NotificationRecorder` / `McpAssert`\r\n  - Deterministic sampling, notification capture, and framework-agnostic assertions.\r\n- `McpServerFactory.Xunit` (separate package)\r\n  - `McpServerFixture` for `IClassFixture\u003cT\u003e` boot-once-per-class.\r\n\r\n## Logging in test output\r\n\r\nBy default, host logging providers are suppressed to keep test output clean.\r\nTo enable custom logging during tests:\r\n\r\n```csharp\r\nusing Microsoft.Extensions.Logging;\r\n\r\nvar options = new McpServerFactoryOptions\r\n{\r\n    SuppressHostLogging = false,\r\n    ConfigureLogging = logging =\u003e logging.SetMinimumLevel(LogLevel.Debug),\r\n};\r\n```\r\n\r\n## Release notes\r\n\r\nSee [CHANGELOG.md](CHANGELOG.md) for release history and upcoming changes.\r\n\r\n## Samples\r\n\r\n- Minimal runnable sample: [`samples/MinimalSmoke`](samples/MinimalSmoke)\r\n\r\n## Repository layout\r\n\r\n- `src/McpServerFactory` — reusable factory library (framework-agnostic core).\r\n- `src/McpServerFactory.Xunit` — xUnit fixtures (`McpServerFixture`).\r\n- `tests/McpServerFactory.Tests` — unit/integration-focused library tests.\r\n- `templates/McpServerFactory.Templates` — `dotnet new` template pack.\r\n- `samples/MinimalSmoke` — runnable sample console app.\r\n- `docs` — architecture notes and usage guidance.\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdiomonogatari%2Fmcp-server-factory","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdiomonogatari%2Fmcp-server-factory","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdiomonogatari%2Fmcp-server-factory/lists"}