[Feature] Add mcp_server to openbb_core api (#7094)

* [Feature] Add 'mcp_server' extention

* [Feature] Refactor of MCP server to FastMCP 2.0

- Also moves settings into the extention
- Refactors the management tool
- Add tool registry
- Use Argpars

* Enable all tools by default, fix linting and type issues

* Fix mcp CORS settings

* Fix linter errors

* Refactored load_settings_from_env + linting fix

* fix stdio transport, add lock file

---------

Co-authored-by: Danglewood <85772166+deeleeramone@users.noreply.github.com>
Co-authored-by: DidierRLopes <didierrlopes@gmail.com>
Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>
This commit is contained in:
Magnus
2025-07-03 09:49:59 +02:00
committed by GitHub
parent 6b0ae943d9
commit e00cf0f142
15 changed files with 3229 additions and 0 deletions

View File

@@ -13,6 +13,7 @@ from openbb_core.app.constants import (
)
from openbb_core.app.model.abstract.tagged import Tagged
from openbb_core.app.model.api_settings import APISettings
from openbb_core.app.model.python_settings import PythonSettings
from openbb_core.app.version import CORE_VERSION, VERSION
from pydantic import ConfigDict, Field, field_validator, model_validator

View File

@@ -50,6 +50,7 @@ openbb-fixedincome = { path = "./extensions/fixedincome", develop = true }
openbb-index = { path = "./extensions/index", develop = true }
openbb-news = { path = "./extensions/news", develop = true }
openbb-regulators = { path = "./extensions/regulators", develop = true }
openbb-mcp-server = { path = "./extensions/mcp_server", develop = true, markers = "python_version >= '3.10'" }
# Community dependencies
openbb-alpha-vantage = { path = "./providers/alpha_vantage", optional = true, develop = true }

View File

@@ -0,0 +1,122 @@
# OpenBB MCP Server
Model Context Protocol (MCP) server extension for OpenBB Platform. This extension enables LLM agents to interact with OpenBB Platform's REST API endpoints through the MCP protocol.
The server provides management tools that allow agents to explore different options and dynamically adjust their active toolset. This prevents agents from being overwhelmed with too many tools while allowing them to discover and activate only the tools they need for specific tasks.
Using these dynamic tool discovery, has one major drawback, it makes the server a single-user server. The tool updates are global, so if one user updates a tool, it will be updated for all users. If you plan to server multiple users, you should disable tool discovery, and instead use the `allowed_tool_categories` and `default_tool_categories` settings to control the tools that are available to the users.
## Installation
```bash
pip install openbb-mcp-server
```
## Usage
Start the OpenBB MCP server with default settings:
```bash
openbb-mcp
```
### Command Line Options
- `--host`: Host to bind to (default: 127.0.0.1)
- `--port`: Port to listen on (default: 8001)
- `--allowed-categories`: Comma-separated list of allowed tool categories
- `--default-categories`: Comma-separated list of categories enabled at startup (default: all)
- `--transport`: Transport protocol (default: streamable-http)
- `--no-tool-discovery`: Disable tool discovery for multi-client deployments
### Examples
```bash
# Start with default settings
openbb-mcp
# Start with specific categories and custom host/port
openbb-mcp --default-categories equity,news --host 0.0.0.0 --port 8080
# Start with allowed categories restriction
openbb-mcp --allowed-categories equity,crypto,news
# Disable tool discovery for multi-client usage
openbb-mcp --no-tool-discovery
```
## Configuration
The server can be configured through multiple methods:
### 1. Configuration File (Recommended)
The server automatically creates and uses `~/.openbb_platform/mcp_settings.json`:
```json
{
"name": "OpenBB MCP",
"description": "All OpenBB REST endpoints exposed as MCP tools...",
"default_tool_categories": ["all"],
"allowed_tool_categories": null,
"enable_tool_discovery": true,
"describe_responses": true
}
```
### 2. Environment Variables
Override settings using environment variables:
- `OPENBB_MCP_NAME`: Server name
- `OPENBB_MCP_DESCRIPTION`: Server description
- `OPENBB_MCP_DEFAULT_TOOL_CATEGORIES`: Comma-separated list of default categories
- `OPENBB_MCP_ALLOWED_TOOL_CATEGORIES`: Comma-separated list of allowed categories
- `OPENBB_MCP_ENABLE_TOOL_DISCOVERY`: true/false - Enable tool discovery features
- `OPENBB_MCP_DESCRIBE_ALL_RESPONSES`: true/false - Include response details in descriptions
- `OPENBB_MCP_DESCRIBE_FULL_RESPONSE_SCHEMA`: true/false - Include full response schemas
### 3. Command Line Arguments
Command line arguments override both configuration file and environment variables.
## Settings Reference
| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| name | string | "OpenBB MCP" | Server name displayed to MCP clients |
| description | string | "All OpenBB REST endpoints..." | Server description |
| default_tool_categories | list[string] | ["all"] | Categories enabled at startup. Use "all" to enable all categories, or specify individual categories |
| allowed_tool_categories | list[string] | null | If set, restricts available categories to this list |
| enable_tool_discovery | boolean | true | Enable discovery and management tools |
| describe_responses | boolean | true | Include response information in tool descriptions |
## Tool Categories
The server organizes OpenBB tools into categories based on the REST API structure:
- **`equity`** - Stock data, fundamentals, price history, estimates
- **`crypto`** - Cryptocurrency data and analysis
- **`economy`** - Economic indicators, GDP, employment data
- **`news`** - Financial news from various sources
- **`fixedincome`** - Bond data, rates, government securities
- **`derivatives`** - Options and futures data
- **`etf`** - ETF information and holdings
- **`currency`** - Foreign exchange data
- **`commodity`** - Commodity prices and data
- **`index`** - Market indices data
- **`regulators`** - SEC, CFTC regulatory data
Each category contains subcategories that group related functionality (e.g., `equity_price`, `equity_fundamental`, etc.).
## Tool Discovery
When `enable_tool_discovery` is enabled (default), the server provides management tools that allow agents to:
- Discover available tool categories and subcategories
- See tool counts and descriptions before activating
- Enable/disable specific tools dynamically during a session
- Start with minimal tools and progressively add more as needed
To take full advantage of minimal startup tools, you should set the `--default-categories` argument to `admin` this will enable only the discovery tools at startup.
For multi-client deployments or scenarios where you want a fixed toolset, disable tool discovery with `--no-tool-discovery`.

View File

@@ -0,0 +1 @@
"""OpenBB MCP Server package."""

View File

@@ -0,0 +1,264 @@
"""OpenBB MCP Server."""
import logging
import re
import sys
from typing import Annotated
import uvicorn
from fastapi import FastAPI
from fastmcp import FastMCP
from fastmcp.server.openapi import (
FastMCPOpenAPI,
OpenAPIResource,
OpenAPIResourceTemplate,
OpenAPITool,
)
from fastmcp.utilities.openapi import HTTPRoute
from openbb_core.api.rest_api import app
from openbb_core.app.service.system_service import SystemService
from pydantic import Field
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
from .registry import ToolRegistry
from .tool_models import CategoryInfo, SubcategoryInfo, ToolInfo
from .utils.config import load_mcp_settings_with_overrides, parse_args
from .utils.route_filtering import create_route_maps_from_settings
from .utils.settings import MCPSettings
logger = logging.getLogger("openbb_mcp_server")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setLevel(logging.INFO)
formatter = logging.Formatter("\n%(message)s\n")
handler.setFormatter(formatter)
logger.addHandler(handler)
PREFIX_SEGMENTS = ("api",)
VERSION_SEGMENT_RE = re.compile(r"v\d+\Z")
def _extract_brief_description(full_description: str) -> str:
"""Extract only the brief description before the detailed API documentation."""
if not full_description:
return "No description available"
brief, *_ = re.split(
r"\n{2,}\*\*(?:Query Parameters|Responses):", full_description, maxsplit=1
)
return brief.strip() or "No description available"
def create_mcp_server(settings: MCPSettings, fastapi_app: FastAPI) -> FastMCPOpenAPI:
"""Create and configure the MCP server."""
tool_registry = ToolRegistry()
def customize_components(
route: HTTPRoute,
component: OpenAPITool | OpenAPIResource | OpenAPIResourceTemplate,
) -> None:
"""Attach category/tool tags & disable tools that are not admin."""
if not isinstance(component, OpenAPITool):
return
segments = [seg for seg in route.path.lstrip("/").split("/") if "{" not in seg]
while segments and segments[0] in PREFIX_SEGMENTS:
segments.pop(0)
if segments and VERSION_SEGMENT_RE.match(segments[0]):
segments.pop(0)
if len(segments) < 2:
return
# Use hierarchical structure: category/subcategory/tool
category = segments[0]
if len(segments) == 2:
subcategory = "general"
tool = segments[1]
else:
subcategory = segments[1]
tool_parts = segments[2:]
tool = "_".join(tool_parts)
component.name = (
f"{category}_{subcategory}_{tool}"
if subcategory != "general"
else f"{category}_{tool}"
)
component.tags.add(category)
if not settings.describe_responses:
component.description = _extract_brief_description(
component.description or ""
)
if "all" in settings.default_tool_categories or any(
tag in settings.default_tool_categories for tag in component.tags
):
component.enable()
else:
component.disable()
tool_registry.register_tool(
category=category,
subcategory=subcategory,
tool_name=component.name,
tool=component,
)
return
mcp = FastMCP.from_fastapi(
app=fastapi_app,
mcp_component_fn=customize_components,
name=settings.name,
route_maps=create_route_maps_from_settings(settings),
)
# Add discovery tools if enabled
if settings.enable_tool_discovery:
@mcp.tool(tags={"admin"})
def available_categories() -> list[CategoryInfo]:
"""List all categories with their subcategories and tool counts.
This gives you a complete overview of what types of tasks you can solve.
"""
categories = tool_registry.get_categories()
return [
CategoryInfo(
name=category_name,
subcategories=[
SubcategoryInfo(name=subcat_name, tool_count=len(tools))
for subcat_name, tools in sorted(subcategories.items())
],
total_tools=sum(len(tools) for tools in subcategories.values()),
)
for category_name, subcategories in sorted(categories.items())
]
@mcp.tool(tags={"admin"})
def available_tools(
category: Annotated[
str, Field(description="The category of tools to list")
],
subcategory: Annotated[
str | None,
Field(
description="Optional subcategory to filter by. Use 'general' for tools directly under the category."
),
] = None,
) -> list[ToolInfo]:
"""For a category, list tools and whether they're currently enabled. Optionally filter by subcategory."""
category_data = tool_registry.get_category_subcategories(category)
if not category_data:
available_categories = list(tool_registry.get_categories().keys())
categories_str = ", ".join(sorted(available_categories))
raise ValueError(
f"Category '{category}' not found. Available categories: {categories_str}"
)
if subcategory:
# Single subcategory
tools_dict = tool_registry.get_category_tools(category, subcategory)
if not tools_dict:
available_subcategories = list(category_data.keys())
subcategories_str = ", ".join(sorted(available_subcategories))
raise ValueError(
f"Subcategory '{subcategory}' not found in category '{category}'. "
f"Available subcategories: {subcategories_str}"
)
return [
ToolInfo(
name=name,
active=tool.enabled,
description=_extract_brief_description(tool.description or ""),
)
for name, tool in sorted(tools_dict.items())
]
# All subcategories - flatten them
tools_dict = tool_registry.get_category_tools(category)
return [
ToolInfo(
name=name,
active=tool.enabled,
description=_extract_brief_description(tool.description or ""),
)
for name, tool in sorted(tools_dict.items())
]
@mcp.tool(tags={"admin"})
def activate_tools(
tool_names: Annotated[
list[str], Field(description="The name of the tool to activate")
],
) -> str:
"""Activate a tool or a list of tools."""
return tool_registry.toggle_tools(tool_names, enable=True).message
@mcp.tool(tags={"admin"})
def deactivate_tools(
tool_names: Annotated[
list[str], Field(description="The name of the tool to deactivate")
],
) -> str:
"""Deactivate a tool or a list of tools."""
return tool_registry.toggle_tools(tool_names, enable=False).message
return mcp
def main():
"""Start the OpenBB MCP server."""
args = parse_args()
try:
overrides = {}
if args.allowed_categories:
overrides["allowed_tool_categories"] = args.allowed_categories.split(",")
if args.default_categories:
overrides["default_tool_categories"] = args.default_categories.split(",")
if args.no_tool_discovery:
overrides["enable_tool_discovery"] = False
settings = load_mcp_settings_with_overrides(**overrides)
mcp = create_mcp_server(settings, app)
if args.transport == "stdio":
mcp.run(transport=args.transport)
else:
# Get CORS settings from system configuration
system_service = SystemService()
cors_settings = system_service.system_settings.api_settings.cors
cors_middleware = [
Middleware(
CORSMiddleware,
allow_origins=cors_settings.allow_origins,
allow_methods=cors_settings.allow_methods,
allow_headers=cors_settings.allow_headers,
),
]
# Create ASGI app with cors middleware
http_app = mcp.http_app(
middleware=cors_middleware,
transport=args.transport,
stateless_http=True,
)
uvicorn.run(http_app, host=args.host, port=args.port)
except Exception as e:
logger.error("Failed to start MCP server: %s", e)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,87 @@
"""Tool registry for managing MCP tools and tool discovery."""
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Mapping
from fastmcp.server.openapi import OpenAPITool
from .tool_models import ToggleResult
@dataclass
class ToolRegistry:
"""Keeps track of categories, subcategories and tool instances."""
_by_category: dict[str, dict[str, dict[str, OpenAPITool]]] = field(
default_factory=lambda: defaultdict(lambda: defaultdict(dict))
)
_by_name: dict[str, OpenAPITool] = field(default_factory=dict)
def register_tool(
self, *, category: str, subcategory: str, tool_name: str, tool: OpenAPITool
) -> None:
"""Register a tool in the registry."""
self._by_category[category][subcategory][tool_name] = tool
self._by_name[tool_name] = tool
def get_categories(self) -> Mapping[str, Mapping[str, Mapping[str, OpenAPITool]]]:
"""Get immutable view of all categories and their tools."""
return self._by_category
def get_category_tools(
self, category: str, subcategory: str | None = None
) -> dict[str, OpenAPITool]:
"""Get tools in a category, optionally filtered by subcategory."""
if subcategory is None:
# flatten all subcategories
return {
name: tool
for subcat_tools in self._by_category.get(category, {}).values()
for name, tool in subcat_tools.items()
}
return self._by_category.get(category, {}).get(subcategory, {})
def get_tool(self, tool_name: str) -> OpenAPITool | None:
"""Get a tool by name."""
return self._by_name.get(tool_name)
def get_category_subcategories(
self, category: str
) -> dict[str, dict[str, OpenAPITool]] | None:
"""Get all subcategories for a specific category."""
return self._by_category.get(category)
def toggle_tools(self, tool_names: list[str], enable: bool) -> ToggleResult:
"""Enable or disable a list of tools, returning a status message."""
successful, failed = [], []
for name in tool_names:
tool = self._by_name.get(name)
if tool:
(tool.enable if enable else tool.disable)()
successful.append(name)
else:
failed.append(name)
action = "activated" if enable else "deactivated"
parts: list[str] = []
if successful:
parts.append(f"{action.capitalize()}: {', '.join(successful)}")
if failed:
parts.append(f"Not found: {', '.join(failed)}")
message = " ".join(parts) if parts else "No tools processed."
return ToggleResult(
action=action,
successful=successful,
failed=failed,
message=message,
)
def clear(self) -> None:
"""Clear the registry."""
self._by_category.clear()
self._by_name.clear()

View File

@@ -0,0 +1,37 @@
"""Tool models for MCP server."""
from typing import List, Literal
from pydantic import BaseModel
class ToolInfo(BaseModel):
"""Information about a single tool."""
name: str
active: bool
description: str
class SubcategoryInfo(BaseModel):
"""Metadata for a tool subcategory."""
name: str
tool_count: int
class CategoryInfo(BaseModel):
"""Metadata for a category of tools."""
name: str
subcategories: List[SubcategoryInfo]
total_tools: int
class ToggleResult(BaseModel):
"""Result of a request to activate or deactivate one or more tools."""
action: Literal["activated", "deactivated"]
successful: List[str]
failed: List[str]
message: str

View File

@@ -0,0 +1 @@
"""Utility functions for MCP server."""

View File

@@ -0,0 +1,147 @@
"""Configuration loader for MCP Server."""
import argparse
import json
import logging
import os
from pathlib import Path
from typing import Dict, List, Union
from openbb_core.app.constants import OPENBB_DIRECTORY
from .settings import MCPSettings
logger = logging.getLogger(__name__)
def get_mcp_config_path() -> Path:
"""Get the path to the MCP configuration file."""
return OPENBB_DIRECTORY / "mcp_settings.json"
def load_mcp_settings() -> MCPSettings:
"""Load MCP settings from configuration file or create defaults."""
config_path = get_mcp_config_path()
if config_path.exists():
try:
with open(config_path, encoding="utf-8") as f:
config_data = json.load(f)
return MCPSettings(**config_data)
except (json.JSONDecodeError, TypeError, ValueError) as e:
logger.warning(
"Error loading MCP configuration from %s: %s. Using default settings.",
config_path,
e,
)
# Create default settings
settings = MCPSettings()
# Create the directory if it doesn't exist
config_path.parent.mkdir(parents=True, exist_ok=True)
# Save default settings to file
save_mcp_settings(settings)
return settings
def save_mcp_settings(settings: MCPSettings) -> None:
"""Save MCP settings to configuration file."""
config_path = get_mcp_config_path()
config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, "w", encoding="utf-8") as f:
json.dump(settings.model_dump(), f, indent=2)
def load_settings_from_env() -> Dict[str, Union[str, List[str], bool]]:
"""Load MCP settings from environment variables."""
env_vars = {
f.alias: os.environ[f.alias]
for f in MCPSettings.model_fields.values()
if f.alias and f.alias in os.environ
}
if not env_vars:
return {}
env_settings = MCPSettings.model_validate(env_vars, from_attributes=False)
return env_settings.model_dump(exclude_unset=True)
def load_mcp_settings_with_overrides(**overrides) -> MCPSettings:
"""Load MCP settings with optional overrides."""
settings = load_mcp_settings()
# Apply environment variable overrides
env_settings = load_settings_from_env()
if env_settings:
settings_dict = settings.model_dump()
settings_dict.update(env_settings)
settings = MCPSettings(**settings_dict)
# Apply function argument overrides
if overrides:
settings_dict = settings.model_dump()
settings_dict.update(overrides)
settings = MCPSettings(**settings_dict)
return settings
def parse_args():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
description="Start the OpenBB MCP server",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
openbb-mcp # Start with default settings
openbb-mcp --host 0.0.0.0 --port 8001 # Custom host and port
openbb-mcp --allowed-categories equity,crypto # Allowed categories
openbb-mcp --default-categories equity,crypto # Override default (all categories enabled by default)
openbb-mcp --transport stdio # Use stdio transport
openbb-mcp --no-tool-discovery # Disable tool discovery (multi-client safe)
The server can also be configured via:
- Configuration file: ~/.openbb_platform/mcp_settings.json
- Environment variables: OPENBB_MCP_HOST, OPENBB_MCP_PORT, etc.
Command line arguments override configuration file and environment variables.
""",
)
parser.add_argument(
"--host",
type=str,
default="127.0.0.1",
help="Host IP address to bind the server to",
)
parser.add_argument(
"--port", type=int, default=8001, help="Port number to bind the server to"
)
parser.add_argument(
"--allowed-categories",
type=str,
default=None,
help="Comma-separated list of tool categories allowed to be used",
)
parser.add_argument(
"--default-categories",
type=str,
default="all",
help="Comma-separated list of tool categories enabled at startup",
)
parser.add_argument(
"--transport",
type=str,
default="streamable-http",
help="MCP transport protocol to use",
choices=["streamable-http", "stdio", "sse"],
)
parser.add_argument(
"--no-tool-discovery",
action="store_true",
help="Disable tool discovery (multi-client safe)",
)
return parser.parse_args()

View File

@@ -0,0 +1,49 @@
"""Route filtering utilities for MCP server using FastMCP RouteMap."""
import re
from typing import List
from fastmcp.server.openapi import MCPType, RouteMap
from .settings import MCPSettings
def create_route_maps_from_settings(settings: MCPSettings) -> List[RouteMap]:
"""
Create RouteMap objects based on MCPSettings for filtering routes.
Parameters
----------
settings : MCPSettings
MCPSettings instance with filtering configuration
Returns
-------
List[RouteMap]
List of RouteMap objects that FastMCP will use for filtering
"""
route_maps = []
if settings.allowed_tool_categories:
# Create patterns for allowed categories
allowed_pattern = "|".join(
re.escape(cat) for cat in settings.allowed_tool_categories
)
# Include only allowed categories
route_maps.append(
RouteMap(
pattern=rf"^/api/v\d+/({allowed_pattern})/.*",
mcp_type=MCPType.TOOL,
)
)
# Exclude everything else
route_maps.append(
RouteMap(
pattern=rf"^/api/v\d+/(?!({allowed_pattern})/).*",
mcp_type=MCPType.EXCLUDE,
)
)
return route_maps

View File

@@ -0,0 +1,70 @@
"""MCP Server Settings model."""
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field, field_validator
class MCPSettings(BaseModel):
"""MCP Server settings model."""
model_config = ConfigDict(
populate_by_name=True,
extra="ignore",
)
# Basic server configuration
name: str = Field(
default="OpenBB MCP",
alias="OPENBB_MCP_NAME",
)
description: str = Field(
default="""All OpenBB REST endpoints exposed as MCP tools. Enables LLM agents
to query financial data, run screeners, and build workflows using
the exact same operations available to REST clients.""",
alias="OPENBB_MCP_DESCRIPTION",
)
# Tool category filtering
default_tool_categories: List[str] = Field(
default_factory=lambda: ["all"],
description="Default active tool categories on startup",
alias="OPENBB_MCP_DEFAULT_TOOL_CATEGORIES",
)
allowed_tool_categories: Optional[List[str]] = Field(
default=None,
description="If set, restricts available tool categories to this list",
alias="OPENBB_MCP_ALLOWED_TOOL_CATEGORIES",
)
# Tool discovery configuration
enable_tool_discovery: bool = Field(
default=True,
description="""
Enable tool discovery, allowing the agent to hot-swap tools at runtime.
Disable for multi-client or fixed toolset deployments.
""",
alias="OPENBB_MCP_ENABLE_TOOL_DISCOVERY",
)
# Response configuration
describe_responses: bool = Field(
default=True,
description="Include response types in tool descriptions",
alias="OPENBB_MCP_DESCRIBE_RESPONSES",
)
@field_validator(
"default_tool_categories", "allowed_tool_categories", mode="before"
)
@classmethod
def _split_list(cls, v):
if isinstance(v, str):
return [part.strip() for part in v.split(",") if part.strip()]
return v
def __repr__(self) -> str:
"""Return string representation."""
return f"{self.__class__.__name__}\n\n" + "\n".join(
f"{k}: {v}" for k, v in self.model_dump().items()
)

2423
openbb_platform/extensions/mcp_server/poetry.lock generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
[tool.poetry]
name = "openbb-mcp-server"
version = "0.0.1"
description = "OpenBB Platform MCP Server"
authors = ["OpenBB <hello@openbb.co>"]
license = "AGPL-3.0-only"
readme = "README.md"
homepage = "https://openbb.co"
repository = "https://github.com/openbb-finance/openbb"
documentation = "https://docs.openbb.co"
packages = [{ include = "openbb_mcp_server" }]
[tool.poetry.scripts]
openbb-mcp = "openbb_mcp_server.main:main"
[tool.poetry.dependencies]
python = ">=3.10,<3.13"
openbb-core = "^1.4.3"
fastmcp = "^2.8.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

View File

@@ -62,6 +62,7 @@ openbb-charting = { version = "^2.3.4", optional = true }
openbb-econometrics = { version = "^1.5.3", optional = true }
openbb-quantitative = { version = "^1.4.3", optional = true }
openbb-technical = { version = "^1.4.3", optional = true }
openbb-mcp-server = { version = "^0.0.1", optional = true, markers = "python_version >= '3.10'" }
[tool.poetry.extras]
alpha_vantage = ["openbb-alpha-vantage"]
@@ -73,6 +74,7 @@ econometrics = ["openbb-econometrics"]
finra = ["openbb-finra"]
finviz = ["openbb-finviz"]
government_us = ["openbb-government-us"]
mcp_server = ["openbb-mcp-server"]
nasdaq = ["openbb-nasdaq"]
multpl = ["openbb-multpl"]
quantitative = ["openbb-quantitative"]
@@ -95,6 +97,7 @@ all = [
"openbb-finra",
"openbb-finviz",
"openbb-government-us",
"openbb-mcp-server",
"openbb-multpl",
"openbb-nasdaq",
"openbb-quantitative",