This is an automated email from the ASF dual-hosted git repository.

skrawcz pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hamilton.git


The following commit(s) were added to refs/heads/main by this push:
     new 87005ed2 Expose DAG validation as LLM tools via MCP (#1490)
87005ed2 is described below

commit 87005ed2e8f604f030e396ada80d32463b8c4ec8
Author: Dev-iL <[email protected]>
AuthorDate: Sun Mar 8 08:12:42 2026 +0200

    Expose DAG validation as LLM tools via MCP (#1490)
    
    * Exposes DAG validation as LLM tools via MCP
    
    This includes 6 tools that leverage Hamilton's compile-time DAG validation 
for LLM feedback loops:
    
    1. validate_dag
    2. list_nodes
    3. visualize
    4. execute
    5. get_docs
    6. scaffold
    
    Install with `pip install "sf-hamilton[mcp]"`, run via `hamilton-mcp`.
    
    * Fix SDK tests failing with pandas >= 3.0 due to stale sf-hamilton dep
    
    The SDK's requirements.txt referenced `sf-hamilton` (the old package
    name), while the project has been renamed to `apache-hamilton`. Both
    packages provide the `hamilton` Python module, so when pip installed
    `sf-hamilton` from PyPI and then the editable `apache-hamilton` from
    source, the two coexisted — the old sf-hamilton files in site-packages
    shadowed the editable install's .pth redirect to the local source.
    This caused the SDK tests to run against the stale PyPI release, which
    lacks the `pd.__version__ < "3.0"` guards added in 157ecd60 for
    kwargs removed in pandas 3.0 (verbose, keep_date_col, delim_whitespace).
    
    Changes:
    - ui/sdk/requirements.txt: sf-hamilton -> apache-hamilton
    - hamilton-sdk.yml: reorder install steps so the editable source install
      runs after requirements, ensuring it always overrides the PyPI version
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * MCP: add a capability (optional deps) discovery tool/stage
    
    * Add SKILL to accompany the MCP
    
    * MCP: add docs
---
 .claude-plugin/plugin.json           |   3 +-
 .claude-plugin/skills/mcp/SKILL.md   | 231 ++++++++++++++++
 .github/workflows/hamilton-main.yml  |   5 +
 docs/conf.py                         |   1 +
 docs/ecosystem/index.md              |   2 +
 docs/ecosystem/mcp-server.md         | 503 +++++++++++++++++++++++++++++++++++
 hamilton/plugins/h_mcp/__init__.py   |  28 ++
 hamilton/plugins/h_mcp/__main__.py   |  33 +++
 hamilton/plugins/h_mcp/_helpers.py   | 129 +++++++++
 hamilton/plugins/h_mcp/_templates.py | 417 +++++++++++++++++++++++++++++
 hamilton/plugins/h_mcp/server.py     | 328 +++++++++++++++++++++++
 pyproject.toml                       |   3 +
 tests/plugins/test_h_mcp.py          | 365 +++++++++++++++++++++++++
 ui/sdk/requirements.txt              |   2 +-
 14 files changed, 2048 insertions(+), 2 deletions(-)

diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index ace6234b..8c6a716e 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -30,6 +30,7 @@
     "./skills/scale",
     "./skills/llm",
     "./skills/observability",
-    "./skills/integrations"
+    "./skills/integrations",
+    "./skills/mcp"
   ]
 }
diff --git a/.claude-plugin/skills/mcp/SKILL.md 
b/.claude-plugin/skills/mcp/SKILL.md
new file mode 100644
index 00000000..ead67055
--- /dev/null
+++ b/.claude-plugin/skills/mcp/SKILL.md
@@ -0,0 +1,231 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+---
+name: hamilton-mcp
+description: Interactive Hamilton DAG development via MCP tools. Validate, 
visualize, scaffold, and execute Hamilton pipelines without leaving the 
conversation. Use when building or debugging Hamilton dataflows interactively.
+allowed-tools: Read, Grep, Glob, Bash(hamilton-mcp:*), Bash(python:*), 
Bash(pip:*)
+user-invocable: true
+disable-model-invocation: false
+---
+
+# Hamilton MCP Server -- Interactive DAG Development
+
+The Hamilton MCP server exposes Hamilton's DAG compilation, validation, and 
execution as interactive tools. It enables a tight feedback loop: write 
functions, validate the DAG, visualize dependencies, fix errors, and execute -- 
all without leaving the conversation.
+
+## Setup
+
+**Run via uvx (recommended).** Add `--with` for whichever libraries your code 
uses:
+```bash
+uvx --from "apache-hamilton[mcp]" hamilton-mcp                              # 
minimal
+uvx --from "apache-hamilton[mcp]" --with pandas --with numpy hamilton-mcp   # 
pandas/numpy project
+uvx --from "apache-hamilton[mcp]" --with polars hamilton-mcp                # 
polars project
+```
+
+**Or install and run directly:**
+```bash
+pip install "apache-hamilton[mcp]"
+hamilton-mcp
+```
+
+**Or use programmatically:**
+```python
+from hamilton.plugins.h_mcp import get_mcp_server
+
+mcp = get_mcp_server()
+mcp.run()
+```
+
+## Workflow: The Golden Path
+
+Always follow this sequence when building Hamilton DAGs interactively:
+
+```
+ask user -> capabilities -> scaffold -> validate -> visualize -> correct -> 
execute
+```
+
+### Step 1: Ask the User Which Libraries They Use
+
+**Before calling any tool, ask the user which data libraries they use** 
(pandas, numpy, polars, etc.). Then pass their answer as `preferred_libraries` 
to `hamilton_capabilities` and `hamilton_scaffold`. This ensures scaffolds 
match the user's project, not the server's environment.
+
+```json
+// Example: user says "I use pandas"
+// Tool call: hamilton_capabilities(preferred_libraries=["pandas"])
+{
+  "libraries": {
+    "pandas": true,
+    "numpy": true,
+    "polars": false,
+    "graphviz": true
+  },
+  "available_scaffolds": [
+    "basic", "basic_pure_python", "config_based",
+    "data_pipeline", "parameterized"
+  ]
+}
+```
+
+**Decision rules:**
+- If user says pandas: use pandas-based scaffolds and DataFrame/Series types
+- If user has no preference or only uses built-in types: use 
`basic_pure_python` scaffold and `int`/`float`/`str`/`dict` types
+- If `graphviz` is available: use `hamilton_visualize` to show the DAG 
structure
+- Never generate code that imports libraries the user hasn't stated they use
+
+### Step 2: Scaffold a Starting Point
+
+Use `hamilton_scaffold` with a pattern name from the capabilities response:
+
+| Pattern | Libraries Required | Use Case |
+|---------|-------------------|----------|
+| `basic_pure_python` | None | Simple pipelines with built-in types |
+| `basic` | pandas | DataFrame cleaning & counting |
+| `parameterized` | pandas | Multiple nodes from one function |
+| `config_based` | pandas | Environment-conditional logic |
+| `data_pipeline` | pandas | ETL: ingest, clean, transform, aggregate |
+| `ml_pipeline` | pandas, numpy | Feature engineering & train/test split |
+| `data_quality` | pandas, numpy | Validation with `@check_output` |
+
+### Step 3: Validate Before Executing
+
+**Always validate before executing.** `hamilton_validate_dag` compiles the DAG 
without running it, catching:
+- Syntax errors
+- Missing dependencies (parameter names that don't match any function)
+- Type annotation issues
+- Circular references
+
+```json
+// Success response
+{
+  "valid": true,
+  "node_count": 5,
+  "nodes": ["cleaned", "feature_a", "feature_b", "raw_data", "result"],
+  "inputs": ["data_path"],
+  "errors": []
+}
+```
+
+```json
+// Failure response
+{
+  "valid": false,
+  "node_count": 0,
+  "nodes": [],
+  "inputs": [],
+  "errors": [{"type": "SyntaxError", "message": "...", "detail": "line 5"}]
+}
+```
+
+**Self-correction loop:** If validation fails, read the error, fix the code, 
and validate again. Do not proceed to execution until validation passes.
+
+### Step 4: Visualize the DAG (if graphviz available)
+
+`hamilton_visualize` returns DOT graph source. Use this to:
+- Confirm dependency structure matches intent
+- Identify unexpected connections
+- Explain the pipeline to the user
+
+### Step 5: Explore Node Details
+
+`hamilton_list_nodes` returns structured info for every node:
+- Name, output type, documentation
+- Whether it's an external input (must be provided at runtime)
+- Required and optional dependencies
+
+Use this to understand what inputs the DAG needs before execution.
+
+### Step 6: Execute
+
+`hamilton_execute` runs the DAG with provided inputs and returns results. Key 
parameters:
+- `code`: The full Python source
+- `final_vars`: List of node names to compute (only these and their 
dependencies run)
+- `inputs`: Dict of external input values
+- `timeout_seconds`: Safety limit (default 30s)
+
+**WARNING:** This executes arbitrary Python code. Always validate first.
+
+## Error Handling & Self-Correction
+
+### Common Errors and Fixes
+
+**"No module named 'X'"**
+The code imports a library that isn't installed. Call `hamilton_capabilities` 
to check availability, then rewrite without the missing library.
+
+**"Missing dependencies: ['node_name']"**
+A function parameter doesn't match any function name or external input. Either:
+1. Add a function with that name, or
+2. Include it in `inputs` when executing
+
+**"Execution timed out after Ns"**
+The code takes too long. Reduce data size, simplify computation, or increase 
`timeout_seconds`.
+
+**Validation passes but execution fails**
+Validation checks structure, not runtime behavior. Common causes:
+- Missing input values at execution time
+- Runtime exceptions in function bodies (division by zero, key errors)
+- Library-specific errors (e.g., column not found in DataFrame)
+
+### Retry Strategy
+
+1. If a tool returns an error, fix the issue in code and retry once
+2. If the same error recurs, explain the issue to the user and ask for guidance
+3. Never retry more than twice on the same error
+
+## Tool Reference
+
+| Tool | Purpose | When to Use |
+|------|---------|-------------|
+| `hamilton_capabilities` | Environment discovery | **Always first** |
+| `hamilton_scaffold` | Generate starter code | Starting a new pipeline |
+| `hamilton_validate_dag` | Compile-time validation | Before every execution |
+| `hamilton_list_nodes` | Inspect DAG structure | Understanding dependencies |
+| `hamilton_visualize` | DOT graph generation | Explaining structure (requires 
graphviz) |
+| `hamilton_execute` | Run the DAG | After successful validation |
+| `hamilton_get_docs` | Hamilton documentation | Learning decorators, patterns 
|
+
+## Environment Fallbacks
+
+**If the MCP server is not running:**
+Fall back to CLI:
+```bash
+# Validate a module
+python -c "from hamilton import driver; import my_module; dr = 
driver.Builder().with_modules(my_module).build(); print('Valid!')"
+```
+
+**If Hamilton is not installed:**
+Provide the user with installation instructions:
+```bash
+uvx --from "apache-hamilton[mcp]" hamilton-mcp   # Run via uvx (add --with 
<lib> as needed)
+pip install "apache-hamilton[mcp]"              # Or install directly
+```
+
+## Success Criteria
+
+A successful MCP interaction produces:
+1. Code that passes `hamilton_validate_dag` with zero errors
+2. All external inputs identified via `hamilton_list_nodes`
+3. Execution results returned from `hamilton_execute`
+4. The user understands the DAG structure (via visualization or node listing)
+
+## Additional Resources
+
+- For core Hamilton patterns: use `/hamilton-core`
+- For scaling with async/Spark: use `/hamilton-scale`
+- For LLM workflow patterns: use `/hamilton-llm`
+- For observability: use `/hamilton-observability`
+- Hamilton documentation: `hamilton_get_docs("overview")`
diff --git a/.github/workflows/hamilton-main.yml 
b/.github/workflows/hamilton-main.yml
index f13bb657..ea1c2c5b 100644
--- a/.github/workflows/hamilton-main.yml
+++ b/.github/workflows/hamilton-main.yml
@@ -85,6 +85,11 @@ jobs:
             uv pip install -r tests/integrations/pandera/requirements.txt
             uv run pytest tests/integrations
 
+        - name: Test MCP
+          run: |
+            uv sync --group test --extra mcp
+            uv run pytest tests/plugins/test_h_mcp.py
+
         - name: Test pandas
           run: |
             uv sync --group test
diff --git a/docs/conf.py b/docs/conf.py
index bac11012..446e09bb 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -75,6 +75,7 @@ extensions = [
     "myst_nb",
     "sphinx_llms_txt",
     "sphinx_sitemap",
+    "sphinxcontrib.mermaid",
     "docs.data_adapters_extension",
 ]
 
diff --git a/docs/ecosystem/index.md b/docs/ecosystem/index.md
index 161843a3..789dcee6 100644
--- a/docs/ecosystem/index.md
+++ b/docs/ecosystem/index.md
@@ -119,6 +119,7 @@ Improve your development workflow:
 | <img src="../_static/logos/jupyter.png" width="20" height="20" 
style="vertical-align: middle;"> **Jupyter** | Notebook magic commands | 
[Examples](https://github.com/apache/hamilton/tree/main/examples/jupyter_notebook_magic)
 |
 | <img src="../_static/logos/vscode.png" width="20" height="20" 
style="vertical-align: middle;"> **VS Code** | Language server and extension | 
[VS Code Guide](../hamilton-vscode/index.rst) |
 | **Claude Code** | AI assistant plugin for Hamilton development | [Plugin 
Guide](claude-code-plugin.md) |
+| **MCP Server** | LLM tool server for interactive DAG development | [MCP 
Guide](mcp-server.md) |
 | <img src="../_static/logos/tqdm.png" width="20" height="20" 
style="vertical-align: middle;"> **tqdm** | Progress bars | [Lifecycle 
Hook](../reference/lifecycle-hooks/ProgressBar.rst) |
 
 ### Cloud Providers & Infrastructure
@@ -226,4 +227,5 @@ If you've created a plugin or integration for Apache 
Hamilton, we'd love to incl
 :hidden:
 
 claude-code-plugin
+mcp-server
 ```
diff --git a/docs/ecosystem/mcp-server.md b/docs/ecosystem/mcp-server.md
new file mode 100644
index 00000000..087bda02
--- /dev/null
+++ b/docs/ecosystem/mcp-server.md
@@ -0,0 +1,503 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# MCP Server
+
+The Hamilton MCP server exposes Hamilton's DAG compilation, validation, and 
execution as tools for LLM clients via the [Model Context 
Protocol](https://modelcontextprotocol.io/) (MCP). It enables LLM-driven 
development workflows where you write Hamilton functions, validate the DAG, 
visualize dependencies, and execute pipelines -- all through natural language 
interaction with clients such as Claude Desktop, Claude Code, or Cursor.
+
+## Architecture
+
+```{mermaid}
+graph LR
+    A[LLM Client] -->|MCP stdio transport| B[Hamilton MCP Server]
+    B -->|build & execute| C[Hamilton Driver]
+    C -->|compile DAG from| D[User Code]
+```
+
+The server runs as a local process using **stdio transport**. The LLM client 
launches the server, sends tool calls over stdin/stdout, and receives 
structured JSON responses. Internally, the server compiles user-provided Python 
source into a Hamilton DAG via `hamilton.driver.Builder` (with dynamic 
execution enabled), then performs the requested operation (validate, list 
nodes, visualize, or execute).
+
+## Installation
+
+The recommended way to run the MCP server is via `uvx`, which handles 
installation automatically:
+
+```bash
+uvx --from "apache-hamilton[mcp]" hamilton-mcp
+```
+
+This creates an ephemeral environment with Hamilton and 
[FastMCP](https://github.com/PrefectHQ/fastmcp), runs the server, and cleans up 
after. No pre-installation required.
+
+Alternatively, install into your own environment:
+
+```bash
+pip install "apache-hamilton[mcp]"
+```
+
+### Optional dependencies
+
+The server can compile and execute user code, so any libraries your code 
imports must be available in the server's environment. When using `uvx`, pass 
`--with` to include them:
+
+| Library | Install command | What it enables |
+|---------|----------------|-----------------|
+| pandas | `pip install pandas` | DataFrame-based scaffold templates and 
result serialization |
+| numpy | `pip install numpy` | ML pipeline scaffolds and ndarray 
serialization |
+| polars | `pip install polars` | Polars DataFrame/Series serialization |
+| graphviz | `pip install "apache-hamilton[visualization]"` | DAG 
visualization via `hamilton_visualize` |
+
+For example, for a pandas/numpy project:
+
+```bash
+uvx --from "apache-hamilton[mcp]" --with pandas --with numpy hamilton-mcp
+```
+
+Or for a polars project:
+
+```bash
+uvx --from "apache-hamilton[mcp]" --with polars hamilton-mcp
+```
+
+## Running the Server
+
+### Via uvx (recommended)
+
+```bash
+uvx --from "apache-hamilton[mcp]" hamilton-mcp
+```
+
+Include the libraries your code uses via `--with`:
+
+```bash
+# pandas/numpy project
+uvx --from "apache-hamilton[mcp]" --with pandas --with numpy hamilton-mcp
+
+# polars project
+uvx --from "apache-hamilton[mcp]" --with polars hamilton-mcp
+```
+
+### Direct CLI
+
+If Hamilton is already installed in your environment:
+
+```bash
+hamilton-mcp
+```
+
+### Programmatic
+
+You can also start the server from Python:
+
+```python
+from hamilton.plugins.h_mcp import get_mcp_server
+
+mcp = get_mcp_server()
+mcp.run()
+```
+
+The `get_mcp_server()` function returns a `FastMCP` instance. Call `.run()` to 
start the stdio transport loop.
+
+## Connecting to an LLM Client
+
+### Claude Desktop
+
+Add the following to your Claude Desktop configuration file 
(`claude_desktop_config.json`). Add `"--with", "<library>"` pairs for whichever 
libraries your code uses:
+
+```json
+{
+  "mcpServers": {
+    "hamilton": {
+      "command": "uvx",
+      "args": [
+        "--from", "apache-hamilton[mcp]",
+        "--with", "pandas",
+        "--with", "numpy",
+        "hamilton-mcp"
+      ]
+    }
+  }
+}
+```
+
+### Claude Code
+
+Add the server to your project configuration, including `--with` flags for 
your libraries:
+
+```bash
+claude mcp add hamilton -- uvx --from "apache-hamilton[mcp]" --with pandas 
--with numpy hamilton-mcp
+```
+
+Or add it to your `.mcp.json` file:
+
+```json
+{
+  "mcpServers": {
+    "hamilton": {
+      "command": "uvx",
+      "args": [
+        "--from", "apache-hamilton[mcp]",
+        "--with", "pandas",
+        "--with", "numpy",
+        "hamilton-mcp"
+      ]
+    }
+  }
+}
+```
+
+See also the [Claude Code Plugin](claude-code-plugin.md) for an accompanying 
skill that provides workflow guidance when using the MCP tools.
+
+### Cursor
+
+In Cursor, open **Settings > MCP Servers** and add (adjust `--with` flags to 
match your project):
+
+```json
+{
+  "hamilton": {
+    "command": "uvx",
+    "args": [
+      "--from", "apache-hamilton[mcp]",
+      "--with", "pandas",
+      "--with", "numpy",
+      "hamilton-mcp"
+    ]
+  }
+}
+```
+
+### Other MCP-compatible clients
+
+Any client that supports the MCP stdio transport can connect by launching the 
server as a subprocess. The server reads JSON-RPC messages from stdin and 
writes responses to stdout.
+
+```bash
+uvx --from "apache-hamilton[mcp]" hamilton-mcp
+```
+
+Add `--with <library>` for any libraries your code imports (e.g., `--with 
pandas --with numpy` or `--with polars`).
+
+## Available Tools
+
+The server exposes seven tools. Each tool accepts and returns JSON.
+
+### `hamilton_capabilities`
+
+Report which optional libraries are installed and which features are 
available. Call this first to discover the environment before generating code. 
If the user specifies which libraries they use, pass them as 
`preferred_libraries` to filter scaffold patterns accordingly.
+
+**Parameters:**
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `preferred_libraries` | `list[str] \| None` | No | Libraries the user wants 
to use (e.g., `["pandas", "numpy"]`). When provided, scaffolds are filtered to 
only those whose requirements are a subset of this list. When omitted, falls 
back to auto-detection. |
+
+**Returns:** A dict with `libraries` (library name to boolean, reflecting what 
is installed) and `available_scaffolds` (list of scaffold pattern names 
filtered by preference or auto-detection).
+
+---
+
+### `hamilton_validate_dag`
+
+Validate Hamilton DAG code by building the Driver. Compiles Python source into 
a Hamilton DAG and checks for missing dependencies, type mismatches, and 
circular references without executing the code.
+
+**Parameters:**
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `code` | `str` | Yes | Python source code defining Hamilton functions |
+| `config` | `dict \| None` | No | Hamilton config dict passed to 
`Builder.with_config()` |
+
+**Returns:** `{"valid": true, "node_count": N, "nodes": [...], "inputs": 
[...], "errors": []}` on success, or `{"valid": false, "node_count": 0, 
"nodes": [], "inputs": [], "errors": [...]}` on failure. Each error is a dict 
with `type`, `message`, `detail` (may be `null` for non-syntax errors), and 
`traceback` fields.
+
+---
+
+### `hamilton_list_nodes`
+
+List all nodes in a Hamilton DAG with their types and dependencies. Returns 
structured info for every node including name, output type, tags, whether it is 
an external input, and its required/optional dependencies.
+
+**Parameters:**
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `code` | `str` | Yes | Python source code defining Hamilton functions |
+| `config` | `dict \| None` | No | Hamilton config dict passed to 
`Builder.with_config()` |
+
+**Returns:** `{"nodes": [...], "errors": []}`. Each node is a dict with 
`name`, `type`, `is_external_input`, `tags`, `required_dependencies`, 
`optional_dependencies`, and `documentation` fields.
+
+---
+
+### `hamilton_visualize`
+
+Visualize the Hamilton DAG as DOT graph source. Requires `graphviz` (`pip 
install "apache-hamilton[visualization]"`).
+
+**Parameters:**
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `code` | `str` | Yes | Python source code defining Hamilton functions |
+| `config` | `dict \| None` | No | Hamilton config dict passed to 
`Builder.with_config()` |
+| `output_format` | `str` | No | Output format (default: `"dot"`). Currently 
only `"dot"` is supported; other values are accepted but ignored. |
+
+**Returns:** A string containing the Graphviz DOT source for the dependency 
graph, or an error message if graphviz is not installed.
+
+---
+
+### `hamilton_execute`
+
+Execute a Hamilton DAG and return the requested outputs. Builds the DAG from 
source, then calls `driver.execute()` with the given `final_vars` and `inputs`. 
Results are serialized to JSON-safe values. A timeout (default 30 seconds) 
guards against long-running code.
+
+**Parameters:**
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `code` | `str` | Yes | Python source code defining Hamilton functions |
+| `final_vars` | `list[str]` | Yes | Node names to compute |
+| `inputs` | `dict \| None` | No | External input values |
+| `config` | `dict \| None` | No | Hamilton config dict passed to 
`Builder.with_config()` |
+| `timeout_seconds` | `int` | No | Execution timeout in seconds (default: 
`30`) |
+
+**Returns:** `{"results": {...}, "execution_time_ms": N}` on success. On 
failure: `{"error": "..."}` for build errors or timeouts, or `{"error": "...", 
"execution_time_ms": N}` for runtime errors during execution. Results are 
serialized: pandas DataFrames/Series become dicts via `.to_dict()`, numpy 
arrays become lists via `.tolist()`, polars DataFrames become list-of-dicts via 
`.to_dicts()`, polars Series become lists via `.to_list()`, and all other types 
use `str()`.
+
+**Warning:** This tool executes arbitrary Python code. Always validate with 
`hamilton_validate_dag` before executing.
+
+---
+
+### `hamilton_get_docs`
+
+Get Hamilton documentation for a specific topic.
+
+**Parameters:**
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `topic` | `str` | Yes | One of: `overview`, `decorators`, `driver`, 
`builder`, or any decorator name (`parameterize`, `extract_columns`, `config`, 
`check_output`, `tag`, `pipe`, `does`, `subdag`, etc.) |
+
+**Returns:** A string containing the requested documentation.
+
+---
+
+### `hamilton_scaffold`
+
+Generate a starter Hamilton module for a given pattern. Available patterns 
depend on installed or preferred libraries. Returns Python source code that is 
a valid Hamilton module, plus a driver script example.
+
+**Parameters:**
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `pattern` | `str` | Yes | Scaffold pattern name (use `hamilton_capabilities` 
to discover available patterns) |
+| `preferred_libraries` | `list[str] \| None` | No | Libraries the user wants 
to use. Filters available patterns the same way as in `hamilton_capabilities`. |
+
+**Returns:** A string containing Python source code for the requested scaffold 
pattern, or an error listing available patterns if the name is invalid.
+
+**Available scaffold patterns:**
+
+| Pattern | Required libraries | Description |
+|---------|-------------------|-------------|
+| `basic_pure_python` | None | Simple data processing pipeline using only 
built-in Python types |
+| `basic` | pandas | DataFrame cleaning and row counting |
+| `parameterized` | pandas | Using `@parameterize` to create multiple nodes 
from one function |
+| `config_based` | pandas | Conditional logic with `@config.when` |
+| `data_pipeline` | pandas | ETL workflow: ingest, clean, transform, aggregate 
|
+| `ml_pipeline` | pandas, numpy | Feature engineering and train/test split |
+| `data_quality` | pandas, numpy | Data validation with `@check_output` |
+
+## Conditional Features
+
+The MCP server adapts to the libraries installed in the current Python 
environment. At startup, it probes for four optional libraries:
+
+- **pandas** -- Enables DataFrame-based scaffold templates and 
DataFrame/Series result serialization
+- **numpy** -- Enables the ML pipeline and data quality scaffolds, plus 
ndarray serialization
+- **polars** -- Enables Polars DataFrame/Series result serialization
+- **graphviz** -- Enables DAG visualization via `hamilton_visualize`
+
+The `hamilton_capabilities` tool reports which libraries are installed. Both 
`hamilton_capabilities` and `hamilton_scaffold` accept an optional 
`preferred_libraries` parameter: when provided, scaffolds are filtered to match 
the user's stated preferences rather than relying on auto-detection. This is 
especially useful with `uvx`, where the server runs in an ephemeral environment 
that may not reflect the user's project.
+
+When no preference is given, the tools fall back to auto-detecting installed 
libraries.
+
+## Usage Examples
+
+### Example 1: Ask the user, discover capabilities, and scaffold
+
+Start by asking which libraries the user works with, then pass their 
preferences:
+
+```
+User: I want to build a Hamilton pipeline.
+
+LLM: Which data libraries do you use? (e.g., pandas, numpy, polars)
+
+User: I use pandas.
+
+Tool call: hamilton_capabilities(preferred_libraries=["pandas"])
+Response:
+{
+  "libraries": {
+    "pandas": true,
+    "numpy": true,
+    "polars": false,
+    "graphviz": true
+  },
+  "available_scaffolds": [
+    "basic", "basic_pure_python", "config_based",
+    "data_pipeline", "parameterized"
+  ]
+}
+
+Tool call: hamilton_scaffold(pattern="data_pipeline", 
preferred_libraries=["pandas"])
+Response:
+"""Hamilton data pipeline: ingest -> clean -> transform -> aggregate."""
+import pandas as pd
+
+def raw_data(raw_data_input: pd.DataFrame) -> pd.DataFrame:
+    """Ingest raw data."""
+    return raw_data_input
+
+def cleaned_data(raw_data: pd.DataFrame) -> pd.DataFrame:
+    """Remove nulls and duplicates."""
+    return raw_data.dropna().drop_duplicates()
+...
+```
+
+### Example 2: Validate and fix, then execute
+
+Write code, validate it, fix errors, and run:
+
+```
+Tool call: hamilton_validate_dag(
+  code="""
+import pandas as pd
+
+def raw_data(raw_data_input: pd.DataFrame) -> pd.DataFrame:
+    return raw_data_input
+
+def cleaned(raw_data: pd.DataFrame) -> pd.DataFrame:
+    return raw_data.dropna()
+
+def row_count(cleaned: pd.DataFrame) -> int:
+    return len(cleaned)
+"""
+)
+Response:
+{
+  "valid": true,
+  "node_count": 3,
+  "nodes": ["cleaned", "raw_data", "row_count"],
+  "inputs": ["raw_data_input"],
+  "errors": []
+}
+
+Tool call: hamilton_execute(
+  code="...(same code)...",
+  final_vars=["row_count"],
+  inputs={"raw_data_input": {"a": [1, 2, None], "b": [4, None, 6]}}
+)
+Response:
+{
+  "results": {"row_count": "1"},
+  "execution_time_ms": 12.3
+}
+```
+
+### Example 3: Explore documentation on decorators
+
+```
+Tool call: hamilton_get_docs(topic="parameterize")
+Response:
+@parameterize
+
+Creates multiple nodes from a single function definition.
+Each parameterization defines a new node name and the argument
+values to pass to the function...
+```
+
+## Recommended Workflow
+
+The server instructions recommend the following sequence for interactive DAG 
development:
+
+```
+ask user -> capabilities -> scaffold -> validate -> visualize -> correct -> 
execute
+```
+
+1. **Ask the user** -- Ask which data libraries they use (pandas, numpy, 
polars, etc.), then pass the answer as `preferred_libraries` to capabilities 
and scaffold.
+2. **Capabilities** -- Call `hamilton_capabilities` to discover the 
environment.
+3. **Scaffold** -- Use `hamilton_scaffold` to generate a starting point 
matched to the user's preferred libraries.
+4. **Validate** -- Call `hamilton_validate_dag` to compile the DAG without 
executing it.
+5. **Visualize** -- Call `hamilton_visualize` to inspect the dependency graph 
(requires graphviz).
+6. **Correct** -- If validation fails, read the error, fix the code, and 
validate again.
+7. **Execute** -- Call `hamilton_execute` after validation passes.
+
+## Accompanying Claude Code Skill
+
+The Hamilton [Claude Code Plugin](claude-code-plugin.md) includes a dedicated 
**hamilton-mcp** skill that provides workflow guidance for using the MCP tools. 
The skill is defined in `.claude-plugin/skills/mcp/SKILL.md` and is registered 
in the plugin manifest.
+
+The skill provides:
+
+- **Golden path workflow** -- Step-by-step instructions for the 
capabilities-scaffold-validate-visualize-correct-execute loop
+- **Decision rules** -- Guidance on choosing scaffolds based on available 
libraries
+- **Error handling strategy** -- Common errors, their causes, and fixes 
(missing modules, missing dependencies, timeouts, runtime vs. validation 
failures)
+- **Retry policy** -- Retry once on error, explain to the user on second 
failure, never retry more than twice on the same error
+- **Success criteria** -- A clear definition of what constitutes a successful 
interaction: zero validation errors, all inputs identified, results returned, 
and the user understands the DAG structure
+
+When Claude Code detects the Hamilton MCP server, the skill activates 
automatically and guides the LLM through best-practice usage of the tools.
+
+## Troubleshooting
+
+### `ModuleNotFoundError: FastMCP is required`
+
+The MCP extra is not installed. Use `uvx` which handles installation 
automatically:
+
+```bash
+uvx --from "apache-hamilton[mcp]" hamilton-mcp
+```
+
+Or install manually: `pip install "apache-hamilton[mcp]"`
+
+### `hamilton-mcp: command not found`
+
+If running directly (without `uvx`), the CLI entrypoint may not be on your 
`PATH`. Either use `uvx` (recommended), activate the virtual environment where 
Hamilton is installed, or use the full path:
+
+```bash
+/path/to/your/venv/bin/hamilton-mcp
+```
+
+### Visualization returns an error
+
+The `hamilton_visualize` tool requires the `graphviz` Python package and the 
Graphviz system binary. Install both:
+
+```bash
+pip install "apache-hamilton[visualization]"
+
+# On Ubuntu/Debian:
+sudo apt-get install graphviz
+
+# On macOS:
+brew install graphviz
+```
+
+### Execution times out
+
+The default timeout is 30 seconds. For long-running pipelines, pass a higher 
`timeout_seconds` value to `hamilton_execute`. Alternatively, reduce the data 
size or simplify the computation.
+
+### Validation passes but execution fails
+
+Validation checks DAG structure (dependencies, types, circular references) but 
does not run function bodies. Common runtime failures include:
+
+- Missing input values not provided in the `inputs` dict
+- Exceptions inside function bodies (division by zero, key errors)
+- Library-specific errors (e.g., column not found in a DataFrame)
+
+## Learn More
+
+- [Hamilton documentation](https://hamilton.apache.org) -- Full framework 
reference
+- [Claude Code Plugin](claude-code-plugin.md) -- AI-powered Hamilton 
development in Claude Code
+- [CLI reference](../how-tos/cli-reference.md) -- The `hamilton` CLI for 
build, diff, version, and view
+- [Model Context Protocol](https://modelcontextprotocol.io/) -- MCP 
specification and ecosystem
diff --git a/hamilton/plugins/h_mcp/__init__.py 
b/hamilton/plugins/h_mcp/__init__.py
new file mode 100644
index 00000000..b542848a
--- /dev/null
+++ b/hamilton/plugins/h_mcp/__init__.py
@@ -0,0 +1,28 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Hamilton MCP server plugin -- exposes DAG validation as interactive 
tools."""
+
+
+def get_mcp_server():
+    """Get the Hamilton MCP server instance.
+
+    Requires fastmcp: pip install "apache-hamilton[mcp]"
+    """
+    from hamilton.plugins.h_mcp.server import mcp
+
+    return mcp
diff --git a/hamilton/plugins/h_mcp/__main__.py 
b/hamilton/plugins/h_mcp/__main__.py
new file mode 100644
index 00000000..ef52408d
--- /dev/null
+++ b/hamilton/plugins/h_mcp/__main__.py
@@ -0,0 +1,33 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+def main():
+    try:
+        from fastmcp import FastMCP  # noqa: F401
+    except ModuleNotFoundError as e:
+        raise ModuleNotFoundError(
+            "FastMCP is required. Install with: pip install 
'apache-hamilton[mcp]'"
+        ) from e
+
+    from hamilton.plugins.h_mcp.server import mcp
+
+    mcp.run()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/hamilton/plugins/h_mcp/_helpers.py 
b/hamilton/plugins/h_mcp/_helpers.py
new file mode 100644
index 00000000..466ad447
--- /dev/null
+++ b/hamilton/plugins/h_mcp/_helpers.py
@@ -0,0 +1,129 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+import linecache
+import sys
+import traceback
+from typing import TYPE_CHECKING
+from uuid import uuid4
+
+from hamilton import ad_hoc_utils, driver
+
+if TYPE_CHECKING:
+    from types import ModuleType
+
+
+def build_driver_from_code(
+    code: str, config: dict | None = None
+) -> tuple[driver.Driver, ModuleType]:
+    """Build a Hamilton Driver from a code string.
+
+    Uses ad_hoc_utils.module_from_source to create a temp module,
+    then builds the Driver with dynamic execution enabled.
+
+    :param code: Python source code defining Hamilton functions.
+    :param config: Optional Hamilton config dict.
+    :return: Tuple of (Driver, temp_module). Caller must clean up the module.
+    """
+    module_name = f"mcp_temp_{uuid4().hex}"
+    module = ad_hoc_utils.module_from_source(code, module_name=module_name)
+    dr = (
+        driver.Builder()
+        .enable_dynamic_execution(allow_experimental_mode=True)
+        .with_modules(module)
+        .with_config(config or {})
+        .build()
+    )
+    return dr, module
+
+
+def cleanup_temp_module(module: ModuleType) -> None:
+    """Remove a temporary module from sys.modules and linecache."""
+    name = module.__name__
+    sys.modules.pop(name, None)
+    linecache.cache.pop(name, None)
+
+
+def format_validation_errors(exc: Exception) -> list[dict]:
+    """Parse a Hamilton or Python exception into structured error dicts.
+
+    Returns a list of ``{"type": ..., "message": ..., "detail": ...}`` dicts.
+    """
+    error_type = type(exc).__name__
+    message = str(exc)
+
+    if isinstance(exc, SyntaxError):
+        return [
+            {
+                "type": error_type,
+                "message": message,
+                "detail": f"line {exc.lineno}" if exc.lineno else None,
+            }
+        ]
+
+    return [{"type": error_type, "message": message, "detail": None}]
+
+
+def format_exception_chain(exc: Exception) -> list[dict]:
+    """Format an exception including its full traceback as structured 
errors."""
+    errors = format_validation_errors(exc)
+    tb = traceback.format_exception(type(exc), exc, exc.__traceback__)
+    errors[0]["traceback"] = "".join(tb)
+    return errors
+
+
+def serialize_results(results: dict) -> dict[str, str]:
+    """Convert Hamilton execution results to JSON-safe string representations.
+
+    Handles pandas DataFrames/Series via .to_dict(), falls back to str().
+    """
+    serialized = {}
+    for key, val in results.items():
+        try:
+            import pandas as pd
+
+            if isinstance(val, pd.DataFrame):
+                serialized[key] = val.to_dict()
+                continue
+            if isinstance(val, pd.Series):
+                serialized[key] = val.to_dict()
+                continue
+        except ImportError:
+            pass
+        try:
+            import numpy as np
+
+            if isinstance(val, np.ndarray):
+                serialized[key] = val.tolist()
+                continue
+        except ImportError:
+            pass
+        try:
+            import polars as pl
+
+            if isinstance(val, pl.DataFrame):
+                serialized[key] = val.to_dicts()
+                continue
+            if isinstance(val, pl.Series):
+                serialized[key] = val.to_list()
+                continue
+        except ImportError:
+            pass
+        serialized[key] = str(val)
+    return serialized
diff --git a/hamilton/plugins/h_mcp/_templates.py 
b/hamilton/plugins/h_mcp/_templates.py
new file mode 100644
index 00000000..fca7dd55
--- /dev/null
+++ b/hamilton/plugins/h_mcp/_templates.py
@@ -0,0 +1,417 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+import dataclasses
+import textwrap
+
+try:
+    import pandas  # noqa: F401
+
+    HAS_PANDAS = True
+except ImportError:
+    HAS_PANDAS = False
+
+try:
+    import numpy  # noqa: F401
+
+    HAS_NUMPY = True
+except ImportError:
+    HAS_NUMPY = False
+
+try:
+    import polars  # noqa: F401
+
+    HAS_POLARS = True
+except ImportError:
+    HAS_POLARS = False
+
+try:
+    import graphviz  # noqa: F401
+
+    HAS_GRAPHVIZ = True
+except ImportError:
+    HAS_GRAPHVIZ = False
+
+AVAILABLE_LIBS: dict[str, bool] = {
+    "pandas": HAS_PANDAS,
+    "numpy": HAS_NUMPY,
+    "polars": HAS_POLARS,
+    "graphviz": HAS_GRAPHVIZ,
+}
+
+
[email protected](frozen=True)
+class ScaffoldTemplate:
+    """A scaffold template with its library requirements."""
+
+    name: str
+    description: str
+    code: str
+    requires: frozenset[str] = frozenset()
+
+
+ALL_TEMPLATES: list[ScaffoldTemplate] = [
+    ScaffoldTemplate(
+        name="basic_pure_python",
+        description="Simple data processing pipeline using only built-in 
Python types.",
+        requires=frozenset(),
+        code=textwrap.dedent('''\
+            """Basic Hamilton module example (pure Python, no external 
libraries)."""
+
+
+            def raw_value(raw_value_input: int) -> int:
+                """Pass-through for raw input value."""
+                return raw_value_input
+
+
+            def doubled(raw_value: int) -> int:
+                """Double the value."""
+                return raw_value * 2
+
+
+            def message(doubled: int) -> str:
+                """Create a summary message."""
+                return f"Result: {doubled}"
+
+
+            # --- Driver script ---
+            # from hamilton import driver
+            # import my_module
+            #
+            # dr = driver.Builder().with_modules(my_module).build()
+            # result = dr.execute(
+            #     ["message", "doubled"],
+            #     inputs={"raw_value_input": 5},
+            # )
+            # print(result)
+        '''),
+    ),
+    ScaffoldTemplate(
+        name="basic",
+        description="Simple data processing pipeline with pandas.",
+        requires=frozenset({"pandas"}),
+        code=textwrap.dedent('''\
+            """Basic Hamilton module example."""
+            import pandas as pd
+
+
+            def raw_data(raw_data_input: pd.DataFrame) -> pd.DataFrame:
+                """Pass-through for raw input data."""
+                return raw_data_input
+
+
+            def cleaned(raw_data: pd.DataFrame) -> pd.DataFrame:
+                """Drop rows with missing values."""
+                return raw_data.dropna()
+
+
+            def row_count(cleaned: pd.DataFrame) -> int:
+                """Count rows after cleaning."""
+                return len(cleaned)
+
+
+            # --- Driver script ---
+            # from hamilton import driver
+            # import my_module
+            #
+            # dr = driver.Builder().with_modules(my_module).build()
+            # result = dr.execute(
+            #     ["row_count", "cleaned"],
+            #     inputs={"raw_data_input": pd.DataFrame({"a": [1, 2, None], 
"b": [4, None, 6]})},
+            # )
+            # print(result)
+        '''),
+    ),
+    ScaffoldTemplate(
+        name="parameterized",
+        description="Using @parameterize to create multiple nodes from one 
function.",
+        requires=frozenset({"pandas"}),
+        code=textwrap.dedent('''\
+            """Hamilton module using @parameterize to create multiple nodes."""
+            import pandas as pd
+
+            from hamilton.function_modifiers import parameterize, value
+
+
+            @parameterize(
+                weekly_mean={"window": value(7)},
+                monthly_mean={"window": value(30)},
+            )
+            def rolling_mean(time_series: pd.Series, window: int) -> pd.Series:
+                """Compute a rolling mean with a given window size."""
+                return time_series.rolling(window).mean()
+
+
+            def time_series(time_series_input: pd.Series) -> pd.Series:
+                """Pass-through for time series input."""
+                return time_series_input
+
+
+            # --- Driver script ---
+            # from hamilton import driver
+            # import my_module
+            #
+            # dr = driver.Builder().with_modules(my_module).build()
+            # result = dr.execute(
+            #     ["weekly_mean", "monthly_mean"],
+            #     inputs={"time_series_input": pd.Series(range(60))},
+            # )
+        '''),
+    ),
+    ScaffoldTemplate(
+        name="config_based",
+        description="Conditional logic with @config.when.",
+        requires=frozenset({"pandas"}),
+        code=textwrap.dedent('''\
+            """Hamilton module using @config.when for conditional logic."""
+            import pandas as pd
+
+            from hamilton.function_modifiers import config
+
+
+            @config.when(env="production")
+            def data_source__prod(db_connection_string: str) -> pd.DataFrame:
+                """Load data from production database."""
+                # In real code: pd.read_sql("SELECT * FROM table", 
db_connection_string)
+                return pd.DataFrame({"value": [1, 2, 3]})
+
+
+            @config.when(env="development")
+            def data_source__dev() -> pd.DataFrame:
+                """Return sample data for development."""
+                return pd.DataFrame({"value": [10, 20, 30]})
+
+
+            def processed(data_source: pd.DataFrame) -> pd.DataFrame:
+                """Process the data source."""
+                return data_source.assign(doubled=data_source["value"] * 2)
+
+
+            # --- Driver script ---
+            # from hamilton import driver
+            # import my_module
+            #
+            # dr = (
+            #     driver.Builder()
+            #     .with_modules(my_module)
+            #     .with_config({"env": "development"})
+            #     .build()
+            # )
+            # result = dr.execute(["processed"])
+        '''),
+    ),
+    ScaffoldTemplate(
+        name="data_pipeline",
+        description="ETL workflow: ingest -> clean -> transform -> aggregate.",
+        requires=frozenset({"pandas"}),
+        code=textwrap.dedent('''\
+            """Hamilton data pipeline: ingest -> clean -> transform -> 
aggregate."""
+            import pandas as pd
+
+
+            def raw_data(raw_data_input: pd.DataFrame) -> pd.DataFrame:
+                """Ingest raw data."""
+                return raw_data_input
+
+
+            def cleaned_data(raw_data: pd.DataFrame) -> pd.DataFrame:
+                """Remove nulls and duplicates."""
+                return raw_data.dropna().drop_duplicates()
+
+
+            def spend(cleaned_data: pd.DataFrame) -> pd.Series:
+                """Extract the spend column."""
+                return cleaned_data["spend"].abs()
+
+
+            def avg_spend(spend: pd.Series) -> float:
+                """Average spend across all records."""
+                return spend.mean()
+
+
+            def total_spend(spend: pd.Series) -> float:
+                """Total spend across all records."""
+                return spend.sum()
+
+
+            # --- Driver script ---
+            # from hamilton import driver
+            # import my_module
+            #
+            # dr = driver.Builder().with_modules(my_module).build()
+            # result = dr.execute(
+            #     ["avg_spend", "total_spend"],
+            #     inputs={"raw_data_input": pd.DataFrame({"spend": [-10, 20, 
-30]})},
+            # )
+        '''),
+    ),
+    ScaffoldTemplate(
+        name="ml_pipeline",
+        description="Feature engineering and train/test split with pandas and 
numpy.",
+        requires=frozenset({"pandas", "numpy"}),
+        code=textwrap.dedent('''\
+            """Hamilton ML pipeline: features -> train/test split -> model -> 
metrics."""
+            import pandas as pd
+            import numpy as np
+
+
+            def feature_matrix(feature_matrix_input: pd.DataFrame) -> 
pd.DataFrame:
+                """Input feature matrix."""
+                return feature_matrix_input
+
+
+            def target(target_input: pd.Series) -> pd.Series:
+                """Input target variable."""
+                return target_input
+
+
+            def train_fraction() -> float:
+                """Fraction of data for training."""
+                return 0.8
+
+
+            def train_indices(
+                feature_matrix: pd.DataFrame, train_fraction: float
+            ) -> np.ndarray:
+                """Random train indices."""
+                n = len(feature_matrix)
+                idx = np.arange(n)
+                np.random.shuffle(idx)
+                return idx[: int(n * train_fraction)]
+
+
+            def test_indices(
+                feature_matrix: pd.DataFrame, train_indices: np.ndarray
+            ) -> np.ndarray:
+                """Test indices (complement of train)."""
+                all_idx = set(range(len(feature_matrix)))
+                return np.array(sorted(all_idx - set(train_indices)))
+
+
+            def train_X(feature_matrix: pd.DataFrame, train_indices: 
np.ndarray) -> pd.DataFrame:
+                """Training features."""
+                return feature_matrix.iloc[train_indices]
+
+
+            def test_X(feature_matrix: pd.DataFrame, test_indices: np.ndarray) 
-> pd.DataFrame:
+                """Test features."""
+                return feature_matrix.iloc[test_indices]
+
+
+            def train_y(target: pd.Series, train_indices: np.ndarray) -> 
pd.Series:
+                """Training target."""
+                return target.iloc[train_indices]
+
+
+            def test_y(target: pd.Series, test_indices: np.ndarray) -> 
pd.Series:
+                """Test target."""
+                return target.iloc[test_indices]
+
+
+            # --- Driver script ---
+            # from hamilton import driver
+            # import my_module
+            #
+            # dr = driver.Builder().with_modules(my_module).build()
+            # result = dr.execute(
+            #     ["train_X", "test_X", "train_y", "test_y"],
+            #     inputs={
+            #         "feature_matrix_input": pd.DataFrame({"a": range(100), 
"b": range(100)}),
+            #         "target_input": pd.Series(range(100)),
+            #     },
+            # )
+        '''),
+    ),
+    ScaffoldTemplate(
+        name="data_quality",
+        description="Data validation with @check_output.",
+        requires=frozenset({"pandas", "numpy"}),
+        code=textwrap.dedent('''\
+            """Hamilton module with data quality checks using @check_output."""
+            import pandas as pd
+            import numpy as np
+
+            from hamilton.function_modifiers import check_output
+
+
+            @check_output(
+                data_type=np.float64,
+                range=(0, None),
+            )
+            def spend(spend_raw: pd.Series) -> pd.Series:
+                """Clean spend: ensure non-negative floats."""
+                return spend_raw.abs().astype(float)
+
+
+            @check_output(
+                data_type=np.float64,
+            )
+            def revenue(revenue_raw: pd.Series) -> pd.Series:
+                """Clean revenue data."""
+                return revenue_raw.astype(float)
+
+
+            def profit(revenue: pd.Series, spend: pd.Series) -> pd.Series:
+                """Profit = revenue - spend."""
+                return revenue - spend
+
+
+            # --- Driver script ---
+            # from hamilton import driver
+            # import my_module
+            #
+            # dr = driver.Builder().with_modules(my_module).build()
+            # result = dr.execute(
+            #     ["profit"],
+            #     inputs={
+            #         "spend_raw": pd.Series([10, 20, 30]),
+            #         "revenue_raw": pd.Series([100, 200, 300]),
+            #     },
+            # )
+        '''),
+    ),
+]
+
+
+def get_available_templates(
+    preferred_libraries: set[str] | None = None,
+) -> dict[str, ScaffoldTemplate]:
+    """Return templates filtered by library availability.
+
+    If *preferred_libraries* is provided, only templates whose requirements
+    are a subset of the preferred set are returned.  Otherwise, falls back
+    to auto-detection via ``AVAILABLE_LIBS``.
+    """
+    if preferred_libraries is not None:
+        return {t.name: t for t in ALL_TEMPLATES if t.requires <= 
preferred_libraries}
+    return {
+        t.name: t
+        for t in ALL_TEMPLATES
+        if all(AVAILABLE_LIBS.get(lib, False) for lib in t.requires)
+    }
+
+
+def get_capabilities(
+    preferred_libraries: set[str] | None = None,
+) -> dict:
+    """Return capabilities, optionally filtered by user-specified libraries."""
+    return {
+        "libraries": dict(AVAILABLE_LIBS),
+        "available_scaffolds": 
sorted(get_available_templates(preferred_libraries).keys()),
+    }
diff --git a/hamilton/plugins/h_mcp/server.py b/hamilton/plugins/h_mcp/server.py
new file mode 100644
index 00000000..c89f5207
--- /dev/null
+++ b/hamilton/plugins/h_mcp/server.py
@@ -0,0 +1,328 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+import inspect
+import textwrap
+import threading
+import time
+
+from fastmcp import FastMCP
+
+from hamilton.plugins.h_mcp._helpers import (
+    build_driver_from_code,
+    cleanup_temp_module,
+    format_exception_chain,
+    serialize_results,
+)
+from hamilton.plugins.h_mcp._templates import get_available_templates, 
get_capabilities
+
+mcp = FastMCP(
+    name="Hamilton",
+    instructions=(
+        "Hamilton is a Python micro-framework where functions define DAG 
nodes. "
+        "Ask the user which data libraries they use (e.g., pandas, numpy, 
polars) "
+        "then call hamilton_capabilities with their preferred_libraries. "
+        "Use hamilton_validate_dag to check code before execution. "
+        "Workflow: ask user -> capabilities -> scaffold -> validate -> 
visualize -> correct -> execute."
+    ),
+)
+
+
[email protected]()
+def hamilton_validate_dag(code: str, config: dict | None = None) -> dict:
+    """Validate Hamilton DAG code by building the Driver.
+
+    Compiles Python source into a Hamilton DAG and checks for missing
+    dependencies, type mismatches, and circular references -- all without
+    executing the code.
+
+    Returns ``{"valid": true, "node_count": N, "nodes": [...], "inputs": 
[...]}``
+    on success or ``{"valid": false, "errors": [...]}`` on failure.
+    """
+    module = None
+    try:
+        dr, module = build_driver_from_code(code, config)
+        variables = dr.list_available_variables()
+        nodes = [v.name for v in variables if not v.is_external_input]
+        inputs = [v.name for v in variables if v.is_external_input]
+        return {
+            "valid": True,
+            "node_count": len(nodes),
+            "nodes": sorted(nodes),
+            "inputs": sorted(inputs),
+            "errors": [],
+        }
+    except Exception as exc:
+        return {
+            "valid": False,
+            "node_count": 0,
+            "nodes": [],
+            "inputs": [],
+            "errors": format_exception_chain(exc),
+        }
+    finally:
+        if module is not None:
+            cleanup_temp_module(module)
+
+
[email protected]()
+def hamilton_list_nodes(code: str, config: dict | None = None) -> dict:
+    """List all nodes in a Hamilton DAG with their types and dependencies.
+
+    Builds the DAG from source, then returns structured info for every node
+    including name, output type, tags, whether it is an external input,
+    and its required/optional dependencies.
+    """
+    module = None
+    try:
+        dr, module = build_driver_from_code(code, config)
+        variables = dr.list_available_variables()
+        from hamilton.htypes import get_type_as_string
+
+        node_list = []
+        for v in variables:
+            node_list.append(
+                {
+                    "name": v.name,
+                    "type": get_type_as_string(v.type) or "",
+                    "is_external_input": v.is_external_input,
+                    "tags": v.tags,
+                    "required_dependencies": sorted(v.required_dependencies),
+                    "optional_dependencies": sorted(v.optional_dependencies),
+                    "documentation": v.documentation,
+                }
+            )
+        return {"nodes": node_list, "errors": []}
+    except Exception as exc:
+        return {"nodes": [], "errors": format_exception_chain(exc)}
+    finally:
+        if module is not None:
+            cleanup_temp_module(module)
+
+
[email protected]()
+def hamilton_visualize(code: str, config: dict | None = None, output_format: 
str = "dot") -> str:
+    """Visualize the Hamilton DAG as DOT graph source.
+
+    Builds the DAG and returns a Graphviz DOT-language string describing
+    the dependency graph. Requires ``graphviz`` (``pip install 
"apache-hamilton[visualization]"``).
+    """
+    module = None
+    try:
+        dr, module = build_driver_from_code(code, config)
+        try:
+            dot = dr.display_all_functions(render_kwargs={"view": False})
+        except ImportError:
+            return (
+                "Error: graphviz is required for visualization. "
+                'Install with: pip install "apache-hamilton[visualization]"'
+            )
+        if dot is None:
+            return (
+                "Error: graphviz is required for visualization. "
+                'Install with: pip install "apache-hamilton[visualization]"'
+            )
+        return dot.source
+    except Exception as exc:
+        return f"Error: {exc}"
+    finally:
+        if module is not None:
+            cleanup_temp_module(module)
+
+
[email protected]()
+def hamilton_execute(
+    code: str,
+    final_vars: list[str],
+    inputs: dict | None = None,
+    config: dict | None = None,
+    timeout_seconds: int = 30,
+) -> dict:
+    """Execute a Hamilton DAG and return the requested outputs.
+
+    Builds the DAG from source, then calls ``driver.execute()`` with the
+    given ``final_vars`` and ``inputs``. Results are serialized to JSON-safe
+    strings. A timeout (default 30s) guards against long-running code.
+
+    WARNING: This executes arbitrary Python code.
+    """
+    module = None
+    result_container: dict = {}
+    error_container: dict = {}
+
+    def _run(dr, final_vars, inputs):
+        try:
+            result_container["results"] = dr.execute(final_vars=final_vars, 
inputs=inputs or {})
+        except Exception as exc:
+            error_container["error"] = exc
+
+    try:
+        dr, module = build_driver_from_code(code, config)
+
+        start = time.monotonic()
+        worker = threading.Thread(target=_run, args=(dr, final_vars, inputs))
+        worker.start()
+        worker.join(timeout=timeout_seconds)
+
+        if worker.is_alive():
+            return {"error": f"Execution timed out after {timeout_seconds}s"}
+
+        elapsed_ms = round((time.monotonic() - start) * 1000, 1)
+
+        if "error" in error_container:
+            return {
+                "error": str(error_container["error"]),
+                "execution_time_ms": elapsed_ms,
+            }
+
+        return {
+            "results": serialize_results(result_container["results"]),
+            "execution_time_ms": elapsed_ms,
+        }
+    except Exception as exc:
+        return {"error": str(exc)}
+    finally:
+        if module is not None:
+            cleanup_temp_module(module)
+
+
[email protected]()
+def hamilton_get_docs(topic: str) -> str:
+    """Get Hamilton documentation for a specific topic.
+
+    Supported topics: ``overview``, ``decorators``, ``driver``, ``builder``,
+    or any decorator name such as ``parameterize``, ``extract_columns``,
+    ``config``, ``check_output``, ``tag``, ``pipe``, ``does``, ``subdag``, etc.
+    """
+    topic = topic.strip().lower()
+
+    if topic == "overview":
+        return textwrap.dedent("""\
+            Hamilton -- Python micro-framework for dataflow DAGs
+
+            Core concepts:
+            - Each Python function defines a DAG node.
+            - The function name becomes the node name.
+            - Function parameters declare dependencies on other nodes.
+            - Return type annotations are required.
+            - The Driver compiles functions into a DAG, validates dependencies
+              and types at build time, then executes the graph.
+
+            Quick start:
+              1. Write functions in a module (my_functions.py).
+              2. Build a Driver:
+                   from hamilton import driver
+                   import my_functions
+                   dr = driver.Builder().with_modules(my_functions).build()
+              3. Execute:
+                   results = dr.execute(["output_node"], inputs={...})
+
+            Key decorators: @parameterize, @extract_columns, @config.when,
+            @check_output, @tag, @pipe, @does, @subdag
+        """)
+
+    if topic == "decorators":
+        from hamilton import function_modifiers
+
+        decorators = {
+            "parameterize": "Create multiple nodes from one function with 
different parameters.",
+            "parameterize_sources": "Parameterize by mapping different source 
nodes.",
+            "parameterize_values": "Parameterize by mapping different literal 
values.",
+            "extract_columns": "Expand a DataFrame-returning function into 
per-column nodes.",
+            "extract_fields": "Expand a dict-returning function into per-field 
nodes.",
+            "config.when": "Conditionally include a function based on config 
values.",
+            "check_output": "Attach data quality validators to a node's 
output.",
+            "tag": "Add metadata tags to a node.",
+            "tag_outputs": "Tag specific outputs of a multi-output function.",
+            "pipe": "Chain transforms: pass a node's output through a 
pipeline.",
+            "does": "Replace function body with another callable.",
+            "subdag": "Include an entire sub-DAG as a namespace.",
+            "inject": "Inject specific values or sources into function 
parameters.",
+            "schema": "Attach schema metadata to a node.",
+            "cache": "Mark a node for caching.",
+            "load_from": "Load data from an external source (data loader).",
+            "save_to": "Save data to an external destination (data saver).",
+        }
+        lines = ["Available Hamilton decorators:\n"]
+        for name, desc in decorators.items():
+            lines.append(f"  @{name} -- {desc}")
+        lines.append("\nUse hamilton_get_docs('<decorator_name>') for full 
documentation.")
+        return "\n".join(lines)
+
+    if topic in ("driver", "builder"):
+        from hamilton import driver as driver_mod
+
+        doc = inspect.getdoc(driver_mod.Builder)
+        return doc or "No documentation found for Builder."
+
+    # Try to find a decorator by name in function_modifiers
+    from hamilton import function_modifiers
+
+    obj = getattr(function_modifiers, topic, None)
+    if obj is not None:
+        doc = inspect.getdoc(obj)
+        if doc:
+            return f"@{topic}\n\n{doc}"
+        # For class-based decorators, try the class itself
+        if isinstance(obj, type):
+            doc = inspect.getdoc(obj)
+            if doc:
+                return f"@{topic}\n\n{doc}"
+
+    return (
+        f"Unknown topic '{topic}'. "
+        "Supported: overview, decorators, driver, builder, "
+        "or any decorator name (parameterize, extract_columns, config, "
+        "check_output, tag, pipe, does, subdag, etc.)"
+    )
+
+
[email protected]()
+def hamilton_capabilities(preferred_libraries: list[str] | None = None) -> 
dict:
+    """Report which optional libraries are installed and which features are 
available.
+
+    Call this first to discover the environment before generating code.
+    If the user specifies which libraries they use, pass them as
+    ``preferred_libraries`` (e.g., ``["pandas", "numpy"]``) to filter
+    scaffold patterns accordingly.
+    """
+    prefs = set(preferred_libraries) if preferred_libraries is not None else 
None
+    return get_capabilities(prefs)
+
+
[email protected]()
+def hamilton_scaffold(
+    pattern: str,
+    preferred_libraries: list[str] | None = None,
+) -> str:
+    """Generate a starter Hamilton module for a given pattern.
+
+    Available patterns depend on installed or preferred libraries.
+    Pass ``preferred_libraries`` to filter to patterns matching the
+    user's environment (e.g., ``["pandas"]``).
+    """
+    prefs = set(preferred_libraries) if preferred_libraries is not None else 
None
+    templates = get_available_templates(prefs)
+    pattern = pattern.strip().lower()
+    template = templates.get(pattern)
+    if template is None:
+        available = ", ".join(sorted(templates.keys()))
+        return f"Unknown pattern '{pattern}'. Available patterns: {available}"
+
+    return template.code
diff --git a/pyproject.toml b/pyproject.toml
index d5ee4bc8..a87f80c9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -79,6 +79,7 @@ rich = ["rich"]
 sdk = ["sf-hamilton-sdk"]
 slack = ["slack-sdk"]
 
+mcp = ["fastmcp>=3.0.0,<4"]
 tqdm = ["tqdm"]
 ui = ["sf-hamilton-ui"]
 
@@ -170,6 +171,7 @@ docs = [
   "sphinx-rtd-theme",
   "sphinx-simplepdf",
   "sphinx-sitemap",
+  "sphinxcontrib-mermaid",
   "tqdm",
   "xgboost",
 ]
@@ -179,6 +181,7 @@ h_experiments = 
"hamilton.plugins.h_experiments.__main__:main"
 hamilton = "hamilton.cli.__main__:cli"
 hamilton-admin-build-ui = "ui.admin:build_ui"
 hamilton-admin-build-and-publish = "ui.admin:build_and_publish"
+hamilton-mcp = "hamilton.plugins.h_mcp.__main__:main"
 hamilton-disable-autoload-extensions = 
"hamilton.registry:config_disable_autoload"
 hamilton-enable-autoload-extensions = 
"hamilton.registry:config_enable_autoload"
 
diff --git a/tests/plugins/test_h_mcp.py b/tests/plugins/test_h_mcp.py
new file mode 100644
index 00000000..de06deca
--- /dev/null
+++ b/tests/plugins/test_h_mcp.py
@@ -0,0 +1,365 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Tests for the Hamilton MCP server plugin.
+
+These tests call the tool functions directly (no MCP client needed).
+"""
+
+from __future__ import annotations
+
+import sys
+import textwrap
+
+import pytest
+
+# Skip entire module if fastmcp is not available (optional dependency)
+pytest.importorskip("fastmcp")
+
+from hamilton.plugins.h_mcp import server as mcp_server
+from hamilton.plugins.h_mcp._helpers import (
+    build_driver_from_code,
+    cleanup_temp_module,
+    format_validation_errors,
+    serialize_results,
+)
+from hamilton.plugins.h_mcp._templates import (
+    HAS_NUMPY,
+    HAS_PANDAS,
+    get_available_templates,
+    get_capabilities,
+)
+
+# In FastMCP v3, @mcp.tool() returns the original function unchanged,
+# so we can call the tool functions directly.
+hamilton_validate_dag = mcp_server.hamilton_validate_dag
+hamilton_list_nodes = mcp_server.hamilton_list_nodes
+hamilton_get_docs = mcp_server.hamilton_get_docs
+hamilton_scaffold = mcp_server.hamilton_scaffold
+hamilton_capabilities = mcp_server.hamilton_capabilities
+
+VALID_CODE = textwrap.dedent("""\
+    def spend(spend_raw: float) -> float:
+        \"\"\"Clean spend data.\"\"\"
+        return abs(spend_raw)
+
+    def avg_spend(spend: float) -> float:
+        \"\"\"Compute average.\"\"\"
+        return spend / 2
+""")
+
+INVALID_CODE_SYNTAX = "def foo( -> int: return 1"
+
+SIMPLE_CODE = textwrap.dedent("""\
+    def a(a_input: int) -> int:
+        return a_input + 1
+
+    def b(a: int) -> int:
+        return a * 2
+""")
+
+PANDAS_CODE = textwrap.dedent("""\
+    import pandas as pd
+
+    def spend(spend_raw: pd.Series) -> pd.Series:
+        \"\"\"Clean spend data.\"\"\"
+        return spend_raw.abs()
+
+    def avg_spend(spend: pd.Series) -> pd.Series:
+        \"\"\"Rolling average.\"\"\"
+        return spend.rolling(3).mean()
+""")
+
+
+class TestValidateDag:
+    def test_valid_code(self):
+        result = hamilton_validate_dag(VALID_CODE)
+        assert result["valid"] is True
+        assert result["node_count"] > 0
+        assert "spend" in result["nodes"]
+        assert "avg_spend" in result["nodes"]
+        assert "spend_raw" in result["inputs"]
+        assert result["errors"] == []
+
+    def test_syntax_error(self):
+        result = hamilton_validate_dag(INVALID_CODE_SYNTAX)
+        assert result["valid"] is False
+        assert len(result["errors"]) > 0
+        assert result["errors"][0]["type"] == "SyntaxError"
+        assert result["node_count"] == 0
+
+    def test_simple_code(self):
+        result = hamilton_validate_dag(SIMPLE_CODE)
+        assert result["valid"] is True
+        assert "a" in result["nodes"]
+        assert "b" in result["nodes"]
+        assert "a_input" in result["inputs"]
+
+    @pytest.mark.skipif(not HAS_PANDAS, reason="pandas not installed")
+    def test_valid_pandas_code(self):
+        result = hamilton_validate_dag(PANDAS_CODE)
+        assert result["valid"] is True
+        assert "spend" in result["nodes"]
+        assert "avg_spend" in result["nodes"]
+
+
+class TestListNodes:
+    def test_basic(self):
+        result = hamilton_list_nodes(VALID_CODE)
+        assert result["errors"] == []
+        names = [n["name"] for n in result["nodes"]]
+        assert "spend" in names
+        assert "avg_spend" in names
+
+    def test_node_details(self):
+        result = hamilton_list_nodes(SIMPLE_CODE)
+        assert result["errors"] == []
+        node_map = {n["name"]: n for n in result["nodes"]}
+        assert "a" in node_map
+        assert node_map["a"]["is_external_input"] is False
+        assert "a_input" in node_map["a"]["required_dependencies"]
+
+    def test_syntax_error(self):
+        result = hamilton_list_nodes(INVALID_CODE_SYNTAX)
+        assert result["nodes"] == []
+        assert len(result["errors"]) > 0
+
+
+class TestGetDocs:
+    def test_overview(self):
+        result = hamilton_get_docs("overview")
+        assert len(result) > 0
+        assert "Hamilton" in result
+        assert "DAG" in result
+
+    def test_decorators_list(self):
+        result = hamilton_get_docs("decorators")
+        assert "parameterize" in result
+        assert "extract_columns" in result
+
+    def test_specific_decorator(self):
+        result = hamilton_get_docs("parameterize")
+        assert len(result) > 0
+        assert "parameterize" in result.lower()
+
+    def test_driver_topic(self):
+        result = hamilton_get_docs("driver")
+        assert len(result) > 0
+
+    def test_unknown_topic(self):
+        result = hamilton_get_docs("nonexistent_topic_xyz")
+        assert "Unknown topic" in result
+
+
+class TestCapabilities:
+    def test_returns_libraries(self):
+        result = hamilton_capabilities()
+        assert "libraries" in result
+        libs = result["libraries"]
+        assert isinstance(libs, dict)
+        for key, val in libs.items():
+            assert isinstance(key, str)
+            assert isinstance(val, bool)
+
+    def test_returns_available_scaffolds(self):
+        result = hamilton_capabilities()
+        assert "available_scaffolds" in result
+        scaffolds = result["available_scaffolds"]
+        assert isinstance(scaffolds, list)
+        assert "basic_pure_python" in scaffolds
+
+    def test_libraries_match_detection(self):
+        result = hamilton_capabilities()
+        assert result["libraries"]["pandas"] == HAS_PANDAS
+        assert result["libraries"]["numpy"] == HAS_NUMPY
+
+
+class TestTemplateAvailability:
+    def test_pure_python_always_available(self):
+        templates = get_available_templates()
+        assert "basic_pure_python" in templates
+
+    @pytest.mark.skipif(not HAS_PANDAS, reason="pandas not installed")
+    def test_pandas_templates_when_installed(self):
+        templates = get_available_templates()
+        assert "basic" in templates
+        assert "data_pipeline" in templates
+        assert "parameterized" in templates
+        assert "config_based" in templates
+
+    @pytest.mark.skipif(not (HAS_PANDAS and HAS_NUMPY), reason="pandas and 
numpy not installed")
+    def test_numpy_pandas_templates_when_installed(self):
+        templates = get_available_templates()
+        assert "ml_pipeline" in templates
+        assert "data_quality" in templates
+
+    def test_monkeypatch_has_pandas_false(self, monkeypatch):
+        from hamilton.plugins.h_mcp import _templates
+
+        monkeypatch.setitem(_templates.AVAILABLE_LIBS, "pandas", False)
+        templates = get_available_templates()
+        for name, t in templates.items():
+            assert "pandas" not in t.requires, (
+                f"Template '{name}' requires pandas but shouldn't be available"
+            )
+
+    def test_monkeypatch_capabilities_reflects_change(self, monkeypatch):
+        from hamilton.plugins.h_mcp import _templates
+
+        monkeypatch.setitem(_templates.AVAILABLE_LIBS, "pandas", False)
+        caps = get_capabilities()
+        assert caps["libraries"]["pandas"] is False
+        assert "basic" not in caps["available_scaffolds"]
+        assert "basic_pure_python" in caps["available_scaffolds"]
+
+
+class TestPreferredLibraries:
+    def test_capabilities_with_preferred_libraries(self):
+        result = hamilton_capabilities(preferred_libraries=["pandas"])
+        scaffolds = result["available_scaffolds"]
+        assert "basic_pure_python" in scaffolds
+        assert "basic" in scaffolds
+        assert "data_pipeline" in scaffolds
+        # ml_pipeline requires pandas AND numpy, so should NOT appear
+        assert "ml_pipeline" not in scaffolds
+
+    def test_capabilities_preferred_empty_list(self):
+        result = hamilton_capabilities(preferred_libraries=[])
+        scaffolds = result["available_scaffolds"]
+        assert scaffolds == ["basic_pure_python"]
+
+    def test_capabilities_no_preference_uses_detection(self):
+        """Passing None falls back to auto-detection (backward compatible)."""
+        result_none = hamilton_capabilities(preferred_libraries=None)
+        result_default = hamilton_capabilities()
+        assert result_none["available_scaffolds"] == 
result_default["available_scaffolds"]
+
+    def test_scaffold_with_preferred_libraries(self):
+        code = hamilton_scaffold("basic", preferred_libraries=["pandas"])
+        assert "def raw_data" in code
+
+    def test_scaffold_preference_filters_correctly(self):
+        """ml_pipeline requires pandas+numpy; passing only pandas should 
reject it."""
+        result = hamilton_scaffold("ml_pipeline", 
preferred_libraries=["pandas"])
+        assert "Unknown pattern" in result
+
+    def test_get_available_templates_with_preferences(self):
+        templates = get_available_templates(preferred_libraries={"pandas", 
"numpy"})
+        assert "basic_pure_python" in templates
+        assert "basic" in templates
+        assert "ml_pipeline" in templates
+        assert "data_quality" in templates
+
+    def test_get_available_templates_empty_preferences(self):
+        templates = get_available_templates(preferred_libraries=set())
+        assert list(templates.keys()) == ["basic_pure_python"]
+
+
+class TestScaffold:
+    def test_pure_python_pattern(self):
+        code = hamilton_scaffold("basic_pure_python")
+        assert "def raw_value" in code
+        assert "def doubled" in code
+        result = hamilton_validate_dag(code)
+        assert result["valid"] is True
+
+    @pytest.mark.skipif(not HAS_PANDAS, reason="pandas not installed")
+    def test_basic_pattern(self):
+        code = hamilton_scaffold("basic")
+        assert "def raw_data" in code
+        assert "def cleaned" in code
+        result = hamilton_validate_dag(code)
+        assert result["valid"] is True
+
+    @pytest.mark.skipif(not HAS_PANDAS, reason="pandas not installed")
+    def test_parameterized_pattern(self):
+        code = hamilton_scaffold("parameterized")
+        assert "parameterize" in code
+        assert "rolling_mean" in code
+
+    @pytest.mark.skipif(not HAS_PANDAS, reason="pandas not installed")
+    def test_config_based_pattern(self):
+        code = hamilton_scaffold("config_based")
+        assert "config.when" in code
+
+    @pytest.mark.skipif(not HAS_PANDAS, reason="pandas not installed")
+    def test_data_pipeline_pattern(self):
+        code = hamilton_scaffold("data_pipeline")
+        assert "def raw_data" in code
+        result = hamilton_validate_dag(code)
+        assert result["valid"] is True
+
+    @pytest.mark.skipif(not (HAS_PANDAS and HAS_NUMPY), reason="pandas and 
numpy not installed")
+    def test_ml_pipeline_pattern(self):
+        code = hamilton_scaffold("ml_pipeline")
+        assert "feature_matrix" in code
+        result = hamilton_validate_dag(code)
+        assert result["valid"] is True
+
+    @pytest.mark.skipif(not (HAS_PANDAS and HAS_NUMPY), reason="pandas and 
numpy not installed")
+    def test_data_quality_pattern(self):
+        code = hamilton_scaffold("data_quality")
+        assert "check_output" in code
+
+    def test_unknown_pattern(self):
+        result = hamilton_scaffold("nonexistent")
+        assert "Unknown pattern" in result
+
+    def test_all_available_patterns_valid(self):
+        """Every available scaffold pattern should produce code that 
validates."""
+        for name in get_available_templates():
+            code = hamilton_scaffold(name)
+            result = hamilton_validate_dag(code)
+            assert result["valid"] is True, f"Pattern '{name}' did not 
validate: {result}"
+
+
+class TestHelpers:
+    def test_build_and_cleanup(self):
+        dr, module = build_driver_from_code(SIMPLE_CODE)
+        module_name = module.__name__
+        assert module_name in sys.modules
+        cleanup_temp_module(module)
+        assert module_name not in sys.modules
+
+    def test_format_validation_errors_syntax(self):
+        try:
+            compile("def foo( -> int:", "<test>", "exec")
+        except SyntaxError as exc:
+            errors = format_validation_errors(exc)
+            assert len(errors) == 1
+            assert errors[0]["type"] == "SyntaxError"
+
+    def test_format_validation_errors_value(self):
+        exc = ValueError("something went wrong")
+        errors = format_validation_errors(exc)
+        assert errors[0]["type"] == "ValueError"
+        assert "something went wrong" in errors[0]["message"]
+
+    def test_serialize_results_basic(self):
+        results = {"count": 42, "name": "test"}
+        serialized = serialize_results(results)
+        assert serialized["count"] == "42"
+        assert serialized["name"] == "test"
+
+    @pytest.mark.skipif(not HAS_PANDAS, reason="pandas not installed")
+    def test_serialize_results_pandas(self):
+        import pandas as pd
+
+        results = {"series": pd.Series([1, 2, 3]), "df": pd.DataFrame({"a": 
[1]})}
+        serialized = serialize_results(results)
+        assert isinstance(serialized["series"], dict)
+        assert isinstance(serialized["df"], dict)
diff --git a/ui/sdk/requirements.txt b/ui/sdk/requirements.txt
index c21ff08d..403b7e6f 100644
--- a/ui/sdk/requirements.txt
+++ b/ui/sdk/requirements.txt
@@ -1,9 +1,9 @@
 aiohttp
+# apache-hamilton>=1.89.0
 click
 gitpython
 jinja2 # for SDK, todo -- move out
 loguru # for init, todo -- move out
 posthog
 requests
-# sf-hamilton>=1.43.0
 sqlglot

Reply via email to