xarical commited on
Commit
0d1138e
·
1 Parent(s): e97fc6c

Add app.py, utils.py, and config.py

Browse files
Files changed (4) hide show
  1. app.py +137 -0
  2. config.py +11 -0
  3. requirements.txt +2 -0
  4. utils.py +124 -0
app.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+ import utils
4
+
5
+
6
+ # Import obb (don't reload it)
7
+ if gr.NO_RELOAD:
8
+ from openbb import obb
9
+
10
+ # Get tool names and guides
11
+ ARGS = "obb", obb # type: ignore
12
+ tool_names = utils.get_callable_names(
13
+ *ARGS,
14
+ include={"equity"}
15
+ )
16
+ tool_guides = utils.generate_callable_guides(*ARGS, tool_names)
17
+
18
+
19
+ with gr.Blocks() as demo:
20
+ gr.Markdown("# OpenBB MCP")
21
+
22
+ # Reference guide
23
+ with gr.Accordion("Tool Reference Guide 📃", open=False):
24
+ for tool_name, tool_guide in zip(tool_names, tool_guides):
25
+
26
+ # Create a collapsible for each tool
27
+ with gr.Accordion(f"{tool_name}", open=False):
28
+ gr.Markdown(f"```\n{tool_guide}```")
29
+
30
+ # Dynamically generate tool test UI
31
+ tool_params = utils.get_callable_params(*ARGS, tool_name)
32
+ tool_param_inputs = []
33
+ tool_param_kinds = []
34
+ with gr.Row():
35
+ for param in tool_params:
36
+ tool_param_inputs.append(
37
+ gr.Textbox(
38
+ label=param["name"],
39
+ value=str(param["default"]) if param["default"] else ""
40
+ )
41
+ )
42
+ tool_param_kinds.append(param["kind"])
43
+ output = gr.Textbox(label="Output", lines=10)
44
+ test_btn = gr.Button("Run")
45
+
46
+ # Format vars
47
+ t = tool_name
48
+ tool_name = tool_name.replace(".", "_")
49
+ tool_guide = tool_guide.replace(t, tool_name)
50
+ tool_param_names = [
51
+ param["name"] for param in tool_params
52
+ ]
53
+ csv_tool_names = ", ".join(tool_param_names)
54
+
55
+ # Dynamically generate function
56
+ # TODO: Fragile. Refactor if possible
57
+ namespace = { # Create a local namespace
58
+ "utils": utils,
59
+ "ARGS": ARGS,
60
+ "tool_param_kinds": tool_param_kinds
61
+ }
62
+ exec(f"""\
63
+ import inspect
64
+ import json
65
+
66
+ def {tool_name}({csv_tool_names}) -> str:
67
+ \"\"\"
68
+ {tool_guide}
69
+ \"\"\"
70
+ print(locals())
71
+ args = []
72
+ try:
73
+ kwargs = json.loads(kwargs)
74
+ except Exception:
75
+ kwargs = {{}}
76
+ for kind, name, value in zip(
77
+ tool_param_kinds,
78
+ {tool_param_names},
79
+ [{csv_tool_names}],
80
+ ):
81
+ if kind in {{
82
+ inspect.Parameter.POSITIONAL_ONLY,
83
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
84
+ }}:
85
+ args.append(value if value else None)
86
+ return utils.test_callable(*ARGS, "{tool_name}", *args, **kwargs)
87
+ """, namespace)
88
+
89
+ # Extract tool from namespace and register to the MCP server
90
+ test_btn.click(
91
+ fn=namespace[tool_name], # type: ignore
92
+ inputs=tool_param_inputs,
93
+ outputs=output,
94
+ )
95
+
96
+ # Usage instructions
97
+ with gr.Accordion("Use via MCP 🛠️", open=False):
98
+ gr.Markdown("""\
99
+ **SSE support**: To add this MCP to clients that support SSE (e.g. Cursor, Windsurf, Cline), simply add the following configuration to your MCP config:
100
+
101
+ ```
102
+ {
103
+ "mcpServers": {
104
+ "OpenBB-MCP": {
105
+ "url": "http://xarical-openbb-mcp.hf.space/gradio_api/mcp/sse"
106
+ }
107
+ }
108
+ }
109
+ ```
110
+
111
+ **Stdio support**: For clients that only support stdio, first install Node.js. Then, you can use the following command:
112
+
113
+ ```
114
+ {
115
+ "mcpServers": {
116
+ "OpenBB-MCP": {
117
+ "command": "npx",
118
+ "args": [
119
+ "mcp-remote",
120
+ "http://xarical-openbb-mcp.hf.space/gradio_api/mcp/sse",
121
+ "--transport",
122
+ "sse-only"
123
+ ]
124
+ }
125
+ }
126
+ }
127
+ ```
128
+ """)
129
+
130
+
131
+ if __name__ == "__main__":
132
+ demo.launch(
133
+ server_name="localhost",
134
+ server_port=7860,
135
+ show_api=True,
136
+ mcp_server=True,
137
+ )
config.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from openbb import obb
2
+ import openbb
3
+
4
+
5
+ # Configure LLM mode for OpenBB
6
+ obb.user.preferences.output_type="llm"
7
+ obb.system.python_settings.docstring_sections=['description', 'examples']
8
+ obb.system.python_settings.docstring_max_length=1024
9
+
10
+ # Build OpenBB
11
+ openbb.build()
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio[mcp]
2
+ openbb
utils.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import inspect
2
+ from typing import Any, Optional
3
+
4
+
5
+ def get_nested_attr(
6
+ module: ...,
7
+ attr_path: str,
8
+ ) -> Any | None:
9
+ """Loop through nested attributes."""
10
+ x = module
11
+ for attr in attr_path.split("."):
12
+ x = getattr(x, attr, None)
13
+ return x
14
+
15
+
16
+ def get_callable_names(
17
+ prefix: str,
18
+ module: ...,
19
+ max_depth: int = 3,
20
+ depth: int = 0,
21
+ include: Optional[set[str]] = None,
22
+ ) -> list[str]:
23
+ """
24
+ Recursively retrieve the names of all callables in a given module.
25
+ """
26
+ callable_names = []
27
+ if depth > max_depth:
28
+ return callable_names
29
+ for attr in dir(module):
30
+ if (
31
+ attr.startswith("_") or
32
+ attr in {"model_fields", "model_computed_fields"} or
33
+ include and attr not in include
34
+ ):
35
+ continue
36
+ try:
37
+ value = getattr(module, attr)
38
+ except Exception:
39
+ continue
40
+ full_name = f"{prefix}.{attr}"
41
+ if callable(value):
42
+ callable_names.append(full_name)
43
+ elif hasattr(value, "__dict__") or hasattr(value, "__module__"):
44
+ callable_names.extend(
45
+ get_callable_names(
46
+ full_name,
47
+ value,
48
+ max_depth,
49
+ depth + 1
50
+ ) # Recurse
51
+ )
52
+ return callable_names
53
+
54
+
55
+ def get_callable_params(
56
+ prefix: str,
57
+ module: ...,
58
+ callable_name: str
59
+ ) -> list[dict[str, str]]:
60
+ """
61
+ Get parameter names and default values for a given callable.
62
+ """
63
+ callable = get_nested_attr(
64
+ module,
65
+ callable_name.replace(f"{prefix}.", "", 1)
66
+ )
67
+ if not callable:
68
+ return []
69
+ try:
70
+ sig = inspect.signature(callable)
71
+ params = []
72
+ for name, param in sig.parameters.items():
73
+ if name == "self":
74
+ continue
75
+ default = (
76
+ param.default if
77
+ param.default is not inspect.Parameter.empty else
78
+ ""
79
+ )
80
+ kind = param.kind
81
+ params.append({
82
+ "name": name,
83
+ "default": default,
84
+ "kind": kind
85
+ })
86
+ return params
87
+ except Exception:
88
+ return []
89
+
90
+
91
+ def test_callable(
92
+ prefix: str,
93
+ module: ...,
94
+ callable_name: str,
95
+ *args, **kwargs
96
+ ) -> str:
97
+ """Test a given callable and return the output."""
98
+
99
+ callable = get_nested_attr(
100
+ module,
101
+ callable_name.replace("_", ".").replace(f"{prefix}.", "", 1)
102
+ )
103
+ if not callable:
104
+ return f"Callable '{callable_name}' not found."
105
+ try:
106
+ result = callable(*args, **kwargs)
107
+ return result
108
+ except TypeError as e:
109
+ return f"Callable '{callable_name}' requires arguments: {e}"
110
+ except Exception as e:
111
+ return f"Error running '{callable_name}': {e}"
112
+
113
+
114
+ def generate_callable_guides(prefix: str, module: ..., callable_names: list[str]) -> list[str]:
115
+ """
116
+ Generate callable reference guides from docstrings, parameters, and
117
+ parameter/return types.
118
+ """
119
+ guide = []
120
+ for callable in callable_names:
121
+ callable = get_nested_attr(module, callable.replace(f"{prefix}.", "", 1))
122
+ doc = callable.__doc__ if callable and hasattr(callable, "__doc__") else "No docstring available."
123
+ guide.append(f"{doc}")
124
+ return guide