Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: Support HTTP in MCP
Signed-off-by: aaronzuo <anarionzuo@outlook.com>
  • Loading branch information
Anarion-zuo authored and ntkathole committed Apr 5, 2026
commit fc7974c6483f7aaf6de7831574063f4f74c71cf4
3 changes: 3 additions & 0 deletions docs/getting-started/genai.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,13 @@ Feast supports the Model Context Protocol (MCP), which enables AI agents and app
type: mcp
enabled: true
mcp_enabled: true
mcp_transport: http
mcp_server_name: "feast-feature-store"
mcp_server_version: "1.0.0"
```

By default, Feast uses the SSE-based MCP transport (`mcp_transport: sse`). Streamable HTTP (`mcp_transport: http`) is recommended for improved compatibility with some MCP clients.

### How It Works

The MCP integration uses the `fastapi_mcp` library to automatically transform your Feast feature server's FastAPI endpoints into MCP-compatible tools. When you enable MCP support:
Expand Down
51 changes: 51 additions & 0 deletions docs/reference/feature-servers/mcp-feature-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# MCP Feature Server

## Overview

Feast can expose the Python Feature Server as an MCP (Model Context Protocol) server using `fastapi_mcp`. When enabled, MCP clients can discover and call Feast tools such as online feature retrieval.

## Installation

```bash
pip install feast[mcp]
```

## Configuration

Add an MCP `feature_server` block to your `feature_store.yaml`:

```yaml
feature_server:
type: mcp
enabled: true
mcp_enabled: true
mcp_transport: http
mcp_server_name: "feast-feature-store"
mcp_server_version: "1.0.0"
```

### mcp_transport

`mcp_transport` controls how MCP is mounted into the Feature Server:

- `sse`: SSE-based transport. This is the default for backward compatibility.
- `http`: Streamable HTTP transport. This is recommended for improved compatibility with some MCP clients.

If `mcp_transport: http` is configured but your installed `fastapi_mcp` version does not support Streamable HTTP mounting, Feast will fail fast with an error asking you to upgrade `fastapi_mcp` (or reinstall `feast[mcp]`).

## Endpoints

MCP is mounted at:

- `/mcp`

## Connecting an MCP client

Use your MCP client’s “HTTP” configuration and point it to the Feature Server base URL. For example, if your Feature Server runs at `http://localhost:6566`, use:

- `http://localhost:6566/mcp`

## Troubleshooting

- If you see a deprecation warning about `mount()` at runtime, upgrade `fastapi_mcp` and use `mcp_transport: http` or `mcp_transport: sse`.
- If your MCP client has intermittent connectivity issues with `mcp_transport: sse`, switch to `mcp_transport: http`.
3 changes: 2 additions & 1 deletion examples/mcp_feature_store/feature_store.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ feature_server:
type: mcp
enabled: true
mcp_enabled: true # Enable MCP support - defaults to false
mcp_transport: http
mcp_server_name: "feast-feature-store"
mcp_server_version: "1.0.0"
feature_logging:
enabled: false

entity_key_serialization_version: 3
entity_key_serialization_version: 3
4 changes: 4 additions & 0 deletions sdk/python/feast/feature_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,10 @@ def _add_mcp_support_if_enabled(app, store: "feast.FeatureStore"):
else:
logger.debug("MCP support is not enabled in feature server configuration")
except Exception as e:
from feast.infra.mcp_servers.mcp_server import McpTransportNotSupportedError

if isinstance(e, McpTransportNotSupportedError):
raise
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
logger.error(f"Error checking/adding MCP support: {e}")
# Don't fail the entire server if MCP fails to initialize

Expand Down
5 changes: 2 additions & 3 deletions sdk/python/feast/infra/mcp_servers/mcp_config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Literal, Optional
from typing import Literal

from pydantic import StrictBool, StrictStr

Expand All @@ -20,8 +20,7 @@ class McpFeatureServerConfig(BaseFeatureServerConfig):
# MCP server version
mcp_server_version: StrictStr = "1.0.0"

# Optional MCP transport configuration
mcp_transport: Optional[StrictStr] = None
mcp_transport: Literal["sse", "http"] = "sse"

# The endpoint definition for transformation_service (inherited from base)
transformation_service_endpoint: StrictStr = "localhost:6566"
29 changes: 26 additions & 3 deletions sdk/python/feast/infra/mcp_servers/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"""

import logging
from typing import Optional
from typing import Literal, Optional

from feast.feature_store import FeatureStore

Expand All @@ -26,6 +26,10 @@
FastApiMCP = None


class McpTransportNotSupportedError(RuntimeError):
pass


def add_mcp_support_to_app(app, store: FeatureStore, config) -> Optional["FastApiMCP"]:
"""Add MCP support to the FastAPI app if enabled in configuration."""
if not MCP_AVAILABLE:
Expand All @@ -40,8 +44,25 @@ def add_mcp_support_to_app(app, store: FeatureStore, config) -> Optional["FastAp
description="Feast Feature Store MCP Server - Access feature store data and operations through MCP",
)

# Mount the MCP server to the FastAPI app
mcp.mount()
transport: Literal["sse", "http"] = getattr(config, "mcp_transport", "sse") or "sse"
if transport == "http":
mount_http = getattr(mcp, "mount_http", None)
if mount_http is None:
raise McpTransportNotSupportedError(
"mcp_transport=http requires fastapi_mcp with FastApiMCP.mount_http(). "
"Upgrade fastapi_mcp (or install feast[mcp]) to a newer version."
)
mount_http()
elif transport == "sse":
mount_sse = getattr(mcp, "mount_sse", None)
if mount_sse is not None:
mount_sse()
else:
mcp.mount()
Comment thread
franciscojavierarceo marked this conversation as resolved.
else:
raise McpTransportNotSupportedError(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch is unreachable — Literal["sse", "http"] in the config rejects anything else at parse time. Either remove it or add a comment that it's a defensive guard for programmatic callers.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Anarion-zuo u didn't solve it btw :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Anarion-zuo any update on this one?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry. Missed this one. Will look at it ASAP.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@YassinNouh21 Made some changes. Is this aligned with what you intended?

f"Unsupported mcp_transport={transport!r}. Expected 'sse' or 'http'."
)

logger.info(
"MCP support has been enabled for the Feast feature server at /mcp endpoint"
Expand All @@ -53,6 +74,8 @@ def add_mcp_support_to_app(app, store: FeatureStore, config) -> Optional["FastAp

return mcp

except McpTransportNotSupportedError:
raise
except Exception as e:
logger.error(f"Failed to initialize MCP integration: {e}")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing exc_info=True — loses the traceback. Without it, debugging a broken init means guessing from just the exception message string.

return None
88 changes: 68 additions & 20 deletions sdk/python/tests/unit/infra/feature_servers/test_mcp_server.py
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import unittest
from types import SimpleNamespace
from unittest.mock import Mock, patch

from pydantic import ValidationError

from feast.feature_store import FeatureStore
from feast.infra.mcp_servers.mcp_config import McpFeatureServerConfig

Expand All @@ -17,7 +20,7 @@ def test_default_config(self):
self.assertFalse(config.mcp_enabled)
self.assertEqual(config.mcp_server_name, "feast-mcp-server")
self.assertEqual(config.mcp_server_version, "1.0.0")
self.assertIsNone(config.mcp_transport)
self.assertEqual(config.mcp_transport, "sse")
self.assertEqual(config.transformation_service_endpoint, "localhost:6566")

def test_custom_config(self):
Expand All @@ -41,11 +44,11 @@ def test_custom_config(self):

def test_config_validation(self):
"""Test configuration validation."""
# Test valid transport options
valid_transports = ["sse", "websocket", None]
for transport in valid_transports:
for transport in ["sse", "http"]:
config = McpFeatureServerConfig(mcp_transport=transport)
self.assertEqual(config.mcp_transport, transport)
with self.assertRaises(ValidationError):
McpFeatureServerConfig(mcp_transport="websocket")

def test_config_inheritance(self):
"""Test that McpFeatureServerConfig properly inherits from BaseFeatureServerConfig."""
Expand All @@ -66,12 +69,13 @@ def test_add_mcp_support_success(self, mock_fast_api_mcp):

mock_app = Mock()
mock_store = Mock(spec=FeatureStore)
mock_config = Mock()
mock_config.mcp_server_name = "test-server"
mock_config.mcp_server_version = "1.0.0"
mock_config = SimpleNamespace(
mcp_server_name="test-server",
mcp_server_version="1.0.0",
mcp_transport="sse",
)

# Mock the FastApiMCP instance
mock_mcp_instance = Mock()
mock_mcp_instance = Mock(spec_set=["mount_sse", "mount", "mount_http"])
mock_fast_api_mcp.return_value = mock_mcp_instance

result = add_mcp_support_to_app(mock_app, mock_store, mock_config)
Expand All @@ -83,8 +87,7 @@ def test_add_mcp_support_success(self, mock_fast_api_mcp):
description="Feast Feature Store MCP Server - Access feature store data and operations through MCP",
)

# Verify mount was called
mock_mcp_instance.mount.assert_called_once()
mock_mcp_instance.mount_sse.assert_called_once()

# Verify the result
self.assertEqual(result, mock_mcp_instance)
Expand All @@ -96,11 +99,9 @@ def test_add_mcp_support_with_defaults(self, mock_fast_api_mcp):

mock_app = Mock()
mock_store = Mock(spec=FeatureStore)
mock_config = Mock()
# Don't set mcp_server_name to test default
del mock_config.mcp_server_name
mock_config = SimpleNamespace(mcp_transport="sse")

mock_mcp_instance = Mock()
mock_mcp_instance = Mock(spec_set=["mount_sse", "mount", "mount_http"])
mock_fast_api_mcp.return_value = mock_mcp_instance

result = add_mcp_support_to_app(mock_app, mock_store, mock_config)
Expand All @@ -114,6 +115,38 @@ def test_add_mcp_support_with_defaults(self, mock_fast_api_mcp):

self.assertEqual(result, mock_mcp_instance)

@patch("feast.infra.mcp_servers.mcp_server.FastApiMCP")
def test_add_mcp_support_http_transport(self, mock_fast_api_mcp):
from feast.infra.mcp_servers.mcp_server import add_mcp_support_to_app

mock_app = Mock()
mock_store = Mock(spec=FeatureStore)
mock_config = SimpleNamespace(mcp_server_name="test-server", mcp_transport="http")

mock_mcp_instance = Mock(spec_set=["mount_http"])
mock_fast_api_mcp.return_value = mock_mcp_instance

result = add_mcp_support_to_app(mock_app, mock_store, mock_config)
mock_mcp_instance.mount_http.assert_called_once()
self.assertEqual(result, mock_mcp_instance)

@patch("feast.infra.mcp_servers.mcp_server.FastApiMCP")
def test_add_mcp_support_http_missing_mount_http_fails(self, mock_fast_api_mcp):
from feast.infra.mcp_servers.mcp_server import (
McpTransportNotSupportedError,
add_mcp_support_to_app,
)

mock_app = Mock()
mock_store = Mock(spec=FeatureStore)
mock_config = SimpleNamespace(mcp_transport="http")

mock_mcp_instance = Mock(spec_set=["mount"])
mock_fast_api_mcp.return_value = mock_mcp_instance

with self.assertRaises(McpTransportNotSupportedError):
add_mcp_support_to_app(mock_app, mock_store, mock_config)

@patch("feast.infra.mcp_servers.mcp_server.FastApiMCP")
@patch("feast.infra.mcp_servers.mcp_server.logger")
def test_add_mcp_support_with_exception(self, mock_logger, mock_fast_api_mcp):
Expand All @@ -122,8 +155,7 @@ def test_add_mcp_support_with_exception(self, mock_logger, mock_fast_api_mcp):

mock_app = Mock()
mock_store = Mock(spec=FeatureStore)
mock_config = Mock()
mock_config.mcp_server_name = "test-server"
mock_config = SimpleNamespace(mcp_server_name="test-server", mcp_transport="sse")

# Mock FastApiMCP to raise an exception
mock_fast_api_mcp.side_effect = Exception("MCP initialization failed")
Expand All @@ -145,10 +177,9 @@ def test_add_mcp_support_mount_exception(self, mock_fast_api_mcp):

mock_app = Mock()
mock_store = Mock(spec=FeatureStore)
mock_config = Mock()
mock_config.mcp_server_name = "test-server"
mock_config = SimpleNamespace(mcp_server_name="test-server", mcp_transport="sse")

mock_mcp_instance = Mock()
mock_mcp_instance = Mock(spec_set=["mount"])
mock_mcp_instance.mount.side_effect = Exception("Mount failed")
mock_fast_api_mcp.return_value = mock_mcp_instance

Expand Down Expand Up @@ -203,3 +234,20 @@ def test_add_mcp_support_if_enabled_exception(self, mock_logger):
mock_logger.error.assert_called_with(
"Error checking/adding MCP support: Test error"
)

@patch("feast.infra.mcp_servers.mcp_server.add_mcp_support_to_app")
def test_add_mcp_support_if_enabled_transport_not_supported_fails(self, mock_add):
from feast.feature_server import _add_mcp_support_if_enabled
from feast.infra.mcp_servers.mcp_server import McpTransportNotSupportedError

mock_app = Mock()
mock_store = Mock()
mock_store.config.feature_server = Mock()
mock_store.config.feature_server.type = "mcp"
mock_store.config.feature_server.mcp_enabled = True
mock_store.config.feature_server.mcp_transport = "http"

mock_add.side_effect = McpTransportNotSupportedError("bad")

with self.assertRaises(McpTransportNotSupportedError):
_add_mcp_support_if_enabled(mock_app, mock_store)