How to Log Incoming Requests in a .NET MCP Server
Use ASP.NET Core HTTP logging for the envelope and an MCP incoming filter for JSON-RPC methods, with safe, production-ready code.
Direct answer: log a .NET MCP server at two layers. Add ASP.NET Core HTTP logging early in the pipeline for the request/response envelope, and add the C# SDK’s incoming message filter to record parsed JSON-RPC methods such as tools/call. Send both through ILogger; keep bodies, authorization headers, cookies, and tool arguments out of normal logs.
1. Choose the layer you need
| Layer | What you see | Best instrument |
|---|---|---|
| HTTP transport | Method, path, status, timing, selected headers | ASP.NET Core HttpLoggingMiddleware |
| MCP protocol | Parsed JSON-RPC type, method name, request id | SDK AddIncomingFilter plus ILogger |
| Client-facing MCP notifications | Messages intentionally sent to an MCP client | MCP Logging utility (separate; deprecated in current v2 docs) |
HTTP logging cannot tell you which MCP method was inside a POST. The message filter cannot show a rejected request that never reached JSON-RPC parsing. In production, use both when you need transport and protocol visibility.
2. Add HTTP request and response logging
Install the ASP.NET Core HTTP logging package that matches your target framework. Register it before building the app and place UseHttpLogging before the MCP endpoint mapping. Microsoft documents the middleware and its privacy implications in HTTP logging in .NET and ASP.NET Core.
using Microsoft.AspNetCore.HttpLogging;
using ModelContextProtocol.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpLogging(options =>
{
// Start with metadata. Do not enable bodies by default.
options.LoggingFields = HttpLoggingFields.RequestPropertiesAndHeaders
| HttpLoggingFields.ResponsePropertiesAndHeaders;
// Allowlist only headers that are useful and safe for your service.
options.RequestHeaders.Add("Accept");
options.RequestHeaders.Add("Content-Type");
options.RequestHeaders.Add("User-Agent");
options.ResponseHeaders.Add("Content-Type");
options.ResponseHeaders.Add("Content-Length");
});
builder.Services.AddMcpServer();
var app = builder.Build();
// Put this early so it observes the MCP route and failures in later middleware.
app.UseHttpLogging();
app.MapMcp();
app.Run();
Set the middleware category to Information if your logging configuration filters it:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
}
}
}
Control fields and headers deliberately
HttpLoggingFields.All is convenient while diagnosing a local issue, but it can capture query strings, cookies, authorization values, and bodies. Prefer an explicit field set and header allowlists. Body logging is opt-in and can add latency and storage cost; if you enable it temporarily, use a size limit and redact before export. Endpoint-specific settings or an IHttpLoggingInterceptor can narrow logging for sensitive routes.
3. Log parsed MCP methods with an incoming filter
The C# SDK filter runs on the parsed JSON-RPC message before request dispatch. The official filter example uses AddIncomingFilter, checks for JsonRpcRequest, and writes the method through ILogger. Package and interface names can change, so confirm the version used by your project.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Protocol;
builder.Services
.AddMcpServer()
.WithMessageFilters(filters =>
{
filters.AddIncomingFilter(next => async (context, cancellationToken) =>
{
var logger = context.Services?.GetService<ILogger<Program>>();
if (context.JsonRpcMessage is JsonRpcRequest request)
{
logger?.LogInformation(
"Incoming MCP request {Method} with id {RequestId}",
request.Method,
request.Id);
}
await next(context, cancellationToken);
});
});
Use structured placeholders, not string interpolation, so your log backend can filter by Method and RequestId. Do not serialize request.Params by default: tool arguments often contain credentials, personal data, or large payloads.
Registering a reusable filter
For larger servers, put the filter in a class and inject a typed logger. Keep the filter fast, and always call next unless you intentionally reject a message.
public sealed class IncomingMcpLoggingFilter(ILogger<IncomingMcpLoggingFilter> logger)
{
public async ValueTask InvokeAsync(
MessageFilterContext context,
MessageFilterDelegate next,
CancellationToken cancellationToken)
{
if (context.JsonRpcMessage is JsonRpcRequest request)
{
logger.LogInformation("MCP method {Method} id {RequestId}",
request.Method, request.Id);
}
await next(context, cancellationToken);
}
}
Adapt the delegate and context types to the SDK version in your project; the inline registration above mirrors the current SDK documentation and is the least ambiguous starting point.
4. Run and verify the endpoint
With Streamable HTTP hosting, MapMcp() exposes the MCP endpoint configured by the SDK. Check your server’s route and protocol version before testing. A minimal JSON-RPC initialize request with cURL is:
curl -i http://localhost:3000/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
Look for one HTTP log entry and one structured MCP entry containing initialize. A notification may have no request id and may not be represented as a JsonRpcRequest; log the message type separately if you need notification accounting.
Python client probe
import json
import requests
payload = {
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2026-07-28", "capabilities": {},
"clientInfo": {"name": "python-probe", "version": "1.0"}}
}
r = requests.post(
"http://localhost:3000/mcp",
headers={"Accept": "application/json, text/event-stream"},
json=payload,
timeout=30,
)
r.raise_for_status()
print(r.status_code, r.text)
Node.js client probe
const payload = {
jsonrpc: '2.0', id: 1, method: 'initialize',
params: { protocolVersion: '2026-07-28', capabilities: {},
clientInfo: { name: 'node-probe', version: '1.0' } }
};
const res = await fetch('http://localhost:3000/mcp', {
method: 'POST',
headers: { 'content-type': 'application/json', 'accept': 'application/json, text/event-stream' },
body: JSON.stringify(payload)
});
console.log(res.status, await res.text());
5. Keep diagnostics separate from MCP client logging
The MCP Logging utility sends notifications to the connected client. It is a protocol feature, not your server’s operational sink, and the v2 SDK documentation marks it deprecated as of specification revision 2026-07-28. Use host-configured ILogger providers for incoming request diagnostics. If you deliberately need client-directed messages, follow the SDK version’s AsClientLoggerProvider() guidance and treat those messages as user-visible.
6. Privacy, performance, and reliability checklist
- Allowlist headers; exclude
Authorization,Cookie, and set-cookie values. - Keep request and response bodies off in normal operation.
- Redact query strings and tool arguments before exporting logs.
- Use asynchronous logging providers and bounded retention so logging cannot block request handling.
- Include a correlation id or JSON-RPC id, but do not assume ids are secrets or globally unique.
- Sample high-volume methods and retain full detail only during a time-boxed incident.
- Monitor log volume and sink failures; the MCP request should not fail because a logging backend is unavailable.
- Place HTTP logging before middleware you want covered. Requests handled earlier, such as static files, will not appear if logging is registered later.
7. Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| No HTTP entries | Middleware category filtered or middleware runs after the endpoint | Set category to Information; call UseHttpLogging early. |
| HTTP entry but no MCP method | Only transport logging is enabled, or JSON was invalid | Add the incoming filter; inspect status and parser errors. |
| Filter never runs | Filter registration is on a different service collection or SDK API differs | Chain WithMessageFilters on AddMcpServer() and verify package versions. |
| Secrets in logs | All headers, bodies, or parameters were enabled | Remove broad fields, allowlist headers, redact, rotate exposed credentials. |
| Duplicate lines | Both framework and provider add the same category | Check provider rules and disable one sink or category. |
| High latency or storage | Body capture and verbose filters on large messages | Disable bodies, cap size, sample, and keep filter work allocation-free. |
| Client disconnects produce errors | Cancellation propagated from transport | Honor the filter cancellation token and log disconnects at a low severity. |
8. Or skip the browser setup
If your MCP workflow needs screenshots of request traces or rendered documentation, ScreenshotNeo provides a single GET request and an MCP server. The DIY approach above remains useful for server logs; ScreenshotNeo removes browser setup when you need a clean image or PDF of a URL.
See the ScreenshotNeo API documentation. cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
9. FAQ
Should I log every JSON-RPC parameter?
No. Log method, id, outcome, and duration. Add narrowly redacted fields only for a diagnosed issue.
Can HTTP logging replace the SDK filter?
No. HTTP logging sees the envelope; the filter sees the parsed MCP method. They answer different questions.
Where should logs go in production?
Use the same structured ILogger providers as the rest of the ASP.NET Core service, with retention, access controls, and redaction managed by your logging platform.
Does Streamable HTTP change what gets logged?
It changes transport behavior, not the division of responsibility: middleware observes HTTP and the filter observes parsed MCP messages.


