feat: Add structured audit logging for MCP feature server#6456
feat: Add structured audit logging for MCP feature server#6456SIDDHESH1564 wants to merge 7 commits into
Conversation
Signed-off-by: Siddhesh Khairnar <khairnarsiddhesh4057@gmail.com>
OTel is a follow-up is fine, but the core schema and sink abstraction feel like reinventing what opentelemetry-sdk already provides. Also, AuditEvent should carry a trace_id/span_id if OpenTelemetry is active. Also, the fundamental problem with this is that it intercepts at the HTTP transport layer rather than at the MCP protocol layer. It should implement bidirectional correlation - linking a request to its response. |
…orrelation Signed-off-by: Siddhesh Khairnar <khairnarsiddhesh4057@gmail.com>
|
Thanks for the review, @ntkathole. Both points have been addressed.
|
| jsonrpc_id: Optional[str] = None | ||
| if request.method == "POST": | ||
| try: | ||
| body = await request.body() |
There was a problem hiding this comment.
This audit hook is at the wrong layer. Instead of parsing raw HTTP bodies, wrap mcp.server.request_handlers[CallToolRequest] inside add_mcp_support_to_app() after FastApiMCP.setup_server(). That gives you typed tool_name, arguments, CallToolResult.isError
There was a problem hiding this comment.
Thanks for the detailed review @ntkathole. I’ll study the suggested MCP protocol-layer approach and make the required changes. Will also address the thread-safety comment for the logger. Thanks!
There was a problem hiding this comment.
@ntkathole, I’ll remove the raw HTTP body parsing from McpAuditMiddleware and wrap the MCP server’s CallToolRequest handler after FastApiMCP.setup_server(). This will provide the typed tool name, arguments, and CallToolResult.isError directly, without json.loads() or response-body replay.
| try: | ||
| from feast.permissions.security_manager import get_security_manager | ||
|
|
||
| sm = get_security_manager() |
There was a problem hiding this comment.
MPC server don't use get_security_manager - again this is HTTP layer, will always return an empty principal for MCP
There was a problem hiding this comment.
@ntkathole, Since get_security_manager() does not apply to MCP requests, I’ll derive the authentication metadata from mcp.server.request_context.request.headers.
| class LoggerAuditSink(AuditSink): | ||
| """Emit audit events through Python's ``logging`` module at INFO level.""" | ||
|
|
||
| def __init__(self, logger_name: str = "feast.audit") -> None: | ||
| self._logger = logging.getLogger(logger_name) | ||
|
|
||
| def emit(self, event: AuditEvent) -> None: | ||
| self._logger.info(event.to_jsonl()) |
There was a problem hiding this comment.
Add a threading.Lock around emit(), when multiple tasks or workers emit the logs, it should handle
There was a problem hiding this comment.
@ntkathole, I’ll add a threading.Lock around sink.emit() in the audit logging path.
|
Will update the tests accordingly and push the revised commit once the changes are complete. Thanks! |
…lock Signed-off-by: Siddhesh Khairnar <khairnarsiddhesh4057@gmail.com>
|
Suggestion: It might help to generate request_id once in the tools/call wrapper and propagate the same value to the internal REST call (e.g. via X-Request-Id and FastApiMCP(headers=[..., "x-request-id"])). Right now the MCP path always mints a new ID while REST reads the header, and most MCP clients don’t send X-Request-Id, so mcp.tools.call and http.request may not correlate in SIEM. Worth keeping jsonrpc_id separate from audit request_id. A test that both events share one request_id per tool call would be great. |
|
@patelchaitany The mcp.tools.call and http.request events currently get independent request_id values, so they won't correlate in SIEM. Will add "x-request-id" to FastApiMCP's forwarded headers and propagate the audit request_id from the wrapper into the internal REST call. jsonrpc_id is already kept separate from request_id. |
|
Now the flow is:
|
Signed-off-by: Siddhesh Khairnar <khairnarsiddhesh4057@gmail.com>
|
@SIDDHESH1564 - have you tested this locally by running the Feast feature server and checking audit logs end-to-end? My setup: fastapi-mcp==0.4.0, mcp==1.27.0, MCP enabled with mcp_transport: http and audit_logging enabled (file sink). What I see: REST audit (http.request) works, but MCP tool-call audit does not. On startup I get: Cannot wrap MCP call_tool handler: _request_handlers not found The wrapper never attaches — the handler stays handler, not audited_call_tool — so no mcp.tools.call events are emitted. Unit tests pass because they mock _request_handlers["tools/call"], which doesn’t exist in 0.4.0. Root cause: The PR wraps mcp.server._request_handlers["tools/call"], but in fastapi-mcp 0.4.0 + mcp 1.x the hook is mcp.server.request_handlers[CallToolRequest] with signature async def handler(req: CallToolRequest). Suggested fix: Update _wrap_call_tool_handler() in mcp_server.py to wrap request_handlers[CallToolRequest], read principal from server.request_context, and forward x-request-id / x-feast-auth-type in FastApiMCP(headers=...). Add a unit test using a real FastApiMCP instance (not a mocked _request_handlers). |
Signed-off-by: Siddhesh Khairnar <khairnarsiddhesh4057@gmail.com>
|
Thanks for the detailed report, @patelchaitany. you're right about the root cause. I've confirmed and fixed the issue. The wrapper was targeting mcp.server._request_handlers["tools/call"], which was the internal API of an older mcp SDK. |
What this PR does / why we need it:
This PR adds structured audit logging in JSONL format for the Python MCP feature server.
Previously, the MCP integration only logged startup events and errors. Operators did not have a unified audit trail for MCP tool calls, internal REST requests, authentication results, authorization decisions, or request correlation.
This change introduces an
audit_loggingconfiguration section underfeature_serverand adds structured audit events for:X-Request-IdEach audit event follows a stable schema and includes:
event_typetimestamprequest_idprincipal(username,roles, andauth_type)source(ipandtransport)action(MCP tool name and/or HTTP path)resource(type,name, and permitted actions)outcomeduration_msSensitive data is intentionally excluded from audit logs. Tokens, entity rows, feature values, and request payloads are not logged.
Audit logging layers
McpAuditMiddleware/mcpJSON-RPC requestsmcp.tools.call,mcp.requestAuditLoggingMiddleware/get-online-featuresand/pushhttp.requestAuditLoggerhelpersauthn.success,authn.failure,authz.decisionNew files
sdk/python/feast/audit/audit_logger.pyAuditEventmodel.StdoutAuditSink,FileAuditSink, andLoggerAuditSink.AuditLoggerhelper methods for MCP, HTTP, authentication, and authorization events.sdk/python/feast/audit/audit_middleware.pyAuditLoggingMiddlewarefor REST endpoint auditing.McpAuditMiddlewarefor MCP JSON-RPC request auditing.Modified files
sdk/python/feast/infra/feature_servers/base_config.pyAuditLoggingConfigtoBaseFeatureServerConfig.sdk/python/feast/feature_server.pyget_app().Example configuration
Example JSONL event
{ "event_type": "mcp.tools.call", "timestamp": "2026-05-28T12:00:00.000Z", "request_id": "...", "principal": { "username": "jane@co.com", "roles": ["reader"], "auth_type": "oidc" }, "source": { "ip": "10.0.0.1", "transport": "mcp-http" }, "action": { "mcp_tool": "get_online_features", "path": "/mcp" }, "outcome": "success", "duration_ms": 42.0 }Which issue(s) this PR fixes:
Fixes #6452
Checks
git commit -s)Testing Strategy
Added 37 unit tests covering:
AuditEventserialization in JSONL format, including exclusion ofNonevaluesAuditLoggerhelpers:log_mcp_calllog_http_requestlog_authnlog_authzSuppression of successful read events when
log_successful_reads: falseAll supported sinks:
StdoutAuditSinkFileAuditSinkLoggerAuditSinkcreate_audit_logger_from_configfactory behavior and fallback handlingAuditLoggingConfigvalidation and inheritance throughMcpFeatureServerConfigAuditLoggingMiddlewarebehavior:X-Request-IdpropagationMcpAuditMiddlewarebehavior:Misc
audit_loggingis defined onBaseFeatureServerConfig, making it available to both local and MCP feature server types.SecurityManagercontext and supportsno_auth,oidc, andkubernetesauthentication modes.