feat: Add structured audit logging for MCP feature server#6456
feat: Add structured audit logging for MCP feature server#6456SIDDHESH1564 wants to merge 4 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>
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.