# MCPGateway

> Searchable MCP and AITool gateway for .NET, built on Microsoft.Extensions.AI with embedding-based tool discovery, lexical fallback, and unified execution

- **Type:** MCP server
- **Install:** `agentstack add mcp-managedcode-mcpgateway`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [managedcode](https://agentstack.voostack.com/s/managedcode)
- **Installs:** 0
- **Category:** [AI & ML](https://agentstack.voostack.com/c/ai-and-ml)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [managedcode](https://github.com/managedcode)
- **Source:** https://github.com/managedcode/MCPGateway

## Install

```sh
agentstack add mcp-managedcode-mcpgateway
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## 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`](https://github.com/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

```bash
dotnet add package ManagedCode.MCPGateway
```

## What You Get

- one gateway for local `AITool` instances 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:

- `IMcpGateway`
- `IMcpGatewayRegistry`
- `IMcpGatewayCatalogRuntime`
- `IMcpGatewayGraphSearch`
- `IMcpGatewayPromptCatalog`
- `IMcpGatewayResourceCatalog`
- `IMcpGatewayFactory`
- `McpGatewayToolSet`

## Quickstart

```csharp
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 = Graph`
- `MarkdownLdGraphSource = GeneratedToolGraph`
- `MarkdownLdGraphSearchMode = Hybrid`
- `SearchQueryNormalization = TranslateToEnglishWhenAvailable`
- `DefaultSearchLimit = 5`
- `MaxSearchResults = 50`
- `MaxDescriptorLength = 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:

```csharp
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:

```csharp
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 `McpClient` instances through `AddMcpClient(...)`
- deferred `McpClient` factories through `AddMcpClientFactory(...)`

If you need to add tools or sources after the container is built, use `IMcpGatewayRegistry`:

```csharp
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:

```csharp
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:

```csharp
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:

```csharp
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:

```csharp
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/list` and `tools/call`
- aggregated prompts through MCP `prompts/list` and `prompts/get`
- aggregated resources through MCP `resources/list`, `resources/templates/list`, and `resources/read`
- prompt and resource completions through MCP `completion/complete`
- forwarded `notifications/prompts/list_changed` when upstream or gateway-owned prompts change
- resource subscriptions through MCP `resources/subscribe` and `resources/unsubscribe`, including forwarded `notifications/resources/updated`
- logging level changes through MCP `logging/setLevel`
- task-backed tool execution through MCP `tools/call` with `task` metadata plus MCP `tasks/list`, `tasks/get`, `tasks/result`, and `tasks/cancel`
- forwarded `notifications/tasks/status` for 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](https://github.com/managedcode)
- **Source:** [managedcode/MCPGateway](https://github.com/managedcode/MCPGateway)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-managedcode-mcpgateway
- Seller: https://agentstack.voostack.com/s/managedcode
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
