You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
### Step 2b: Create dynamic tools with `@fenic_tool`
142
+
143
+
Dynamic tools let you expose arbitrary Python logic as an MCP tool. They are defined with the `@fenic_tool` decorator and must return a Fenic `DataFrame`. Annotate parameters with `typing_extensions.Annotated` to provide per-argument descriptions in the tool schema. The server automatically adds `limit` and `table_format` keyword-only parameters for limiting the size of result sets and output formatting.
144
+
145
+
```python
146
+
from typing_extensions import Annotated
147
+
148
+
from fenic import Session
149
+
from fenic.api.dataframe.dataframe import DataFrame
150
+
from fenic.api.functions import col, coalesce, lit
151
+
from fenic.api.functions importsumas sum_
152
+
from fenic.api.mcp.tool_generation import fenic_tool
153
+
154
+
session = Session.get_or_create()
155
+
156
+
# Two base DataFrames
157
+
users = session.create_dataframe({
158
+
"id": [1, 2, 3, 4],
159
+
"name": ["Alice", "Bob", "Charlie", "Diana"],
160
+
"age": [25, 40, 31, 18],
161
+
})
162
+
163
+
orders = session.create_dataframe({
164
+
"order_id": [10, 11, 12, 13, 14],
165
+
"user_id": [1, 1, 2, 3, 3],
166
+
"amount": [50.0, 75.0, 20.0, 15.0, 120.0],
167
+
})
168
+
169
+
# Aggregate orders per user
170
+
orders_total = orders.group_by("user_id").agg(
171
+
sum_(col("amount")).alias("total_amount")
172
+
)
173
+
174
+
175
+
@fenic_tool(
176
+
tool_name="users_with_min_spend",
177
+
tool_description="Users whose name matches regex (optional) and total order amount >= min_total",
178
+
max_result_limit=100,
179
+
default_table_format="markdown",
180
+
)
181
+
defusers_with_min_spend(
182
+
name_regex: Annotated[Optional[str], "Regex for user name (use (?i) for case-insensitive)"] =None,
183
+
min_total: Annotated[float, "Minimum total order amount"],
184
+
) -> DataFrame:
185
+
joined = users.join(orders_total, left_on="id", right_on="user_id", how="left")
186
+
pred_name = col("name").rlike(name_regex) if name_regex else fc.lit(True)
- The decorated function must not use `*args` or `**kwargs` and must return a Fenic `DataFrame`.
194
+
- Use `Annotated[type, "description"]` for parameters to generate a clear MCP schema.
195
+
- Dynamic tools are not stored in the catalog; they exist only while your server process is running.
196
+
- Dynamic tools can be used to integrate your MCP server with external data sources or APIs to perform operations.
197
+
198
+
### Step 2c: Auto-generate core analysis tools from catalog tables
199
+
200
+
You can generate a suite of reusable data tools (Schema, Profile, Read, Search Summary, Search Content, Analyze) directly from catalog tables and their descriptions. This is helpful for quickly exposing exploratory and read/query capabilities to MCP.
201
+
202
+
Requirements:
203
+
204
+
- Each table must exist and have a non-empty description (see Step 1).
205
+
206
+
Example:
207
+
208
+
```python
209
+
from fenic import Session
210
+
from fenic.api.mcp.server import create_mcp_server
211
+
from fenic.api.mcp.tool_generation import ToolGenerationConfig
212
+
213
+
session = Session.get_or_create()
214
+
215
+
server = create_mcp_server(
216
+
session,
217
+
server_name="Fenic MCP",
218
+
automated_tool_generation=ToolGenerationConfig(
219
+
table_names=["orders", "users"],
220
+
tool_group_name="Dataset Exploration",
221
+
sql_max_rows=200,
222
+
),
223
+
)
224
+
```
225
+
141
226
## Step 3a: Serve tools programmatically
142
227
143
228
Use the MCP server helpers to serve existing catalog tools. If you want all registered tools, call `list_tools()`. If you want a subset, fetch by name.
from fenic.api.mcp.tool_generation import fenic_tool
283
+
284
+
# Assume `users_name_regex` is defined as in Step 2b
285
+
server = create_mcp_server(
286
+
session,
287
+
server_name="Fenic MCP",
288
+
parameterized_tools=tools,
289
+
dynamic_tools=[users_name_regex],
290
+
)
291
+
```
292
+
293
+
Enable automated tool generation (Schema/Profile/Read/Search/Analyze) from catalog tables:
294
+
295
+
```python
296
+
from fenic.api.mcp.tool_generation import ToolGenerationConfig
297
+
298
+
server = create_mcp_server(
299
+
session,
300
+
server_name="Fenic MCP",
301
+
automated_tool_generation=ToolGenerationConfig(
302
+
table_names=["orders", "users"],
303
+
tool_group_name="Core Datasets",
304
+
sql_max_rows=200,
305
+
),
306
+
)
307
+
```
308
+
194
309
## Step 3b: Serve tools via CLI (fenic-serve)
195
310
196
311
The CLI starts an MCP server directly from your catalog. By default, it serves all registered tools in the current database, using uvicorn.
@@ -223,6 +338,18 @@ Example `session.config.json` (minimal):
223
338
224
339
Environment variables for model providers (if your tools use semantic operators) should be set in your shell, or via your runner (for example: `uv run --env-file .env ...`).
| Flexibility | Highest: arbitrary Python and Fenic ops; closures; any logic that returns a `DataFrame`. | Moderate: declarative `DataFrame` queries with `tool_param` placeholders. |
346
+
| Persistence/Portability | Not persisted; exist only while the server process is running; require source code at runtime. | Persisted in the catalog; portable across sessions and environments without access to original code. |
347
+
| Discoverability | Not listed in the catalog; visible only via the MCP server that registers them. | First-class catalog objects: `list_tools()`, `get_tool()`, `drop_tool()`. |
348
+
| Parameters/Schema | From function signature using `Annotated[type, "description"]`. `limit` and `table_format` are auto-added. | From `ToolParam` definitions bound to `tool_param(...)` in the plan. Defaults mark params as optional. |
349
+
| Execution context | Executes the returned logical plan from your function; can capture `DataFrame`s or use session access in code. | Executes a stored logical plan with bound parameters from the catalog. |
350
+
| Result formatting |`table_format` supports `markdown` or `structured`; `limit` caps rows (capped by `max_result_limit` if set). | Same. |
351
+
| Best for | Custom logic, semantic/procedural transforms, quick EDA, mixing multiple data sources in code. | Reusable, shareable queries/macros that outlive the application process. |
352
+
226
353
## Troubleshooting
227
354
228
355
- No tools found: ensure you have created tools in the current database (`session.catalog.list_tools()`).
pred = fc.semantic.predicate("Matches: {{q}}", q=query, bio=fc.col("bio"))
109
-
return df.filter(pred).limit(limit)
107
+
pred = fc.semantic.predicate("Matches: {{q}} Data: {{bio}}", q=query, bio=fc.col("bio"))
108
+
return df.filter(pred)
109
+
110
+
mcp_server = fc.create_mcp_server(
111
+
local_session,
112
+
"...",
113
+
dynamic_tools=[find_rust],
114
+
)
115
+
fc.run_mcp_server_sync(mcp_server)
110
116
111
117
Notes:
112
-
- The decorated function MUST NOT use *args/**kwargs, and should annotate parameters with Annotated descriptions.
118
+
- The decorated function MUST NOT use *args/**kwargs
113
119
- The decorated function MUST return a fenic DataFrame.
120
+
- The decorated function SHOULD annotate parameters with `Annotated` types and descriptions.
114
121
- The returned object is a DynamicTool ready for registration.
122
+
- A `limit` parameter is automatically added to the function signature, which can be used to limit the number of rows returned up to the tool's `max_result_limit`.
123
+
- A `table_format` parameter is automatically added to the function signature, which can be used to specify the format of the returned data (markdown, structured)
0 commit comments