Install
$ agentstack add mcp-managedcode-mcpgateway ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
ManagedCode.MCPGateway
[](https://github.com/managedcode/MCPGateway/actions/workflows/ci.yml) [](https://github.com/managedcode/MCPGateway/actions/workflows/release.yml) [](https://github.com/managedcode/MCPGateway/actions/workflows/codeql.yml) [](https://www.nuget.org/packages/ManagedCode.MCPGateway) [](LICENSE)
ManagedCode.MCPGateway is a .NET 10 library that turns local AITool instances and remote MCP servers into one searchable and invokable execution surface for Microsoft.Extensions.AI.
It is built on:
Microsoft.Extensions.AI- the official
ModelContextProtocol.NET SDK
ManagedCode.MCPGateway treats the official modelcontextprotocol/csharp-sdk as its MCP protocol baseline. The package builds on top of that SDK rather than replacing it with a narrower custom protocol layer, and the shipped gateway surface now includes aggregated MCP tools, gateway-owned and upstream MCP prompts, and MCP resources plus downstream MCP export support for completion, prompt list-change notifications, resource subscriptions, logging/setLevel, and task-backed MCP tool execution.
Install
dotnet add package ManagedCode.MCPGateway
What You Get
- one gateway for local
AIToolinstances and MCP tools - one prompt catalog for source-aware MCP prompts plus gateway-owned custom and composite prompts
- one resource catalog for MCP resources and resource templates aggregated across registered MCP sources
- one downstream MCP server export path over the aggregated tool, prompt, and resource catalogs with stable MCP protocol parity for completions, prompt list-change notifications, resource subscriptions, logging level changes, and task-backed tool execution
- one search API with default schema-aware Markdown-LD SPARQL graph ranking, opt-in vector ranking, and vector-first
Auto - one graph search API for schema/profile inspection, schema-aware SPARQL search, explicit allowlisted federation, graph evidence, and graph export
- one category-first routing API for advanced tool discovery flows
- one invocation API for both local tools and MCP tools
- additive catalog registration through
IMcpGatewayRegistry - full in-memory catalog control through
IMcpGatewayCatalogRuntime - prompt inspection and rendering through
IMcpGatewayPromptCatalog - resource inspection and reading through
IMcpGatewayResourceCatalog - DI-owned factory creation for isolated custom gateway instances
- reusable gateway meta-tools for chat loops
- optional warmup, caching, query normalization, and embedding reuse
- BenchmarkDotNet performance and allocation benchmarks for search, indexing, and meta-tools
After services.AddMcpGateway(...), the container exposes:
IMcpGatewayIMcpGatewayRegistryIMcpGatewayCatalogRuntimeIMcpGatewayGraphSearchIMcpGatewayPromptCatalogIMcpGatewayResourceCatalogIMcpGatewayFactoryMcpGatewayToolSet
Quickstart
using ManagedCode.MCPGateway;
using ManagedCode.MCPGateway.Abstractions;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddMcpGateway(options =>
{
options.AddTool(
"local",
AIFunctionFactory.Create(
static (string query) => $"github:{query}",
new AIFunctionFactoryOptions
{
Name = "github_search_repositories",
Description = "Search GitHub repositories by user query."
}));
options.AddStdioServer(
sourceId: "filesystem",
command: "npx",
arguments: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]);
});
await using var serviceProvider = services.BuildServiceProvider();
var gateway = serviceProvider.GetRequiredService();
var search = await gateway.SearchAsync("find github repositories");
var route = await gateway.RouteToolsAsync(new McpGatewayToolRouteRequest(
Query: "find github repositories",
MaxCategories: 3,
MaxToolsPerCategory: 2));
var invoke = await gateway.InvokeAsync(new McpGatewayInvokeRequest(
ToolId: search.Matches[0].ToolId,
Query: "managedcode"));
Default behavior:
SearchStrategy = GraphMarkdownLdGraphSource = GeneratedToolGraphMarkdownLdGraphSearchMode = HybridSearchQueryNormalization = TranslateToEnglishWhenAvailableDefaultSearchLimit = 5MaxSearchResults = 50MaxDescriptorLength = 16384- the index is built lazily on first list, search, or invoke
Hybrid means the Markdown-LD graph path runs schema-aware SPARQL search first, then uses the gateway-built ranked graph candidate path as supporting evidence and fuzzy fallback for noisy queries. It is not a tokenizer-only search path.
DefaultSearchLimit is the normal top-N result size when a caller does not ask for a count. MaxSearchResults is only a hard cap for caller-requested result sizes so LLM-facing tool discovery cannot accidentally flood context. MaxDescriptorLength bounds the generated descriptor text used by search and Markdown-LD graph indexing; hosts with very large tool schemas can raise it explicitly.
Register Tools And Sources
Register local tools during startup:
services.AddMcpGateway(options =>
{
options.AddTool(
"local",
AIFunctionFactory.Create(
static (string query) => $"weather:{query}",
new AIFunctionFactoryOptions
{
Name = "weather_search_forecast",
Description = "Search weather forecast and temperature information by city name."
}));
});
Register MCP sources during startup:
services.AddMcpGateway(options =>
{
options.AddHttpServer(
sourceId: "docs",
endpoint: new Uri("https://example.com/mcp"));
options.AddStdioServer(
sourceId: "filesystem",
command: "npx",
arguments: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]);
});
AddHttpServer(...) uses the official MCP C# SDK Streamable HTTP transport for modern remote MCP endpoints and keeps the source registered as an HTTP MCP source in gateway descriptors and downstream export metadata. Use the overload with HttpTransportMode only when a legacy endpoint requires AutoDetect or Sse. Use McpGatewayHttpServerOptions when a host needs the SDK HTTP transport knobs such as additional headers, connection timeout, known session id, session ownership, OAuth options, or SSE reconnection settings. The package does not set a transport timeout by default; hosts can pass one explicitly or own deadline policy through cancellation tokens and hosting infrastructure.
You can also register:
- existing
McpClientinstances throughAddMcpClient(...) - deferred
McpClientfactories throughAddMcpClientFactory(...)
If you need to add tools or sources after the container is built, use IMcpGatewayRegistry:
await using var serviceProvider = services.BuildServiceProvider();
var registry = serviceProvider.GetRequiredService();
var gateway = serviceProvider.GetRequiredService();
registry.AddTool(
"runtime",
AIFunctionFactory.Create(
static (string query) => $"status:{query}",
new AIFunctionFactoryOptions
{
Name = "project_status_lookup",
Description = "Look up project status by identifier or short title."
}));
var tools = await gateway.ListToolsAsync();
Registry updates invalidate the catalog. The next list, search, or invoke rebuilds the index automatically.
List And Render MCP Prompts
If registered MCP sources expose prompts, resolve IMcpGatewayPromptCatalog from DI and use it as the aggregated MCP prompt surface:
var promptCatalog = serviceProvider.GetRequiredService();
var prompts = await promptCatalog.ListPromptsAsync();
var prompt = await promptCatalog.GetPromptAsync(new McpGatewayPromptRequest(
SourceId: prompts[0].SourceId,
PromptName: prompts[0].PromptName,
Arguments: new Dictionary
{
["repository"] = "ManagedCode.MCPGateway"
}));
Prompt retrieval is source-aware on purpose. Different MCP servers may expose prompts with the same name, so callers should use the SourceId plus PromptName pair returned by ListPromptsAsync(). Identically named prompts are not merged implicitly by the gateway. If you want one higher-level prompt that combines or modifies multiple upstream prompts, register an explicit gateway-owned prompt instead.
Create Gateway-Owned Prompts
You can register custom prompts directly on the gateway and compose them from multiple upstream prompt sources:
services.AddMcpGateway(options =>
{
options.AddMcpClient("repo", repositoryClient, disposeClient: false);
options.AddMcpClient("ops", operationsClient, disposeClient: false);
options.AddPrompt(
new McpGatewayPrompt("release_review_bundle", async (context, cancellationToken) =>
{
var repositoryPrompt = await context.GetPromptAsync(
"repo",
"repository_triage_system_prompt",
new Dictionary
{
["repository"] = context.Arguments["repository"],
["locale"] = context.Arguments["locale"]
},
cancellationToken);
var deploymentPrompt = await context.GetPromptAsync(
"ops",
"deployment_review_system_prompt",
new Dictionary
{
["environment"] = context.Arguments["environment"]
},
cancellationToken);
return new GetPromptResult
{
Description = "Release review bundle prompt.",
Messages =
[
new PromptMessage
{
Role = Role.User,
Content = new TextContentBlock
{
Text = "Combine repository and deployment guidance into one review plan."
}
},
..repositoryPrompt!.Messages,
..deploymentPrompt!.Messages
]
};
})
{
DisplayName = "Release review bundle",
Description = "Combines repository and deployment review guidance into one prompt.",
Arguments =
[
new McpGatewayPromptArgumentDescriptor("repository", "Repository", "Repository name.", true),
new McpGatewayPromptArgumentDescriptor("environment", "Environment", "Deployment environment.", true),
new McpGatewayPromptArgumentDescriptor("locale", "Locale", "Preferred locale.", false)
],
CompleteAsync = static (context, cancellationToken) =>
{
cancellationToken.ThrowIfCancellationRequested();
var values = context.ArgumentName == "repository"
? new[] { "ManagedCode/MCPGateway", "ManagedCode/AIBase" }
: Array.Empty();
var matches = values
.Where(value => value.StartsWith(context.ArgumentValue, StringComparison.OrdinalIgnoreCase))
.ToList();
return ValueTask.FromResult(new CompleteResult
{
Completion = new Completion
{
Values = matches,
Total = matches.Count,
HasMore = false
}
});
}
});
});
Use gateway-owned prompts when you need:
- one prompt that combines several upstream prompts
- a modified or opinionated overlay on top of an upstream prompt
- custom prompt argument completion values exposed through downstream MCP
completion/complete
List And Read MCP Resources
If registered MCP sources expose resources, resolve IMcpGatewayResourceCatalog from DI and use it as the aggregated MCP resource surface:
var resourceCatalog = serviceProvider.GetRequiredService();
var resources = await resourceCatalog.ListResourcesAsync();
var templates = await resourceCatalog.ListResourceTemplatesAsync();
var issue = await resourceCatalog.ReadResourceAsync(new McpGatewayResourceRequest(
SourceId: templates[0].SourceId,
ResourceUri: "docs://issues/42"));
Resource reads are source-aware on purpose. Different MCP servers may expose the same URI scheme or URI shape, so callers should use the SourceId returned by ListResourcesAsync() or ListResourceTemplatesAsync(). For templated resources, expand the template to a concrete resource URI before calling ReadResourceAsync(...).
Export The Gateway As An MCP Server
If you want one downstream MCP endpoint over the aggregated gateway catalog, register an MCP server and add the gateway export:
var services = new ServiceCollection();
services.AddLogging();
services.AddMcpGateway(options =>
{
options.AddMcpClient("docs", docsClient, disposeClient: false);
options.AddMcpClient("ops", opsClient, disposeClient: false);
});
services.AddMcpServer()
.WithStdioServerTransport()
.WithMcpGatewayCatalog();
WithMcpGatewayCatalog() exports:
- aggregated tools through MCP
tools/listandtools/call - aggregated prompts through MCP
prompts/listandprompts/get - aggregated resources through MCP
resources/list,resources/templates/list, andresources/read - prompt and resource completions through MCP
completion/complete - forwarded
notifications/prompts/list_changedwhen upstream or gateway-owned prompts change - resource subscriptions through MCP
resources/subscribeandresources/unsubscribe, including forwardednotifications/resources/updated - logging level changes through MCP
logging/setLevel - task-backed tool execution through MCP
tools/callwithtaskmetadata plus MCPtasks/list,tasks/get,tasks/result, andtasks/cancel - forwarded
notifications/tasks/statusfor exported gateway tasks
Exported MCP tool names are canonical lowercase MCP-safe gateway ids. A unique upstream tool such as notion-fetch is exported as notion-fetch; when multiple sources expose the same normalized tool name, the gateway uses a source-qualified safe id such as docs_search_repository or ops_search_repository. Hash suffixes are added only when length or collision handling requires them. Names that would exceed 64 characters keep a readable normalized prefix and receive a deterministic hash suffix instead of being blindly truncated. McpGatewayToolDescriptor.ToolId, downstream MCP Tool.Name, and downstream tools/call names are the same value, and gateway-owned name resolution is case-insensitive so callers can vary casing without creating a different tool identity. Prompt names and exported resource names use the same lowercase MCP-safe source qualification, for example ops_deployment_review_system_prompt or local_release_review_bundle, while raw upstream SourceId, prompt name, tool name, and resource URI remain available as routing metadata and _meta values. Exported MCP resource URIs and URI templates are rewritten into gateway-owned opaque URIs so downstream resources/read calls route back to the correct upstream source even when multiple servers expose overlapping URI spaces. The same source-aware rewrite is also used for completion/complete, forwarded prompt list changes, and forwarded resource update notifications, so downstream clients always talk in terms of gateway-owned prompt names and resource URIs while the gateway proxies the corresponding upstream MCP operations. When an upstream MCP tool already advertises task support, the gateway preserves that contract on the exported tool and proxies th
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: managedcode
- Source: managedcode/MCPGateway
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.