Skip to main content

MCP Server

Rag.NET can be exposed as a Model Context Protocol (MCP) server, making it consumable by any MCP-compatible host — Claude Desktop, Cursor, LM Studio, and others — without custom integration code.

Packages​

PackagePurpose
Rag.NET.McpMCP tools (rag_retrieve, rag_ask, rag_ingest) — resolves IRagPipeline from DI
Rag.NET.ApiASP.NET Core REST API exposing IRagPipeline as HTTP endpoints
Rag.NET.Api.ClientIRagPipeline implementation that calls Rag.NET.Api over HTTP
Rag.NET.Api.GrpcgRPC service exposing IRagPipeline
Rag.NET.Api.Grpc.ClientIRagPipeline implementation that calls Rag.NET.Api.Grpc over gRPC
Rag.NET.Mcp.Toolragnet-mcp dotnet global tool — self-contained, no code required
Rag.NET.HostingConfiguration-driven pipeline wiring (AddRagNetPipelineFromConfiguration) shared by Rag.NET.Mcp.Tool and the ragnet CLI — published, and usable directly if you want the same configuration-driven wiring in your own host

Deployment patterns​

The MCP server and the RAG pipeline run in the same process. Rag.NET.Mcp resolves IRagPipeline directly from DI — no extra HTTP hop, no auth between MCP and the pipeline.

dotnet add package Rag.NET.Mcp
dotnet add package ModelContextProtocol.AspNetCore
var builder = WebApplication.CreateBuilder(args);

// 1. Register AI services (embedding + chat)
builder.Services.AddEmbeddingGenerator(...);
builder.Services.AddChatClient(...);

// 2. Configure Rag.NET pipeline
builder.Services.AddRagNet(rag => rag
.UsePgVector("Host=localhost;Database=ragdb;Username=postgres;Password=secret",
vectorDimensions: 1536));

// 3. Add MCP server on top — resolves IRagPipeline from the same DI container.
// Rag.NET.Mcp does not reference ModelContextProtocol.AspNetCore (that would force an ASP.NET
// Core dependency on every stdio-only consumer), so the guarded HTTP transport lives in
// Rag.NET.Mcp.AspNetCore:
builder.Services
.AddRagNetMcpServer()
.WithRagNetHttpTransport(o => o.ApiKey = "your-secret");
// .WithStdioTransport(); // use this instead for Claude Desktop (subprocess)

var app = builder.Build();

// The key check is attached to the endpoints MapRagNetMcp maps — there is no separate middleware
// to remember, and no way to map the endpoints without it.
app.MapRagNetMcp("/mcp");
await app.RunAsync();

Pattern B — Shared backend (REST)​

Run one Rag.NET REST backend and point multiple MCP server instances at it. Useful when you want a single, centrally-managed knowledge base.

Claude Desktop ──┐
Cursor ├──→ MCP Server (Rag.NET.Mcp + Rag.NET.Api.Client)
LM Studio ───────┘ ↓ X-Api-Key
Backend (Rag.NET + Rag.NET.Api)

Backend app:

dotnet add package Rag.NET
dotnet add package Rag.NET.Api
builder.Services.AddRagNet(...);
builder.Services.AddRagNetApi(o => o.ApiKeys = ["key-for-mcp-1", "key-for-mcp-2"]);
app.UseRagNetApiAuthentication();
app.MapRagNetApi();

MCP proxy app:

dotnet add package Rag.NET.Mcp
dotnet add package Rag.NET.Api.Client
builder.Services.AddRagNetApiClient(o =>
{
o.BaseUrl = "https://rag-backend.internal";
o.ApiKey = "key-for-mcp-1";
});
builder.Services
.AddRagNetMcpServer()
.WithStdioTransport();

Pattern C — Shared backend (gRPC)​

Same as Pattern B but using gRPC for the MCP server → backend hop. Preferred for internal service-to-service communication: strongly typed, lower overhead, and native server-streaming.

# Backend
dotnet add package Rag.NET.Api.Grpc

# MCP proxy
dotnet add package Rag.NET.Mcp
dotnet add package Rag.NET.Api.Grpc.Client
// Backend
builder.Services.AddRagNetGrpcApi(o => o.ApiKeys = ["key-for-mcp-1"]);
app.MapRagNetGrpcApi();

// MCP proxy
builder.Services.AddRagNetGrpcClient(o =>
{
o.BaseUrl = "https://rag-backend.internal:5001";
o.ApiKey = "key-for-mcp-1";
});
builder.Services.AddRagNetMcpServer().WithStdioTransport();

Pattern D — dotnet global tool (no code required)​

Install and run without writing any code. You supply an appsettings.json with your pipeline configuration (embedding provider, vector store, chat client) and the tool starts the MCP server.

dotnet tool install -g Rag.NET.Mcp.Tool

# stdio (Claude Desktop subprocess)
ragnet-mcp

# HTTP/SSE on port 5050
ragnet-mcp --transport http --port 5050 --api-key your-secret

The tool wires its pipeline — chat client, embedding generator, vector store — from an appsettings.json next to its working directory, or from environment variables (__ as the section separator, e.g. RagNet__ChatClient__ApiKey). A sample appsettings.sample.json, showing every supported VectorStore:Kind, ships alongside the installed binaries; copy it to appsettings.json and fill in real values.

{
"RagNet": {
"ChatClient": { "Endpoint": "https://api.openai.com/v1", "ApiKey": "…", "Model": "gpt-4o-mini" },
"Embeddings": { "Endpoint": "https://api.openai.com/v1", "ApiKey": "…", "Model": "text-embedding-3-small", "VectorDimensions": 1536 },
"VectorStore": {
"Kind": "InMemory | Qdrant | PgVector",
"Qdrant": { "Host": "…", "Port": 6334, "CollectionName": "…" },
"PgVector": { "ConnectionString": "…" }
}
}
}

VectorDimensions lives under Embeddings, not under the store, because it is a property of the embedding model — every store merely has to agree with it. The chat client and embedding generator both go through one OpenAI-compatible endpoint, which covers OpenAI, Azure OpenAI, OpenRouter, Ollama, and LM Studio. The vector store is one of three kinds: InMemory (the default, zero setup, but every ingested document is lost when the process exits — a warning is logged at startup), Qdrant, or PgVector.

note

A misconfigured setting — an unrecognised Kind, a missing Endpoint/Model, an absent or non-positive VectorDimensions — fails at startup with a message naming both the setting and the RagNet:… configuration key that fixes it, rather than failing the first time an MCP client calls a tool.

Need a provider outside that bounded set — Weaviate, Pinecone, Chroma, Azure AI Search, ONNX embeddings, a bespoke IChatClient? Host Rag.NET.Mcp directly in your own application (Pattern A above) and register whatever you like; this tool covers the bounded, OpenAI-compatible set above and nothing wider.


MCP tools​

Three tools are exposed to the LLM host:

rag_retrieve​

Search the knowledge base and return ranked chunks.

ParameterTypeDefaultDescription
querystring—The natural-language query
topKint5Maximum number of results
useHybridbooltrueHybrid retrieval. Not always BM25 — see the note below

Returns a JSON array of SearchResult objects with Chunk.Text, Chunk.DocumentId, Chunk.ChunkIndex, Score, and Chunk.Metadata.

rag_ask​

Retrieve relevant chunks and generate a grounded answer.

ParameterTypeDefaultDescription
querystring—The question
topKint5Chunks to retrieve
useHybridbooltrueHybrid search

Returns a JSON object with Answer (string) and Sources (array of SearchResult).

rag_ingest​

Add a document to the knowledge base at runtime.

ParameterTypeDefaultDescription
contentstring—Document text (inline)
documentIdstring?autoIdentifies the document. Does not enable update or delete — see the note below
fileNamestring?{id}.txtUsed to infer content type
contentTypestring?—MIME type override
tagsstring[]?—Metadata as key=value pairs, e.g. ["author=Alice", "topic=AI"]. Every value is stored as a string — see the note below

Returns { "DocumentId": "...", "ChunksStored": N }.

What these three parameters do not do

Corrected 2026-08-12 after an audit of the tool surface against the code. All three were documented as doing more than they do.

documentId does not give you updates or deletes. Updating an existing document needs IngestionOptions.Overwrite, and rag_ingest never passes an IngestionOptions at all — so re-ingesting the same id appends a second copy rather than replacing the first. There is no delete tool either, although IRagPipeline.DeleteAsync exists in the library. Over MCP, documents can currently be created but not changed or removed. Tracked in #161.

useHybrid does not always run BM25. When the vector store implements IHybridSearchable and MinScore is 0.0 — which it always is over MCP, since no tool exposes it — the store's own server-side fusion runs and no BM25 arm executes. You get hybrid retrieval, but the store's, not this library's.

tags values are always strings. The tool assigns every value as a string, so year=2024 is stored as "2024" and a typed MetadataFilter comparing it against the number 2024 will not match. Use string comparisons, or set typed metadata through the library rather than the tool.


Claude Desktop configuration​

Add Rag.NET as an MCP server in Claude Desktop's config file.

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json Windows: %APPDATA%\Claude\claude_desktop_config.json

{
"mcpServers": {
"rag-net": {
"command": "dotnet",
"args": ["run", "--project", "/path/to/your/RagMcpHost"],
"env": {}
}
}
}

Authentication​

TransportMechanism
stdioNone — process boundary is the security boundary
HTTP/SSEX-Api-Key header; configure ApiKeys array for rotation and per-client revocation
MCP over HTTP (ragnet-mcp --transport http)X-Api-Key header; --api-key or RAGNET_MCP_API_KEY, or --allow-anonymous to opt out deliberately
MCP over HTTP (your own host)X-Api-Key header; WithRagNetHttpTransport(o => o.ApiKey = ...) from Rag.NET.Mcp.AspNetCore, or o.AllowAnonymous = true to opt out deliberately. Configuring neither throws at startup — this surface exposes rag_ingest, so serving it unauthenticated is a decision rather than a default
gRPCx-api-key metadata header; validated by a server-side interceptor

Both AddRagNetApi and AddRagNetGrpcApi require an explicit authentication decision at registration: configure at least one key, or opt out deliberately with AllowAnonymous = true (e.g. behind a trusted gateway). Registering with an empty ApiKeys and no opt-out throws at startup, and setting both at once is rejected as a contradiction. At request time the middleware/interceptor fail closed — no keys and no opt-out means the request is refused, never silently served.

ragnet-mcp --transport http follows the same rule and refuses to start without one of the two: it exposes ingest, retrieve and ask, and binds every interface rather than loopback. Building your own MCP server on Rag.NET.Mcp instead? McpApiKeyAuthorization.IsAuthorized is the same decision the tool uses — the middleware is yours to write, because it is ASP.NET Core and that package deliberately does not reference a web framework, but the comparison is not something to reimplement.

Multiple keys are supported for rotation without downtime:

services.AddRagNetApi(o => o.ApiKeys = ["key-a", "key-b"]);
// Rotate: add "key-c", redeploy, then remove "key-a"

REST API endpoints​

When using Rag.NET.Api, the following endpoints are available:

MethodPathMaps to
POST/rag/ingestIngestAsync
POST/rag/retrieveRetrieveAsync
POST/rag/askAskAsync
GET/rag/ask/stream?query=...AskStreamingAsync (SSE)
DELETE/rag/documents/{id}DeleteAsync

The route prefix /rag is configurable via RagApiOptions.RoutePrefix.